package admin import ( "bytes" "encoding/json" "fmt" "net/http" "strconv" "time" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/repository/mysql/model" ) type listSystemTitlesRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` Name string `form:"name"` Status *int32 `form:"status"` } type listSystemTitlesResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []*model.SystemTitles `json:"list"` } // ListSystemTitles 系统称号列表 func (h *handler) ListSystemTitles() core.HandlerFunc { return func(ctx core.Context) { req := new(listSystemTitlesRequest) rsp := new(listSystemTitlesResponse) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageSize > 100 { req.PageSize = 100 } q := h.readDB.SystemTitles.WithContext(ctx.RequestContext()).ReadDB() if req.Name != "" { q = q.Where(h.readDB.SystemTitles.Name.Like("%" + req.Name + "%")) } if req.Status != nil { q = q.Where(h.readDB.SystemTitles.Status.Eq(*req.Status)) } total, err := q.Count() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30101, err.Error())) return } rows, err := q.Order(h.readDB.SystemTitles.ID.Desc()). Offset((req.Page-1)*req.PageSize).Limit(req.PageSize).Find() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30102, err.Error())) return } rsp.Page = req.Page rsp.PageSize = req.PageSize rsp.Total = total rsp.List = rows ctx.Payload(rsp) } } type createSystemTitleRequest struct { Name string `json:"name" binding:"required,min=1"` Description string `json:"description"` Status int32 `json:"status"` ObtainRulesJSON string `json:"obtain_rules_json"` ScopesJSON string `json:"scopes_json"` } type modifySystemTitleRequest struct { Name string `json:"name"` Description string `json:"description"` Status *int32 `json:"status"` ObtainRulesJSON string `json:"obtain_rules_json"` ScopesJSON string `json:"scopes_json"` } type simpleMessageResponseTitle struct { Message string `json:"message"` } func (h *handler) CreateSystemTitle() core.HandlerFunc { return func(ctx core.Context) { var req createSystemTitleRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } if req.ObtainRulesJSON == "" { req.ObtainRulesJSON = "{}" } if req.ScopesJSON == "" { req.ScopesJSON = "{}" } it := &model.SystemTitles{ Name: req.Name, Description: req.Description, Status: req.Status, ObtainRulesJSON: req.ObtainRulesJSON, ScopesJSON: req.ScopesJSON, } if err := h.writeDB.SystemTitles.WithContext(ctx.RequestContext()).Create(it); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30106, "创建称号失败")) return } ctx.Payload(&simpleMessageResponseTitle{Message: "创建成功"}) } } func (h *handler) ModifySystemTitle() core.HandlerFunc { return func(ctx core.Context) { titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64) if err != nil || titleID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID")) return } var req modifySystemTitleRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } row, err := h.readDB.SystemTitles.WithContext(ctx.RequestContext()).Where(h.readDB.SystemTitles.ID.Eq(titleID)).First() if err != nil || row == nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30107, "称号不存在")) return } if req.Name != "" { row.Name = req.Name } row.Description = req.Description if req.Status != nil { row.Status = *req.Status } if req.ObtainRulesJSON != "" { row.ObtainRulesJSON = req.ObtainRulesJSON } if req.ScopesJSON != "" { row.ScopesJSON = req.ScopesJSON } if err := h.writeDB.SystemTitles.Save(row); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30108, "修改称号失败")) return } ctx.Payload(&simpleMessageResponseTitle{Message: "修改成功"}) } } func (h *handler) DeleteSystemTitle() core.HandlerFunc { return func(ctx core.Context) { titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64) if err != nil || titleID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID")) return } del := &model.SystemTitles{ID: titleID} if _, err := h.writeDB.SystemTitles.Delete(del); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30109, "删除称号失败")) return } ctx.Payload(&simpleMessageResponseTitle{Message: "删除成功"}) } } type listEffectsResponse struct { List []*model.SystemTitleEffects `json:"list"` Total int64 `json:"total"` } func (h *handler) ListSystemTitleEffects() core.HandlerFunc { return func(ctx core.Context) { titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64) if err != nil || titleID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID")) return } rows, err := h.readDB.SystemTitleEffects.WithContext(ctx.RequestContext()).Where(h.readDB.SystemTitleEffects.TitleID.Eq(titleID)).Order(h.readDB.SystemTitleEffects.Sort).Find() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30110, err.Error())) return } total := int64(len(rows)) ctx.Payload(&listEffectsResponse{List: rows, Total: total}) } } type createEffectRequest struct { EffectType int32 `json:"effect_type" binding:"required"` ParamsJSON string `json:"params_json" binding:"required"` StackingStrategy int32 `json:"stacking_strategy"` CapValueX1000 int32 `json:"cap_value_x1000"` ScopesJSON string `json:"scopes_json"` Sort int32 `json:"sort"` Status int32 `json:"status"` } func (h *handler) CreateSystemTitleEffect() core.HandlerFunc { return func(ctx core.Context) { titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64) if err != nil || titleID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID")) return } var req createEffectRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } if req.ParamsJSON == "" { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "params_json不能为空")); return } if req.ScopesJSON == "" { req.ScopesJSON = "{}" } existed, _ := h.readDB.SystemTitleEffects.WithContext(ctx.RequestContext()). Where(h.readDB.SystemTitleEffects.TitleID.Eq(titleID)). Where(h.readDB.SystemTitleEffects.EffectType.Eq(req.EffectType)).First() if existed != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30115, "同类型效果已存在")) return } sanitized, verr := validateEffectParams(req.EffectType, req.ParamsJSON) if verr != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, verr.Error())) return } ef := &model.SystemTitleEffects{ TitleID: titleID, EffectType: req.EffectType, ParamsJSON: sanitized, StackingStrategy: req.StackingStrategy, CapValueX1000: req.CapValueX1000, ScopesJSON: req.ScopesJSON, Sort: req.Sort, Status: req.Status, } if err := h.writeDB.SystemTitleEffects.WithContext(ctx.RequestContext()).Create(ef); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30111, "创建效果失败")) return } ctx.Payload(&simpleMessageResponseTitle{Message: "创建成功"}) } } type modifyEffectRequest struct { EffectType *int32 `json:"effect_type"` ParamsJSON string `json:"params_json"` StackingStrategy *int32 `json:"stacking_strategy"` CapValueX1000 *int32 `json:"cap_value_x1000"` ScopesJSON string `json:"scopes_json"` Sort *int32 `json:"sort"` Status *int32 `json:"status"` } func (h *handler) ModifySystemTitleEffect() core.HandlerFunc { return func(ctx core.Context) { titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64) if err != nil || titleID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID")) return } effectID, err := strconv.ParseInt(ctx.Param("effect_id"), 10, 64) if err != nil || effectID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递效果ID")) return } var req modifyEffectRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } row, err := h.readDB.SystemTitleEffects.WithContext(ctx.RequestContext()).Where(h.readDB.SystemTitleEffects.ID.Eq(effectID)).Where(h.readDB.SystemTitleEffects.TitleID.Eq(titleID)).First() if err != nil || row == nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30112, "效果不存在")) return } if req.EffectType != nil { existed, _ := h.readDB.SystemTitleEffects.WithContext(ctx.RequestContext()). Where(h.readDB.SystemTitleEffects.TitleID.Eq(titleID)). Where(h.readDB.SystemTitleEffects.EffectType.Eq(*req.EffectType)). Where(h.readDB.SystemTitleEffects.ID.Neq(effectID)).First() if existed != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30116, "同类型效果已存在")) return } row.EffectType = *req.EffectType } if req.ParamsJSON != "" { et := row.EffectType if req.EffectType != nil { et = *req.EffectType } sanitized, verr := validateEffectParams(et, req.ParamsJSON) if verr != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, verr.Error())) return } row.ParamsJSON = sanitized } if req.StackingStrategy != nil { row.StackingStrategy = *req.StackingStrategy } if req.CapValueX1000 != nil { row.CapValueX1000 = *req.CapValueX1000 } if req.ScopesJSON != "" { row.ScopesJSON = req.ScopesJSON } if req.Sort != nil { row.Sort = *req.Sort } if req.Status != nil { row.Status = *req.Status } if err := h.writeDB.SystemTitleEffects.Save(row); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30113, "修改效果失败")) return } ctx.Payload(&simpleMessageResponseTitle{Message: "修改成功"}) } } func validateEffectParams(effectType int32, raw string) (string, error) { dec := json.NewDecoder(bytes.NewBufferString(raw)) dec.DisallowUnknownFields() switch effectType { case 1: var p struct { TemplateID int64 `json:"template_id"` Frequency struct { Period string `json:"period"` Times int `json:"times"` } `json:"frequency"` } if err := dec.Decode(&p); err != nil { return "", err } if p.TemplateID < 0 { return "", fmt.Errorf("template_id无效") } if p.Frequency.Times < 1 || p.Frequency.Times > 100 { return "", fmt.Errorf("times范围错误") } if p.Frequency.Period != "day" && p.Frequency.Period != "week" && p.Frequency.Period != "month" { return "", fmt.Errorf("period无效") } b, _ := json.Marshal(p); return string(b), nil case 2: var p struct { DiscountType string `json:"discount_type"` ValueX1000 int32 `json:"value_x1000"` MaxDiscountX1000 int32 `json:"max_discount_x1000"` } if err := dec.Decode(&p); err != nil { return "", err } if p.DiscountType != "percentage" && p.DiscountType != "fixed" { return "", fmt.Errorf("discount_type无效") } if p.ValueX1000 < 0 || p.MaxDiscountX1000 < 0 { return "", fmt.Errorf("数值必须>=0") } b, _ := json.Marshal(p); return string(b), nil case 3: var p struct { MultiplierX1000 int32 `json:"multiplier_x1000"` DailyCapPoints int32 `json:"daily_cap_points"` } if err := dec.Decode(&p); err != nil { return "", err } if p.MultiplierX1000 < 0 || p.DailyCapPoints < 0 { return "", fmt.Errorf("数值必须>=0") } b, _ := json.Marshal(p); return string(b), nil case 4: var p struct { TemplateID int64 `json:"template_id"` Frequency struct { Period string `json:"period"` Times int `json:"times"` } `json:"frequency"` } if err := dec.Decode(&p); err != nil { return "", err } if p.TemplateID < 0 { return "", fmt.Errorf("template_id无效") } if p.Frequency.Times < 1 || p.Frequency.Times > 100 { return "", fmt.Errorf("times范围错误") } if p.Frequency.Period != "week" && p.Frequency.Period != "month" { return "", fmt.Errorf("period无效") } b, _ := json.Marshal(p); return string(b), nil case 5: var p struct { TargetPrizeIDs []int64 `json:"target_prize_ids"` BoostX1000 int32 `json:"boost_x1000"` CapX1000 *int32 `json:"cap_x1000"` } if err := dec.Decode(&p); err != nil { return "", err } if p.BoostX1000 < 0 || p.BoostX1000 > 100000 { return "", fmt.Errorf("boost_x1000范围错误") } if p.CapX1000 != nil && *p.CapX1000 < 0 { return "", fmt.Errorf("cap_x1000必须>=0") } if len(p.TargetPrizeIDs) > 200 { return "", fmt.Errorf("target_prize_ids数量过多") } m := make(map[int64]struct{}) out := make([]int64, 0, len(p.TargetPrizeIDs)) for _, id := range p.TargetPrizeIDs { if _, ok := m[id]; !ok { m[id] = struct{}{}; out = append(out, id) } } p.TargetPrizeIDs = out b, _ := json.Marshal(p); return string(b), nil case 6: var p struct { TargetPrizeIDs []int64 `json:"target_prize_ids"` ChanceX1000 int32 `json:"chance_x1000"` PeriodCapTimes *int32 `json:"period_cap_times"` } if err := dec.Decode(&p); err != nil { return "", err } if p.ChanceX1000 < 0 || p.ChanceX1000 > 100000 { return "", fmt.Errorf("chance_x1000范围错误") } if p.PeriodCapTimes != nil && *p.PeriodCapTimes < 0 { return "", fmt.Errorf("period_cap_times必须>=0") } if len(p.TargetPrizeIDs) > 200 { return "", fmt.Errorf("target_prize_ids数量过多") } m := make(map[int64]struct{}) out := make([]int64, 0, len(p.TargetPrizeIDs)) for _, id := range p.TargetPrizeIDs { if _, ok := m[id]; !ok { m[id] = struct{}{}; out = append(out, id) } } p.TargetPrizeIDs = out b, _ := json.Marshal(p); return string(b), nil default: return raw, nil } } func (h *handler) DeleteSystemTitleEffect() core.HandlerFunc { return func(ctx core.Context) { titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64) if err != nil || titleID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID")) return } effectID, err := strconv.ParseInt(ctx.Param("effect_id"), 10, 64) if err != nil || effectID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递效果ID")) return } del := &model.SystemTitleEffects{ID: effectID, TitleID: titleID} if _, err := h.writeDB.SystemTitleEffects.Delete(del); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30114, "删除效果失败")) return } ctx.Payload(&simpleMessageResponseTitle{Message: "删除成功"}) } } type assignUserTitleRequest struct { TitleID int64 `json:"title_id" binding:"required,min=1"` ExpiresAt *string `json:"expires_at"` // RFC3339 字符串,可空 Remark string `json:"remark"` } type assignUserTitleResponse struct { Message string `json:"message"` } // AssignUserTitle 给用户分配称号(存在则更新有效期与备注,并激活) func (h *handler) AssignUserTitle() core.HandlerFunc { return func(ctx core.Context) { userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) if err != nil || userID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) return } var req assignUserTitleRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } // 校验称号存在且启用 title, err := h.readDB.SystemTitles.WithContext(ctx.RequestContext()). Where(h.readDB.SystemTitles.ID.Eq(req.TitleID)).First() if err != nil || title == nil || title.Status != 1 { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30103, "称号不存在或未启用")) return } // 解析过期时间 var exp time.Time if req.ExpiresAt != nil && *req.ExpiresAt != "" { t, perr := time.Parse(time.RFC3339, *req.ExpiresAt) if perr != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "expires_at格式错误")) return } exp = t } // upsert 用户称号 // 先查是否存在 ut, err := h.readDB.UserTitles.WithContext(ctx.RequestContext()). Where(h.readDB.UserTitles.UserID.Eq(userID)). Where(h.readDB.UserTitles.TitleID.Eq(req.TitleID)).First() if err == nil && ut != nil { now := time.Now() if ut.Active == 1 && (ut.ExpiresAt.IsZero() || ut.ExpiresAt.After(now)) { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30117, "该用户已拥有该称号")) return } // 更新 ut.Active = 1 ut.Remark = req.Remark if !exp.IsZero() { ut.ExpiresAt = exp } if err := h.writeDB.UserTitles.Save(ut); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30104, "更新称号失败")) return } others, _ := h.readDB.UserTitles.WithContext(ctx.RequestContext()). Where(h.readDB.UserTitles.UserID.Eq(userID)). Where(h.readDB.UserTitles.TitleID.Neq(req.TitleID)). Where(h.readDB.UserTitles.Active.Eq(1)).Find() for _, o := range others { o.Active = 0 _ = h.writeDB.UserTitles.Save(o) } } else { now := time.Now() newUT := &model.UserTitles{ UserID: userID, TitleID: req.TitleID, Active: 1, ObtainedAt: now, Source: "admin_assign", Remark: req.Remark, } if !exp.IsZero() { newUT.ExpiresAt = exp } if err := h.writeDB.UserTitles.Create(newUT); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30105, "分配称号失败")) return } others, _ := h.readDB.UserTitles.WithContext(ctx.RequestContext()). Where(h.readDB.UserTitles.UserID.Eq(userID)). Where(h.readDB.UserTitles.TitleID.Neq(req.TitleID)). Where(h.readDB.UserTitles.Active.Eq(1)).Find() for _, o := range others { o.Active = 0 _ = h.writeDB.UserTitles.Save(o) } } ctx.Payload(&assignUserTitleResponse{Message: "分配称号成功"}) } }