package handler import ( "errors" "net/http" "strconv" "fashionapi/internal/dto" "fashionapi/internal/service" "github.com/gin-gonic/gin" ) // IngestHandler 爬虫上报入口(:8092 的 /admin/internal/ingest)。 // // 安全:不挂后台登录 cookie 中间件,改走 HMAC 验签中间件(middleware.IngestAuth); // 因此多节点爬虫可直连上报,无需回环绑定。接口本身很薄:验签 → Submit 入队 → 立即 202。 type IngestHandler struct { ingest *service.IngestService brands service.BrandService } // NewIngestHandler 创建入库 handler。 func NewIngestHandler(ingest *service.IngestService, brands service.BrandService) *IngestHandler { return &IngestHandler{ingest: ingest, brands: brands} } // Submit 接收单场走秀上报,入队后返回 202 Accepted(异步处理)。 func (h *IngestHandler) Submit(c *gin.Context) { var p dto.RunwayIngest if err := c.ShouldBindJSON(&p); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json: " + err.Error()}) return } id, err := h.ingest.Submit(c.Request.Context(), p) if err != nil { // 业务错误(缺字段 / 未知品牌等)映射为 4xx,其余 5xx if errors.Is(err, service.ErrBrandNotFound) || isClientErr(err) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "enqueue failed"}) return } c.JSON(http.StatusAccepted, gin.H{"job_id": id, "status": "queued"}) } // CrawlBrands 爬虫取任务接口(GET /admin/internal/crawl/brands,复用 IngestAuth HMAC 验签)。 // 返回所有可抓取品牌的 brand_uid(hashid)+ 英文品牌名,供 spider 拼 vogue 抓取 URL 并上送 ingest。 func (h *IngestHandler) CrawlBrands(c *gin.Context) { // 支持 ?brand= 单品牌调试:解析失败或非正数时回落全量。 var brandID uint32 if v := c.Query("brand"); v != "" { if id, err := strconv.ParseUint(v, 10, 32); err == nil { brandID = uint32(id) } } brands, err := h.brands.CrawlTasks(c.Request.Context(), brandID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "fetch crawl tasks failed"}) return } c.JSON(http.StatusOK, gin.H{"brands": brands}) } func isClientErr(err error) bool { // service 层 NewError 带 http 状态码;这里简单按消息前缀判断,避免暴露内部类型。 if err == nil { return false } msg := err.Error() return len(msg) > 0 && msg == "brand_uid required" }