bindbox-game/internal/api/admin/item_cards_admin.go
win 8d1eef2f7f fix(channel): 修复渠道统计GMV重复计数和商城直购误计入
1. 排除商城直购(source_type=1):GMV和成本过滤条件从IN(1,2,3,4)改为IN(2,3,4)
2. 排除次卡免费使用订单(actual_amount=0):避免购买次卡和使用次卡双重计入GMV
   - source_type=4 一番赏使用次卡:1578单 44032元重复
   - source_type=3 对对碰使用次卡:422单 7042元重复
   - 合计去除51074元虚增GMV(29.1%)
3. 成本过滤条件同步修正:source_type IN(2,3,4),total_amount>0

修正后:GMV从175600降至124527元,毛利率从37.4%回到真实的11.8%
2026-03-16 21:41:39 +08:00

418 lines
15 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 simpleMessageResponse struct {
Message string `json:"message"`
}
type createItemCardRequest struct {
Name string `json:"name" binding:"required"`
Status int32 `json:"status"`
CardType int32 `json:"card_type" binding:"required"`
ScopeType int32 `json:"scope_type" binding:"required"`
ActivityCategoryID int64 `json:"activity_category_id"`
ActivityID int64 `json:"activity_id"`
IssueID int64 `json:"issue_id"`
Price int64 `json:"price" binding:"required"`
ValidStartUnix *int64 `json:"valid_start_unix"`
ValidEndUnix *int64 `json:"valid_end_unix"`
EffectType int32 `json:"effect_type" binding:"required"`
RewardMultiplierX1000 int32 `json:"reward_multiplier_x1000"`
BoostRateX1000 int32 `json:"boost_rate_x1000"`
StackingStrategy int32 `json:"stacking_strategy"`
MaxEffectValueX1000 int32 `json:"max_effect_value_x1000"`
Remark string `json:"remark"`
}
type createItemCardResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
// CreateSystemItemCard 创建道具卡
// @Summary 创建道具卡
// @Description 管理员创建新的道具卡,支持设置类型、效果、有效期等属性
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param RequestBody body createItemCardRequest true "创建道具卡请求参数"
// @Success 200 {object} createItemCardResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 403 {object} code.Failure "无权限,仅超管可操作"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/system_item_cards [post]
func (h *handler) CreateSystemItemCard() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createItemCardRequest)
res := new(createItemCardResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
item := &model.SystemItemCards{
Name: req.Name,
Status: func() int32 {
if req.Status == 0 {
return 1
}
return req.Status
}(),
CardType: req.CardType,
ScopeType: req.ScopeType,
ActivityCategoryID: req.ActivityCategoryID,
ActivityID: req.ActivityID,
IssueID: req.IssueID,
Price: req.Price,
EffectType: req.EffectType,
RewardMultiplierX1000: req.RewardMultiplierX1000,
BoostRateX1000: req.BoostRateX1000,
StackingStrategy: func() int32 {
if req.StackingStrategy == 0 {
return 1
}
return req.StackingStrategy
}(),
MaxEffectValueX1000: req.MaxEffectValueX1000,
Remark: req.Remark,
}
do := h.writeDB.SystemItemCards.WithContext(ctx.RequestContext())
if req.ValidStartUnix != nil {
item.ValidStart = time.Unix(*req.ValidStartUnix, 0)
}
if req.ValidEndUnix != nil {
item.ValidEnd = time.Unix(*req.ValidEndUnix, 0)
} else {
do = do.Omit(h.writeDB.SystemItemCards.ValidEnd)
}
if err := do.Create(item); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
res.ID = item.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifyItemCardRequest struct {
Name *string `json:"name"`
Status *int32 `json:"status"`
CardType *int32 `json:"card_type"`
ScopeType *int32 `json:"scope_type"`
ActivityCategoryID *int64 `json:"activity_category_id"`
ActivityID *int64 `json:"activity_id"`
IssueID *int64 `json:"issue_id"`
Price *int64 `json:"price"`
ValidStartUnix *int64 `json:"valid_start_unix"`
ValidEndUnix *int64 `json:"valid_end_unix"`
EffectType *int32 `json:"effect_type"`
RewardMultiplierX1000 *int32 `json:"reward_multiplier_x1000"`
BoostRateX1000 *int32 `json:"boost_rate_x1000"`
StackingStrategy *int32 `json:"stacking_strategy"`
MaxEffectValueX1000 *int32 `json:"max_effect_value_x1000"`
Remark *string `json:"remark"`
}
// ModifySystemItemCard 修改道具卡
// @Summary 修改道具卡
// @Description 管理员修改道具卡信息,支持修改名称、价格、有效期等属性
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param item_card_id path integer true "道具卡ID"
// @Param RequestBody body modifyItemCardRequest true "修改道具卡请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 403 {object} code.Failure "无权限,仅超管可操作"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/system_item_cards/{item_card_id} [put]
func (h *handler) ModifySystemItemCard() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyItemCardRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
idStr := ctx.Param("item_card_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
set := map[string]any{}
if req.Name != nil {
set["name"] = *req.Name
}
if req.Status != nil {
set["status"] = *req.Status
}
if req.CardType != nil {
set["card_type"] = *req.CardType
}
if req.ScopeType != nil {
set["scope_type"] = *req.ScopeType
}
if req.ActivityCategoryID != nil {
set["activity_category_id"] = *req.ActivityCategoryID
}
if req.ActivityID != nil {
set["activity_id"] = *req.ActivityID
}
if req.IssueID != nil {
set["issue_id"] = *req.IssueID
}
if req.Price != nil {
set["price"] = *req.Price
}
if req.EffectType != nil {
set["effect_type"] = *req.EffectType
}
if req.RewardMultiplierX1000 != nil {
set["reward_multiplier_x1000"] = *req.RewardMultiplierX1000
}
if req.BoostRateX1000 != nil {
set["boost_rate_x1000"] = *req.BoostRateX1000
}
if req.StackingStrategy != nil {
set["stacking_strategy"] = *req.StackingStrategy
}
if req.MaxEffectValueX1000 != nil {
set["max_effect_value_x1000"] = *req.MaxEffectValueX1000
}
if req.Remark != nil {
set["remark"] = *req.Remark
}
if req.ValidStartUnix != nil {
set["valid_start"] = time.Unix(*req.ValidStartUnix, 0)
}
if req.ValidEndUnix != nil {
set["valid_end"] = time.Unix(*req.ValidEndUnix, 0)
}
if len(set) == 0 {
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
return
}
if _, err := h.writeDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.writeDB.SystemItemCards.ID.Eq(id)).Updates(set); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
}
}
// DeleteSystemItemCard 删除道具卡
// @Summary 删除道具卡
// @Description 管理员删除指定的道具卡
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param item_card_id path integer true "道具卡ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 403 {object} code.Failure "无权限,仅超管可操作"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/system_item_cards/{item_card_id} [delete]
func (h *handler) DeleteSystemItemCard() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("item_card_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
uid := int64(ctx.SessionUserInfo().Id)
set := map[string]any{"deleted_at": time.Now(), "deleted_by": uid}
if _, err := h.writeDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.writeDB.SystemItemCards.ID.Eq(id)).Updates(set); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
}
}
type listItemCardsRequest struct {
Name string `form:"name"`
Status int32 `form:"status"`
CardType int32 `form:"card_type"`
ScopeType int32 `form:"scope_type"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type itemCardListItem struct {
ID int64 `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
Status int32 `json:"status"`
CardType int32 `json:"card_type"`
ScopeType int32 `json:"scope_type"`
ActivityCategoryID int64 `json:"activity_category_id"`
ActivityID int64 `json:"activity_id"`
IssueID int64 `json:"issue_id"`
Price int64 `json:"price"`
ValidStart time.Time `json:"valid_start"`
ValidEnd time.Time `json:"valid_end"`
EffectType int32 `json:"effect_type"`
RewardMultiplierX1000 int32 `json:"reward_multiplier_x1000"`
BoostRateX1000 int32 `json:"boost_rate_x1000"`
StackingStrategy int32 `json:"stacking_strategy"`
MaxEffectValueX1000 int32 `json:"max_effect_value_x1000"`
Remark string `json:"remark"`
}
type listItemCardsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []itemCardListItem `json:"list"`
}
// ListSystemItemCards 获取道具卡列表
// @Summary 获取道具卡列表
// @Description 管理员获取道具卡列表,支持按名称、状态、类型等条件筛选
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param name query string false "道具卡名称"
// @Param status query integer false "状态1启用 2禁用"
// @Param card_type query integer false "道具卡类型1抽奖卡 2加成卡 3保底卡"
// @Param scope_type query integer false "适用范围1全局 2活动分类 3活动 4期次"
// @Param page query integer false "页码默认1"
// @Param page_size query integer false "每页条数默认10"
// @Success 200 {object} listItemCardsResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/system_item_cards [get]
func (h *handler) ListSystemItemCards() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listItemCardsRequest)
res := new(listItemCardsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
q := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).ReadDB()
if req.Name != "" {
q = q.Where(h.readDB.SystemItemCards.Name.Like("%" + req.Name + "%"))
}
if req.Status != 0 {
q = q.Where(h.readDB.SystemItemCards.Status.Eq(req.Status))
}
if req.CardType != 0 {
q = q.Where(h.readDB.SystemItemCards.CardType.Eq(req.CardType))
}
if req.ScopeType != 0 {
q = q.Where(h.readDB.SystemItemCards.ScopeType.Eq(req.ScopeType))
}
total, err := q.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
req.PageSize = 100
}
rows, err := q.Order(h.readDB.SystemItemCards.ID.Desc()).Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]itemCardListItem, len(rows))
for i, r := range rows {
res.List[i] = itemCardListItem{
ID: r.ID,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
Name: r.Name,
Status: r.Status,
CardType: r.CardType,
ScopeType: r.ScopeType,
ActivityCategoryID: r.ActivityCategoryID,
ActivityID: r.ActivityID,
IssueID: r.IssueID,
Price: r.Price,
ValidStart: r.ValidStart,
ValidEnd: r.ValidEnd,
EffectType: r.EffectType,
RewardMultiplierX1000: r.RewardMultiplierX1000,
BoostRateX1000: r.BoostRateX1000,
StackingStrategy: r.StackingStrategy,
MaxEffectValueX1000: r.MaxEffectValueX1000,
Remark: r.Remark,
}
}
ctx.Payload(res)
}
}
type assignItemCardRequest struct {
CardID int64 `json:"card_id" binding:"required"`
Quantity int `json:"quantity"`
}
// AssignUserItemCard 给用户分配道具卡
// @Summary 给用户分配道具卡
// @Description 管理员给指定用户分配道具卡,可指定数量
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param RequestBody body assignItemCardRequest true "分配道具卡请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 403 {object} code.Failure "无权限,仅超管可操作"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/users/{user_id}/item_cards [post]
func (h *handler) AssignUserItemCard() core.HandlerFunc {
return func(ctx core.Context) {
req := new(assignItemCardRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.userSvc.AddItemCard(ctx.RequestContext(), userID, req.CardID, req.Quantity); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
}
}