package handler import ( "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" "github.com/wangjia/jiu/backend/internal/model" ) // 默认话术(保质期/储存方式空时使用) const ( defaultShelfLife = "无限期(适饮)" defaultStorage = "阴凉干燥、避光保存" ) type PublicHandler struct { db *gorm.DB } func NewPublicHandler(db *gorm.DB) *PublicHandler { return &PublicHandler{db: db} } // GetProduct GET /api/v1/public/products/:public_id (no auth) func (h *PublicHandler) GetProduct(c *gin.Context) { publicID := c.Param("public_id") var product model.Product if err := h.db.Where("public_id = ? AND deleted_at IS NULL", publicID). Preload("Images"). Preload("Origin"). Preload("ShelfLife"). Preload("Storage"). Preload("DescriptionDoc"). First(&product).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } // Fetch shop public info var shop model.Shop shopData := gin.H{} if err := h.db.Where("id = ?", product.ShopID).First(&shop).Error; err == nil { shopData = gin.H{ "name": shop.Name, "code": shop.Code, "address": shop.Address, "phone": shop.Phone, "business_hours": shop.BusinessHours, } } // Fetch latest inventory batch for this product var inv model.Inventory batchData := gin.H(nil) if err := h.db.Where("product_id = ? AND quantity > 0 AND deleted_at IS NULL", product.ID). Order("created_at DESC"). First(&inv).Error; err == nil { var pdStr *string if inv.ProductionDate != nil { s := inv.ProductionDate.Time.Format("2006-01-02") pdStr = &s } batchData = gin.H{ "production_date": pdStr, "batch_no": inv.BatchNo, "in_stock_date": inv.CreatedAt.Format("2006-01-02"), } } // 产地:空串表示无,前端不展示该行 origin := "" if product.Origin != nil { origin = product.Origin.Name } // 保质期:无关联时用默认话术 shelfLife := defaultShelfLife if product.ShelfLife != nil { shelfLife = product.ShelfLife.Name } // 储存方式:无关联时用默认话术 storage := defaultStorage if product.Storage != nil { storage = product.Storage.Name } // 介绍三级回退:描述文档 → 旧 Description → 通用中性兜底 descTitle, descBody, descKeywords := buildDescription(product) c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "id": product.ID, "name": product.Name, "series": product.Series, "spec": product.Spec, "brand": product.Brand, "unit": product.Unit, "description": descBody, "description_title": descTitle, "description_keywords": descKeywords, "origin": origin, "shelf_life": shelfLife, "storage": storage, "images": product.Images, "shop": shopData, "batch": batchData, }, }) } // buildDescription 商品介绍三级回退逻辑 // 1. DescriptionDoc.Content(结构化介绍文档) // 2. Product.Description(旧自由文本) // 3. 通用中性兜底(用真实字段拼,不含品牌专属词) func buildDescription(p model.Product) (title, body string, keywords []string) { // 1. 描述文档 if p.DescriptionDoc != nil && p.DescriptionDoc.Content != "" { return p.DescriptionDoc.Title, p.DescriptionDoc.Content, buildKeywords(p) } // 2. 旧 Description 自由文本 if p.Description != "" { return "商品介绍", p.Description, buildKeywords(p) } // 3. 通用中性兜底 parts := make([]string, 0, 4) if p.Brand != "" && p.Name != "" { parts = append(parts, fmt.Sprintf("%s%s", p.Brand, p.Name)) } else if p.Name != "" { parts = append(parts, p.Name) } if p.Series != "" { parts = append(parts, fmt.Sprintf("%s系列", p.Series)) } if p.Spec != "" { parts = append(parts, fmt.Sprintf("规格%s", p.Spec)) } intro := strings.Join(parts, ",") if intro != "" { intro += "。" } intro += "本商品由门店正品供应,支持扫码验真,请认准官方渠道。" return "商品介绍", intro, buildKeywords(p) } // buildKeywords 从真实商品属性派生关键词 chips func buildKeywords(p model.Product) []string { kws := make([]string, 0, 4) if p.Brand != "" { kws = append(kws, p.Brand) } if p.Series != "" { kws = append(kws, p.Series) } kws = append(kws, "正品保障", "扫码验真") return kws } // GetRelease GET /api/v1/public/release (no auth) func (h *PublicHandler) GetRelease(c *gin.Context) { cfg, err := loadVersionConfig() if err != nil { c.JSON(http.StatusOK, gin.H{ "version": "1.0.0", "release_notes": "", "download_urls": gin.H{"web": "https://jiu.51yanmei.com/app"}, }) return } c.JSON(http.StatusOK, gin.H{ "version": cfg.Version, "release_notes": cfg.ReleaseNotes, "download_urls": cfg.DownloadURLs, }) }