You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

262 lines
8.4 KiB

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)})
}