package sevendaycheckin 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"` Rewards []rewardDTO `json:"rewards"` } type configDTO struct { AppCode string `json:"app_code"` Enabled bool `json:"enabled"` ConfigVersion int64 `json:"config_version"` Rewards []rewardDTO `json:"rewards"` UpdatedByAdminID int64 `json:"updated_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type rewardDTO struct { DayIndex int32 `json:"day_index"` ResourceGroupID int64 `json:"resource_group_id"` } type claimDTO struct { ClaimID string `json:"claim_id"` CommandID string `json:"command_id"` UserID int64 `json:"user_id,string"` User *userDTO `json:"user,omitempty"` CycleNo int64 `json:"cycle_no"` CheckinDay string `json:"checkin_day"` DayIndex int32 `json:"day_index"` ConfigVersion int64 `json:"config_version"` ResourceGroupID int64 `json:"resource_group_id"` WalletCommandID string `json:"wallet_command_id"` WalletGrantID string `json:"wallet_grant_id"` Status string `json:"status"` FailureReason string `json:"failure_reason"` SignedAtMS int64 `json:"signed_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,string"` DisplayUserID string `json:"display_user_id"` Username string `json:"username"` Avatar string `json:"avatar"` } func (h *Handler) GetSevenDayCheckInConfig(c *gin.Context) { resp, err := h.activity.GetSevenDayCheckInConfig(c.Request.Context(), &activityv1.GetSevenDayCheckInConfigRequest{Meta: h.meta(c)}) if err != nil { response.ServerError(c, "获取七日签到配置失败") return } response.OK(c, configFromProto(resp.GetConfig())) } func (h *Handler) UpdateSevenDayCheckInConfig(c *gin.Context) { var req configRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "七日签到配置参数不正确") return } rewards := make([]*activityv1.SevenDayCheckInReward, 0, len(req.Rewards)) for _, reward := range req.Rewards { rewards = append(rewards, &activityv1.SevenDayCheckInReward{ DayIndex: reward.DayIndex, ResourceGroupId: reward.ResourceGroupID, }) } resp, err := h.activity.UpdateSevenDayCheckInConfig(c.Request.Context(), &activityv1.UpdateSevenDayCheckInConfigRequest{ Meta: h.meta(c), Enabled: req.Enabled, Rewards: rewards, OperatorAdminId: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } item := configFromProto(resp.GetConfig()) shared.OperationLogWithResourceID(c, h.audit, "update-seven-day-checkin", "seven_day_checkin_configs", item.AppCode, "success", "") response.OK(c, item) } func (h *Handler) ListSevenDayCheckInClaims(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 } dayIndex := parseOptionalInt(c.Query("day_index")) if dayIndex < 0 || dayIndex > 7 { response.BadRequest(c, "签到天数参数不正确") return } resp, err := h.activity.ListSevenDayCheckInClaims(c.Request.Context(), &activityv1.ListSevenDayCheckInClaimsRequest{ Meta: h.meta(c), Status: strings.TrimSpace(options.Status), UserId: userID, CheckinDay: strings.TrimSpace(firstQuery(c, "checkin_day", "checkinDay")), DayIndex: int32(dayIndex), 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.SevenDayCheckInConfig) configDTO { if config == nil { return configDTO{Rewards: defaultRewards()} } rewards := make([]rewardDTO, 0, len(config.GetRewards())) for _, reward := range config.GetRewards() { rewards = append(rewards, rewardDTO{DayIndex: reward.GetDayIndex(), ResourceGroupID: reward.GetResourceGroupId()}) } return configDTO{ AppCode: config.GetAppCode(), Enabled: config.GetEnabled(), ConfigVersion: config.GetConfigVersion(), Rewards: rewards, UpdatedByAdminID: config.GetUpdatedByAdminId(), CreatedAtMS: config.GetCreatedAtMs(), UpdatedAtMS: config.GetUpdatedAtMs(), } } func claimFromProto(claim *activityv1.SevenDayCheckInClaim) claimDTO { if claim == nil { return claimDTO{} } return claimDTO{ ClaimID: claim.GetClaimId(), CommandID: claim.GetCommandId(), UserID: claim.GetUserId(), CycleNo: claim.GetCycleNo(), CheckinDay: claim.GetCheckinDay(), DayIndex: claim.GetDayIndex(), ConfigVersion: claim.GetConfigVersion(), ResourceGroupID: claim.GetResourceGroupId(), WalletCommandID: claim.GetWalletCommandId(), WalletGrantID: claim.GetWalletGrantId(), Status: claim.GetStatus(), FailureReason: claim.GetFailureReason(), SignedAtMS: claim.GetSignedAtMs(), GrantedAtMS: claim.GetGrantedAtMs(), CreatedAtMS: claim.GetCreatedAtMs(), UpdatedAtMS: claim.GetUpdatedAtMs(), } } func defaultRewards() []rewardDTO { items := make([]rewardDTO, 0, 7) for day := int32(1); day <= 7; day++ { items = append(items, rewardDTO{DayIndex: day}) } return items } func firstQuery(c *gin.Context, keys ...string) string { for _, key := range keys { if value := c.Query(key); value != "" { return value } } return "" } func parseOptionalInt(value string) int { value = strings.TrimSpace(value) if value == "" { return 0 } var result int for _, char := range value { if char < '0' || char > '9' { return -1 } result = result*10 + int(char-'0') } return result } 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 "" } return strings.TrimRight(strings.Repeat("?,", count), ",") }