package console import ( "encoding/json" "strings" "time" "yunyan/lego/sys/postgres" "yunyan/pb" "github.com/gin-gonic/gin" ) // ============================ 应用商品配置 (appgoods) ============================ // // 「应用商品配置」页(admin appgoods.vue)的后端:读写 console 主库的 app_goods 表—— // 即各应用的**内购商品**(VIP 时长包、AI/会议/翻译资源点包等)及其多语言售卖信息。 // // 与「应用环境配置」(appcfg)的差别: // - appcfg 按「应用 × 部署环境」写各应用**业务库**; // - 商品是应用级的售卖定义,与部署在哪个区域无关,故按 **app_name** 隔离、统一落 console 公共库, // 同一应用的多个区域部署共用一份。 // // 多语言:前端按语言逐条编辑(语言 / 名称 / 货币符号 / 价格),后端序列化成 // pb.DBGoods.localname 同构的 JSON 存 Localname 列,列表/详情再解析回 locales 下发, // 前端不必碰 JSON 字符串。 // GoodsLocale 单语言售卖信息,字段与 pb.DBGoodsLocal 的 json 名一致。 type GoodsLocale struct { Name string `json:"name"` // 该语言下的商品名 Currency string `json:"currency"` // 货币符号,如 ¥ / $ Price float64 `json:"price"` // 该语言(地区)售价 } // appGoodsReq 新增/更新商品的请求体(locales 为结构化多语言,落库时序列化进 localname)。 type appGoodsReq struct { Id uint32 `json:"id"` AppName string `json:"app_name"` GoodsId string `json:"goods_id"` Enable bool `json:"enable"` Name string `json:"name"` Price int32 `json:"price"` Usagetype int32 `json:"usagetype"` Ainum int64 `json:"ainum"` Meetnum int64 `json:"meetnum"` Tradenum int64 `json:"tradenum"` Viptime int32 `json:"viptime"` Locales map[string]*GoodsLocale `json:"locales"` Sort int32 `json:"sort"` Remark string `json:"remark"` } // appGoodsView 下发给前端的商品:库行 + 解析好的多语言(localname 原串一并保留,便于排查)。 type appGoodsView struct { *AppGoods Locales map[string]*GoodsLocale `json:"locales"` } // parseGoodsLocales 解析 localname JSON;空串或坏数据都返回空 map(坏数据不该让整页打不开)。 func parseGoodsLocales(s string) map[string]*GoodsLocale { out := make(map[string]*GoodsLocale) if strings.TrimSpace(s) == "" { return out } if err := json.Unmarshal([]byte(s), &out); err != nil { return make(map[string]*GoodsLocale) } for k, v := range out { if v == nil { delete(out, k) } } return out } // marshalGoodsLocales 序列化多语言为 localname JSON:语言码去空白、丢弃空码与空项; // 一项都没有时存空串(而不是 "{}"),与历史数据保持一致。 func marshalGoodsLocales(in map[string]*GoodsLocale) (string, error) { clean := make(map[string]*GoodsLocale, len(in)) for k, v := range in { lang := strings.TrimSpace(k) if lang == "" || v == nil { continue } clean[lang] = &GoodsLocale{ Name: strings.TrimSpace(v.Name), Currency: strings.TrimSpace(v.Currency), Price: v.Price, } } if len(clean) == 0 { return "", nil } b, err := json.Marshal(clean) if err != nil { return "", err } return string(b), nil } // applyGoodsReq 把请求体写进模型(不含 Id/AppName/时间戳,由调用方按新增/更新分别处理)。 func applyGoodsReq(m *AppGoods, req *appGoodsReq, localname string) { m.GoodsId = strings.TrimSpace(req.GoodsId) m.Enable = req.Enable m.Name = strings.TrimSpace(req.Name) m.Price = req.Price m.Usagetype = req.Usagetype m.Ainum = req.Ainum m.Meetnum = req.Meetnum m.Tradenum = req.Tradenum m.Viptime = req.Viptime m.Localname = localname m.Sort = req.Sort m.Remark = strings.TrimSpace(req.Remark) } // checkGoodsReq 校验必填并归一,返回错误说明(空串=通过)。 func checkGoodsReq(req *appGoodsReq) string { req.AppName = strings.TrimSpace(req.AppName) req.GoodsId = strings.TrimSpace(req.GoodsId) if req.AppName == "" { return "app_name 必填" } if req.GoodsId == "" { return "商品ID(goods_id) 必填" } return "" } // appGoodsList 列出某应用的全部商品(含多语言)。 func (this *serverComp) appGoodsList(c *gin.Context) { var req struct { AppName string `json:"app_name"` } _ = c.ShouldBindJSON(&req) req.AppName = strings.TrimSpace(req.AppName) if req.AppName == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "app_name 必填") return } models, err := this.module.model.listGoods(req.AppName) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } items := make([]*appGoodsView, 0, len(models)) for _, m := range models { items = append(items, &appGoodsView{AppGoods: m, Locales: parseGoodsLocales(m.Localname)}) } writeOK(c, gin.H{"items": items}) } // appGoodsAdd 新增商品:同一应用内 goods_id 不可重复。 func (this *serverComp) appGoodsAdd(c *gin.Context) { var req appGoodsReq if err := c.ShouldBindJSON(&req); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } if msg := checkGoodsReq(&req); msg != "" { writeErr(c, pb.ErrorCode_ReqParameterError, msg) return } dup, err := this.module.model.getGoodsByGid(req.AppName, req.GoodsId) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if dup != nil { writeErr(c, pb.ErrorCode_ReqParameterError, "该应用下商品ID已存在: "+req.GoodsId) return } localname, err := marshalGoodsLocales(req.Locales) if err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, "多语言配置格式错误: "+err.Error()) return } now := time.Now().Unix() m := &AppGoods{AppName: req.AppName, Createtime: now, Updatetime: now} applyGoodsReq(m, &req, localname) if err := this.module.model.addGoods(m); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &appGoodsView{AppGoods: m, Locales: parseGoodsLocales(m.Localname)}) } // appGoodsUpdate 更新商品:应用归属不可改;改了 goods_id 同样要在应用内查重。 func (this *serverComp) appGoodsUpdate(c *gin.Context) { var req appGoodsReq if err := c.ShouldBindJSON(&req); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } if msg := checkGoodsReq(&req); msg != "" { writeErr(c, pb.ErrorCode_ReqParameterError, msg) return } if req.Id == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填") return } old, err := this.module.model.getGoods(req.Id) if err == postgres.ErrNoDocuments || (err == nil && old.Id == 0) { writeErr(c, pb.ErrorCode_ReqParameterError, "商品不存在") return } if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if old.AppName != req.AppName { writeErr(c, pb.ErrorCode_ReqParameterError, "商品归属应用不可修改") return } if old.GoodsId != strings.TrimSpace(req.GoodsId) { dup, err := this.module.model.getGoodsByGid(req.AppName, req.GoodsId) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if dup != nil && dup.Id != req.Id { writeErr(c, pb.ErrorCode_ReqParameterError, "该应用下商品ID已存在: "+req.GoodsId) return } } localname, err := marshalGoodsLocales(req.Locales) if err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, "多语言配置格式错误: "+err.Error()) return } applyGoodsReq(old, &req, localname) old.Updatetime = time.Now().Unix() if err := this.module.model.saveGoods(old); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &appGoodsView{AppGoods: old, Locales: parseGoodsLocales(old.Localname)}) } // appGoodsDel 按 id 批量删除某应用的商品。 func (this *serverComp) appGoodsDel(c *gin.Context) { var req struct { AppName string `json:"app_name"` Ids []uint32 `json:"ids"` } _ = c.ShouldBindJSON(&req) req.AppName = strings.TrimSpace(req.AppName) if req.AppName == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "app_name 必填") return } if len(req.Ids) == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "ids 必填") return } if err := this.module.model.delGoods(req.AppName, req.Ids); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, gin.H{"deleted": len(req.Ids)}) }