package inviteactivityreward 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"` PerInviteInviterRewardCoinAmount int64 `json:"per_invite_inviter_reward_coin_amount"` PerInviteInviteeRewardCoinAmount int64 `json:"per_invite_invitee_reward_coin_amount"` Tiers []tierDTO `json:"tiers"` } type configDTO struct { AppCode string `json:"app_code"` Enabled bool `json:"enabled"` PerInviteInviterRewardCoinAmount int64 `json:"per_invite_inviter_reward_coin_amount"` PerInviteInviteeRewardCoinAmount int64 `json:"per_invite_invitee_reward_coin_amount"` 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"` RewardType string `json:"reward_type"` TierCode string `json:"tier_code"` TierName string `json:"tier_name"` ThresholdCoinAmount int64 `json:"threshold_coin_amount"` ThresholdValidInviteCount int64 `json:"threshold_valid_invite_count"` RewardCoinAmount int64 `json:"reward_coin_amount"` 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"` AppCode string `json:"app_code"` CycleKey string `json:"cycle_key"` UserID int64 `json:"user_id"` User *userDTO `json:"user,omitempty"` RewardType string `json:"reward_type"` CommandID string `json:"command_id"` TierID int64 `json:"tier_id"` TierCode string `json:"tier_code"` TierName string `json:"tier_name"` ThresholdCoinAmount int64 `json:"threshold_coin_amount"` ThresholdValidInviteCount int64 `json:"threshold_valid_invite_count"` ReachedValue int64 `json:"reached_value"` RewardCoinAmount int64 `json:"reward_coin_amount"` Status string `json:"status"` WalletCommandID string `json:"wallet_command_id"` WalletTransactionID string `json:"wallet_transaction_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"` 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.GetInviteActivityRewardConfig(c.Request.Context(), &activityv1.GetInviteActivityRewardConfigRequest{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.InviteActivityRewardTier, 0, len(req.Tiers)) for _, tier := range req.Tiers { // admin-server 只裁剪字符串和注入操作人;档位门槛、奖励金额、最高档领取语义都由 activity-service 统一校验。 tiers = append(tiers, &activityv1.InviteActivityRewardTier{ TierId: tier.TierID, RewardType: strings.TrimSpace(tier.RewardType), TierCode: strings.TrimSpace(tier.TierCode), TierName: strings.TrimSpace(tier.TierName), ThresholdCoinAmount: tier.ThresholdCoinAmount, ThresholdValidInviteCount: tier.ThresholdValidInviteCount, RewardCoinAmount: tier.RewardCoinAmount, Status: strings.TrimSpace(tier.Status), SortOrder: tier.SortOrder, }) } resp, err := h.activity.UpdateInviteActivityRewardConfig(c.Request.Context(), &activityv1.UpdateInviteActivityRewardConfigRequest{ Meta: h.meta(c), Enabled: req.Enabled, PerInviteInviterRewardCoinAmount: req.PerInviteInviterRewardCoinAmount, PerInviteInviteeRewardCoinAmount: req.PerInviteInviteeRewardCoinAmount, 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-invite-activity-reward", "invite_activity_reward_configs", item.AppCode, "success", "") response.OK(c, item) } func (h *Handler) ListClaims(c *gin.Context) { options := shared.ListOptions(c) userID, matched, ok := h.resolveUserID(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.ListInviteActivityRewardClaims(c.Request.Context(), &activityv1.ListInviteActivityRewardClaimsRequest{ Meta: h.meta(c), Status: strings.TrimSpace(options.Status), RewardType: strings.TrimSpace(c.Query("reward_type")), 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([]claimDTO, 0, len(resp.GetClaims())) for _, claim := range resp.GetClaims() { items = append(items, claimFromProto(claim)) } if err := h.fillUsers(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) resolveUserID(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) fillUsers(ctx context.Context, claims []claimDTO) error { if h.userDB == nil || len(claims) == 0 { return nil } ids := collectUserIDs(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.InviteActivityRewardConfig) 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(), PerInviteInviterRewardCoinAmount: config.GetPerInviteInviterRewardCoinAmount(), PerInviteInviteeRewardCoinAmount: config.GetPerInviteInviteeRewardCoinAmount(), Tiers: tiers, UpdatedByAdminID: config.GetUpdatedByAdminId(), CreatedAtMS: config.GetCreatedAtMs(), UpdatedAtMS: config.GetUpdatedAtMs(), } } func tierFromProto(tier *activityv1.InviteActivityRewardTier) tierDTO { if tier == nil { return tierDTO{} } return tierDTO{ TierID: tier.GetTierId(), RewardType: tier.GetRewardType(), TierCode: tier.GetTierCode(), TierName: tier.GetTierName(), ThresholdCoinAmount: tier.GetThresholdCoinAmount(), ThresholdValidInviteCount: tier.GetThresholdValidInviteCount(), RewardCoinAmount: tier.GetRewardCoinAmount(), Status: tier.GetStatus(), SortOrder: tier.GetSortOrder(), CreatedAtMS: tier.GetCreatedAtMs(), UpdatedAtMS: tier.GetUpdatedAtMs(), } } func claimFromProto(claim *activityv1.InviteActivityRewardClaim) claimDTO { if claim == nil { return claimDTO{} } return claimDTO{ ClaimID: claim.GetClaimId(), AppCode: claim.GetAppCode(), CycleKey: claim.GetCycleKey(), UserID: claim.GetUserId(), RewardType: claim.GetRewardType(), CommandID: claim.GetCommandId(), TierID: claim.GetTierId(), TierCode: claim.GetTierCode(), TierName: claim.GetTierName(), ThresholdCoinAmount: claim.GetThresholdCoinAmount(), ThresholdValidInviteCount: claim.GetThresholdValidInviteCount(), ReachedValue: claim.GetReachedValue(), RewardCoinAmount: claim.GetRewardCoinAmount(), Status: claim.GetStatus(), WalletCommandID: claim.GetWalletCommandId(), WalletTransactionID: claim.GetWalletTransactionId(), FailureReason: claim.GetFailureReason(), GrantedAtMS: claim.GetGrantedAtMs(), CreatedAtMS: claim.GetCreatedAtMs(), UpdatedAtMS: claim.GetUpdatedAtMs(), } } func collectUserIDs(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, ",") }