package cumulativerechargereward import ( "context" "database/sql" "strings" "time" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/activityclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/response" activityv1 "hyapp.local/api/proto/activity/v1" "github.com/gin-gonic/gin" ) type Handler struct { activity activityclient.Client userDB *sql.DB audit shared.OperationLogger } func New(activity activityclient.Client, userDB *sql.DB, audit shared.OperationLogger) *Handler { return &Handler{activity: activity, userDB: userDB, audit: audit} } type configRequest struct { Enabled bool `json:"enabled"` Tiers []tierDTO `json:"tiers"` } type configDTO struct { AppCode string `json:"app_code"` Enabled bool `json:"enabled"` Tiers []tierDTO `json:"tiers"` UpdatedByAdminID int64 `json:"updated_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type tierDTO struct { TierID int64 `json:"tier_id"` TierCode string `json:"tier_code"` TierName string `json:"tier_name"` ThresholdUSDMinor int64 `json:"threshold_usd_minor"` ResourceGroupID int64 `json:"resource_group_id"` Status string `json:"status"` SortOrder int32 `json:"sort_order"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type grantDTO struct { GrantID string `json:"grant_id"` AppCode string `json:"app_code"` CycleKey string `json:"cycle_key"` UserID int64 `json:"user_id,string"` User *userDTO `json:"user,omitempty"` EventID string `json:"event_id"` TransactionID string `json:"transaction_id"` CommandID string `json:"command_id"` TierID int64 `json:"tier_id"` TierCode string `json:"tier_code"` ThresholdUSDMinor int64 `json:"threshold_usd_minor"` ResourceGroupID int64 `json:"resource_group_id"` ReachedUSDMinor int64 `json:"reached_usd_minor"` QualifyingUSDMinor int64 `json:"qualifying_usd_minor"` RechargeCoinAmount int64 `json:"recharge_coin_amount"` RechargeType string `json:"recharge_type"` Status string `json:"status"` WalletCommandID string `json:"wallet_command_id"` WalletGrantID string `json:"wallet_grant_id"` FailureReason string `json:"failure_reason"` GrantedAtMS int64 `json:"granted_at_ms"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type userDTO struct { UserID int64 `json:"user_id,string"` DisplayUserID string `json:"display_user_id"` Username string `json:"username"` Avatar string `json:"avatar"` } func (h *Handler) GetConfig(c *gin.Context) { resp, err := h.activity.GetCumulativeRechargeRewardConfig(c.Request.Context(), &activityv1.GetCumulativeRechargeRewardConfigRequest{Meta: h.meta(c)}) if err != nil { response.ServerError(c, "获取累充奖励配置失败") return } response.OK(c, configFromProto(resp.GetConfig())) } func (h *Handler) UpdateConfig(c *gin.Context) { var req configRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "累充奖励配置参数不正确") return } // admin-server 只做输入透传和操作人注入,档位有效性、重复门槛和启用规则统一交给 activity-service 校验。 tiers := make([]*activityv1.CumulativeRechargeRewardTier, 0, len(req.Tiers)) for _, tier := range req.Tiers { tiers = append(tiers, &activityv1.CumulativeRechargeRewardTier{ TierId: tier.TierID, TierCode: strings.TrimSpace(tier.TierCode), TierName: strings.TrimSpace(tier.TierName), ThresholdUsdMinor: tier.ThresholdUSDMinor, ResourceGroupId: tier.ResourceGroupID, Status: strings.TrimSpace(tier.Status), SortOrder: tier.SortOrder, }) } resp, err := h.activity.UpdateCumulativeRechargeRewardConfig(c.Request.Context(), &activityv1.UpdateCumulativeRechargeRewardConfigRequest{ Meta: h.meta(c), Enabled: req.Enabled, Tiers: tiers, OperatorAdminId: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } item := configFromProto(resp.GetConfig()) shared.OperationLogWithResourceID(c, h.audit, "update-cumulative-recharge-reward", "cumulative_recharge_reward_configs", item.AppCode, "success", "") response.OK(c, item) } func (h *Handler) ListGrants(c *gin.Context) { options := shared.ListOptions(c) // 发放记录主表只存 user_id;后台 keyword 先在 users 表解析成精确用户,再下推给 activity-service 查询。 userID, matched, ok := h.resolveGrantUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) if !ok { response.ServerError(c, "查询用户信息失败") return } if options.Keyword != "" && !matched { response.OK(c, response.Page{Items: []grantDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) return } resp, err := h.activity.ListCumulativeRechargeRewardGrants(c.Request.Context(), &activityv1.ListCumulativeRechargeRewardGrantsRequest{ Meta: h.meta(c), Status: strings.TrimSpace(options.Status), UserId: userID, CycleKey: strings.TrimSpace(c.Query("cycle_key")), Page: int32(options.Page), PageSize: int32(options.PageSize), }) if err != nil { response.ServerError(c, "获取累充奖励发放记录失败") return } items := make([]grantDTO, 0, len(resp.GetGrants())) for _, grant := range resp.GetGrants() { items = append(items, grantFromProto(grant)) } if err := h.fillGrantUsers(c.Request.Context(), items); err != nil { response.ServerError(c, "补全用户信息失败") return } response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()}) } func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta { return &activityv1.RequestMeta{ RequestId: middleware.CurrentRequestID(c), Caller: "admin-server", AppCode: appctx.FromContext(c.Request.Context()), SentAtMs: time.Now().UnixMilli(), } } func (h *Handler) resolveGrantUserID(ctx context.Context, keyword string) (int64, bool, bool) { if keyword == "" { return 0, false, true } // 累计充值奖励按 activity-service 的内部 user_id 过滤;keyword 的长 ID、短号、靓号、昵称优先级由 shared 统一维护。 userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) if err != nil { return 0, false, false } return userID, matched, true } func (h *Handler) fillGrantUsers(ctx context.Context, grants []grantDTO) error { if h.userDB == nil || len(grants) == 0 { return nil } // 列表补用户资料是展示增强,找不到用户时仍返回 grant 原始 user_id,避免审计记录因为用户资料缺失而不可见。 ids := collectGrantUserIDs(grants) if len(ids) == 0 { return nil } args := append([]any{appctx.FromContext(ctx)}, int64Args(ids)...) rows, err := h.userDB.QueryContext(ctx, ` SELECT user_id, current_display_user_id, COALESCE(username, ''), COALESCE(avatar, '') FROM users WHERE app_code = ? AND user_id IN (`+placeholders(len(ids))+`)`, args..., ) if err != nil { return err } defer rows.Close() users := make(map[int64]userDTO, len(ids)) for rows.Next() { var user userDTO if err := rows.Scan(&user.UserID, &user.DisplayUserID, &user.Username, &user.Avatar); err != nil { return err } users[user.UserID] = user } if err := rows.Err(); err != nil { return err } for index := range grants { if user, ok := users[grants[index].UserID]; ok { grants[index].User = &user continue } grants[index].User = &userDTO{UserID: grants[index].UserID} } return nil } func configFromProto(config *activityv1.CumulativeRechargeRewardConfig) configDTO { if config == nil { return configDTO{Tiers: []tierDTO{}} } tiers := make([]tierDTO, 0, len(config.GetTiers())) for _, tier := range config.GetTiers() { tiers = append(tiers, tierFromProto(tier)) } return configDTO{ AppCode: config.GetAppCode(), Enabled: config.GetEnabled(), Tiers: tiers, UpdatedByAdminID: config.GetUpdatedByAdminId(), CreatedAtMS: config.GetCreatedAtMs(), UpdatedAtMS: config.GetUpdatedAtMs(), } } func tierFromProto(tier *activityv1.CumulativeRechargeRewardTier) tierDTO { if tier == nil { return tierDTO{} } return tierDTO{ TierID: tier.GetTierId(), TierCode: tier.GetTierCode(), TierName: tier.GetTierName(), ThresholdUSDMinor: tier.GetThresholdUsdMinor(), ResourceGroupID: tier.GetResourceGroupId(), Status: tier.GetStatus(), SortOrder: tier.GetSortOrder(), CreatedAtMS: tier.GetCreatedAtMs(), UpdatedAtMS: tier.GetUpdatedAtMs(), } } func grantFromProto(grant *activityv1.CumulativeRechargeRewardGrant) grantDTO { if grant == nil { return grantDTO{} } return grantDTO{ GrantID: grant.GetGrantId(), AppCode: grant.GetAppCode(), CycleKey: grant.GetCycleKey(), UserID: grant.GetUserId(), EventID: grant.GetEventId(), TransactionID: grant.GetTransactionId(), CommandID: grant.GetCommandId(), TierID: grant.GetTierId(), TierCode: grant.GetTierCode(), ThresholdUSDMinor: grant.GetThresholdUsdMinor(), ResourceGroupID: grant.GetResourceGroupId(), ReachedUSDMinor: grant.GetReachedUsdMinor(), QualifyingUSDMinor: grant.GetQualifyingUsdMinor(), RechargeCoinAmount: grant.GetRechargeCoinAmount(), RechargeType: grant.GetRechargeType(), Status: grant.GetStatus(), WalletCommandID: grant.GetWalletCommandId(), WalletGrantID: grant.GetWalletGrantId(), FailureReason: grant.GetFailureReason(), GrantedAtMS: grant.GetGrantedAtMs(), CreatedAtMS: grant.GetCreatedAtMs(), UpdatedAtMS: grant.GetUpdatedAtMs(), } } func collectGrantUserIDs(grants []grantDTO) []int64 { seen := make(map[int64]struct{}, len(grants)) ids := make([]int64, 0, len(grants)) for _, grant := range grants { if grant.UserID <= 0 { continue } if _, ok := seen[grant.UserID]; ok { continue } seen[grant.UserID] = struct{}{} ids = append(ids, grant.UserID) } return ids } func int64Args(ids []int64) []any { args := make([]any, 0, len(ids)) for _, id := range ids { args = append(args, id) } return args } func placeholders(count int) string { if count <= 0 { return "" } parts := make([]string, count) for i := range parts { parts[i] = "?" } return strings.Join(parts, ",") }