package registrationreward import ( "context" "database/sql" "strconv" "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"` RewardType string `json:"reward_type"` CoinAmount int64 `json:"coin_amount"` ResourceGroupID int64 `json:"resource_group_id"` DailyLimit int64 `json:"daily_limit"` } type configDTO struct { AppCode string `json:"app_code"` Enabled bool `json:"enabled"` RewardType string `json:"reward_type"` CoinAmount int64 `json:"coin_amount"` ResourceGroupID int64 `json:"resource_group_id"` DailyLimit int64 `json:"daily_limit"` UpdatedByAdminID int64 `json:"updated_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type claimDTO struct { ClaimID string `json:"claim_id"` CommandID string `json:"command_id"` UserID int64 `json:"user_id,string"` User *userDTO `json:"user,omitempty"` RewardDay string `json:"reward_day"` RewardType string `json:"reward_type"` CoinAmount int64 `json:"coin_amount"` ResourceGroupID int64 `json:"resource_group_id"` Status string `json:"status"` WalletCommandID string `json:"wallet_command_id"` WalletTransactionID string `json:"wallet_transaction_id"` ResourceGrantID string `json:"resource_grant_id"` FailureReason string `json:"failure_reason"` ClaimedAtMS int64 `json:"claimed_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"` } type claimsPageDTO struct { Items []claimDTO `json:"items"` Page int `json:"page"` PageSize int `json:"pageSize"` Total int64 `json:"total"` TodayClaimedCount int64 `json:"today_claimed_count"` } func (h *Handler) GetRegistrationRewardConfig(c *gin.Context) { resp, err := h.activity.GetRegistrationRewardConfig(c.Request.Context(), &activityv1.GetRegistrationRewardConfigRequest{Meta: h.meta(c)}) if err != nil { response.ServerError(c, "获取注册奖励配置失败") return } response.OK(c, configFromProto(resp.GetConfig())) } func (h *Handler) UpdateRegistrationRewardConfig(c *gin.Context) { var req configRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "注册奖励配置参数不正确") return } resp, err := h.activity.UpdateRegistrationRewardConfig(c.Request.Context(), &activityv1.UpdateRegistrationRewardConfigRequest{ Meta: h.meta(c), Enabled: req.Enabled, RewardType: strings.TrimSpace(req.RewardType), CoinAmount: req.CoinAmount, ResourceGroupId: req.ResourceGroupID, DailyLimit: req.DailyLimit, OperatorAdminId: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } item := configFromProto(resp.GetConfig()) shared.OperationLogWithResourceID(c, h.audit, "update-registration-reward", "registration_reward_configs", item.AppCode, "success", item.RewardType) response.OK(c, item) } func (h *Handler) ListRegistrationRewardClaims(c *gin.Context) { options := shared.ListOptions(c) claimedStartMS := queryInt64(c, "claimed_start_ms", "claimedStartMs", "start_ms", "startMs") claimedEndMS := queryInt64(c, "claimed_end_ms", "claimedEndMs", "end_ms", "endMs") userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) if !ok { response.ServerError(c, "查询用户信息失败") return } if options.Keyword != "" && !matched { todayClaimedCount, ok := h.loadTodayClaimedCount(c) if !ok { return } response.OK(c, claimsPageDTO{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0, TodayClaimedCount: todayClaimedCount}) return } resp, err := h.activity.ListRegistrationRewardClaims(c.Request.Context(), &activityv1.ListRegistrationRewardClaimsRequest{ Meta: h.meta(c), Status: strings.TrimSpace(options.Status), UserId: userID, ClaimedStartMs: claimedStartMS, ClaimedEndMs: claimedEndMS, Page: int32(options.Page), PageSize: int32(options.PageSize), }) if err != nil { response.ServerError(c, "获取注册奖励领取记录失败") return } items := make([]claimDTO, 0, len(resp.GetClaims())) for _, item := range resp.GetClaims() { items = append(items, claimFromProto(item)) } if err := h.fillClaimUsers(c.Request.Context(), items); err != nil { response.ServerError(c, "补全用户信息失败") return } response.OK(c, claimsPageDTO{ Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal(), TodayClaimedCount: resp.GetTodayClaimedCount(), }) } func (h *Handler) loadTodayClaimedCount(c *gin.Context) (int64, bool) { resp, err := h.activity.ListRegistrationRewardClaims(c.Request.Context(), &activityv1.ListRegistrationRewardClaimsRequest{ Meta: h.meta(c), Page: 1, PageSize: 1, }) if err != nil { response.ServerError(c, "获取注册奖励今日领取数量失败") return 0, false } return resp.GetTodayClaimedCount(), true } 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 } // 注册奖励领取记录的 owner 是 activity-service;列表关键词先解析成内部 user_id,再保持下游分页和 total 同源。 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) 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 } // 领取事实必须可追踪;用户资料缺失时仍返回 user_id 让后台能定位数据问题。 claims[index].User = &userDTO{UserID: claims[index].UserID} } return nil } func configFromProto(item *activityv1.RegistrationRewardConfig) configDTO { if item == nil { return configDTO{RewardType: "coin"} } return configDTO{ AppCode: item.GetAppCode(), Enabled: item.GetEnabled(), RewardType: item.GetRewardType(), CoinAmount: item.GetCoinAmount(), ResourceGroupID: item.GetResourceGroupId(), DailyLimit: item.GetDailyLimit(), UpdatedByAdminID: item.GetUpdatedByAdminId(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func claimFromProto(item *activityv1.RegistrationRewardClaim) claimDTO { if item == nil { return claimDTO{} } return claimDTO{ ClaimID: item.GetClaimId(), CommandID: item.GetCommandId(), UserID: item.GetUserId(), RewardDay: item.GetRewardDay(), RewardType: item.GetRewardType(), CoinAmount: item.GetCoinAmount(), ResourceGroupID: item.GetResourceGroupId(), Status: item.GetStatus(), WalletCommandID: item.GetWalletCommandId(), WalletTransactionID: item.GetWalletTransactionId(), ResourceGrantID: item.GetResourceGrantId(), FailureReason: item.GetFailureReason(), ClaimedAtMS: item.GetClaimedAtMs(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.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 placeholders(count int) string { if count <= 0 { return "" } items := make([]string, count) for index := range items { items[index] = "?" } return strings.Join(items, ",") } func int64Args(values []int64) []any { args := make([]any, 0, len(values)) for _, value := range values { args = append(args, value) } return args } func queryInt64(c *gin.Context, names ...string) int64 { for _, name := range names { value := strings.TrimSpace(c.Query(name)) if value == "" { continue } parsed, err := strconv.ParseInt(value, 10, 64) if err == nil && parsed > 0 { return parsed } } return 0 }