package firstrechargereward 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"` MinCoinAmount int64 `json:"min_coin_amount"` MaxCoinAmount int64 `json:"max_coin_amount"` 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 claimDTO struct { ClaimID string `json:"claim_id"` EventID string `json:"event_id"` TransactionID string `json:"transaction_id"` CommandID string `json:"command_id"` UserID int64 `json:"user_id"` User *userDTO `json:"user,omitempty"` TierID int64 `json:"tier_id"` TierCode string `json:"tier_code"` ResourceGroupID int64 `json:"resource_group_id"` RechargeCoinAmount int64 `json:"recharge_coin_amount"` RechargeSequence int64 `json:"recharge_sequence"` 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"` RechargedAtMS int64 `json:"recharged_at_ms"` 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"` 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.GetFirstRechargeRewardConfig(c.Request.Context(), &activityv1.GetFirstRechargeRewardConfigRequest{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 } tiers := make([]*activityv1.FirstRechargeRewardTier, 0, len(req.Tiers)) for _, tier := range req.Tiers { tiers = append(tiers, &activityv1.FirstRechargeRewardTier{ TierId: tier.TierID, TierCode: strings.TrimSpace(tier.TierCode), TierName: strings.TrimSpace(tier.TierName), MinCoinAmount: tier.MinCoinAmount, MaxCoinAmount: tier.MaxCoinAmount, ResourceGroupId: tier.ResourceGroupID, Status: strings.TrimSpace(tier.Status), SortOrder: tier.SortOrder, }) } resp, err := h.activity.UpdateFirstRechargeRewardConfig(c.Request.Context(), &activityv1.UpdateFirstRechargeRewardConfigRequest{ 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-first-recharge-reward", "first_recharge_reward_configs", item.AppCode, "success", "") response.OK(c, item) } func (h *Handler) ListClaims(c *gin.Context) { options := shared.ListOptions(c) userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) if !ok { response.ServerError(c, "查询用户信息失败") return } if options.Keyword != "" && !matched { response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) return } resp, err := h.activity.ListFirstRechargeRewardClaims(c.Request.Context(), &activityv1.ListFirstRechargeRewardClaimsRequest{ Meta: h.meta(c), Status: strings.TrimSpace(options.Status), UserId: userID, Page: int32(options.Page), PageSize: int32(options.PageSize), }) if err != nil { response.ServerError(c, "获取首冲奖励发放记录失败") return } items := make([]claimDTO, 0, len(resp.GetClaims())) for _, claim := range resp.GetClaims() { items = append(items, claimFromProto(claim)) } if err := h.fillClaimUsers(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) resolveClaimUserID(ctx context.Context, keyword string) (int64, bool, bool) { if keyword == "" { return 0, false, true } if h.userDB == nil { return 0, false, true } appCode := appctx.FromContext(ctx) rows, err := h.userDB.QueryContext(ctx, ` SELECT user_id FROM users WHERE app_code = ? AND ( CAST(user_id AS CHAR) = ? OR current_display_user_id = ? OR username LIKE ? ) ORDER BY CASE WHEN CAST(user_id AS CHAR) = ? THEN 0 WHEN current_display_user_id = ? THEN 1 ELSE 2 END, user_id DESC LIMIT 1`, appCode, keyword, keyword, "%"+keyword+"%", keyword, keyword, ) if err != nil { return 0, false, false } defer rows.Close() if !rows.Next() { return 0, false, rows.Err() == nil } var userID int64 if err := rows.Scan(&userID); err != nil { return 0, false, false } return userID, true, rows.Err() == nil } func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error { if h.userDB == nil || len(claims) == 0 { return nil } ids := collectClaimUserIDs(claims) 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 claims { if user, ok := users[claims[index].UserID]; ok { claims[index].User = &user continue } claims[index].User = &userDTO{UserID: claims[index].UserID} } return nil } func configFromProto(config *activityv1.FirstRechargeRewardConfig) 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.FirstRechargeRewardTier) tierDTO { if tier == nil { return tierDTO{} } return tierDTO{ TierID: tier.GetTierId(), TierCode: tier.GetTierCode(), TierName: tier.GetTierName(), MinCoinAmount: tier.GetMinCoinAmount(), MaxCoinAmount: tier.GetMaxCoinAmount(), ResourceGroupID: tier.GetResourceGroupId(), Status: tier.GetStatus(), SortOrder: tier.GetSortOrder(), CreatedAtMS: tier.GetCreatedAtMs(), UpdatedAtMS: tier.GetUpdatedAtMs(), } } func claimFromProto(claim *activityv1.FirstRechargeRewardClaim) claimDTO { if claim == nil { return claimDTO{} } return claimDTO{ ClaimID: claim.GetClaimId(), EventID: claim.GetEventId(), TransactionID: claim.GetTransactionId(), CommandID: claim.GetCommandId(), UserID: claim.GetUserId(), TierID: claim.GetTierId(), TierCode: claim.GetTierCode(), ResourceGroupID: claim.GetResourceGroupId(), RechargeCoinAmount: claim.GetRechargeCoinAmount(), RechargeSequence: claim.GetRechargeSequence(), RechargeType: claim.GetRechargeType(), Status: claim.GetStatus(), WalletCommandID: claim.GetWalletCommandId(), WalletGrantID: claim.GetWalletGrantId(), FailureReason: claim.GetFailureReason(), RechargedAtMS: claim.GetRechargedAtMs(), GrantedAtMS: claim.GetGrantedAtMs(), CreatedAtMS: claim.GetCreatedAtMs(), UpdatedAtMS: claim.GetUpdatedAtMs(), } } func collectClaimUserIDs(claims []claimDTO) []int64 { seen := make(map[int64]struct{}, len(claims)) ids := make([]int64, 0, len(claims)) for _, claim := range claims { if claim.UserID <= 0 { continue } if _, ok := seen[claim.UserID]; ok { continue } seen[claim.UserID] = struct{}{} ids = append(ids, claim.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, ",") }