bindbox-game/internal/api/admin/titles_admin.go
邹方成 8141a47690
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 39s
feat(称号系统): 新增称号管理功能与抽奖效果集成
- 新增系统称号模板与效果配置表及相关CRUD接口
- 实现用户称号分配与抽奖效果应用逻辑
- 优化抽奖接口支持用户ID参数以应用称号效果
- 新增称号管理前端页面与分配功能
- 修复Windows时区错误与JSON字段初始化问题
- 移除无用管理接口代码并更新文档说明
2025-11-16 11:37:40 +08:00

386 lines
16 KiB
Go

package admin
import (
"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
}
ef := &model.SystemTitleEffects{
TitleID: titleID,
EffectType: req.EffectType,
ParamsJSON: req.ParamsJSON,
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 != "" { row.ParamsJSON = req.ParamsJSON }
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 (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 {
// 更新
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: "分配称号成功"})
}
}