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