From df9a779b621d0b5ffc80d62b01b0771685932b2d Mon Sep 17 00:00:00 2001 From: hy001 Date: Mon, 20 Apr 2026 22:07:10 +0800 Subject: [PATCH] feat: add sign in reward admin flow --- internal/config/config.go | 5 +- internal/integration/java.go | 11 +- internal/router/sign_in_reward_routes.go | 20 +++ internal/service/signinreward/admin.go | 159 ++++++++++++++++++ internal/service/signinreward/app.go | 34 +--- internal/service/signinreward/config.go | 14 +- .../service/signinreward/signinreward_test.go | 52 +++++- internal/service/signinreward/types.go | 87 ++++++++-- migrations/006_sign_in_reward.sql | 2 +- 9 files changed, 317 insertions(+), 67 deletions(-) create mode 100644 internal/service/signinreward/admin.go diff --git a/internal/config/config.go b/internal/config/config.go index 8e8e84b..795eec9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -118,6 +118,7 @@ type LuckyGiftProviderConfig struct { func Load() Config { javaAppBaseURL := getEnvAny([]string{"CHATAPP_JAVA_APP_BASE_URL", "INVITE_JAVA_APP_BASE_URL"}, "http://127.0.0.1:2400") + consoleBaseURL := getEnvAny([]string{"CHATAPP_JAVA_CONSOLE_BASE_URL", "JAVA_CONSOLE_BASE_URL"}, "http://127.0.0.1:2700/console") return Config{ HTTP: HTTPConfig{ @@ -134,10 +135,10 @@ func Load() Config { }, Java: JavaConfig{ AuthBaseURL: getEnvAny([]string{"CHATAPP_JAVA_AUTH_BASE_URL", "INVITE_JAVA_AUTH_BASE_URL"}, "http://127.0.0.1:2100"), - ConsoleBaseURL: getEnvAny([]string{"CHATAPP_JAVA_CONSOLE_BASE_URL", "JAVA_CONSOLE_BASE_URL"}, "http://127.0.0.1:2700/console"), + ConsoleBaseURL: consoleBaseURL, DeviceBaseURL: getEnvAny([]string{"CHATAPP_JAVA_DEVICE_BASE_URL", "INVITE_JAVA_DEVICE_BASE_URL"}, "http://127.0.0.1:2400"), AppBaseURL: javaAppBaseURL, - BridgeBaseURL: getEnvAny([]string{"CHATAPP_JAVA_BRIDGE_BASE_URL", "INVITE_JAVA_BRIDGE_BASE_URL"}, "http://127.0.0.1:2200"), + BridgeBaseURL: getEnvAny([]string{"CHATAPP_JAVA_BRIDGE_BASE_URL", "INVITE_JAVA_BRIDGE_BASE_URL"}, consoleBaseURL), ExternalBaseURL: getEnvAny([]string{"CHATAPP_JAVA_EXTERNAL_BASE_URL", "INVITE_JAVA_EXTERNAL_BASE_URL"}, "http://127.0.0.1:3000"), OtherBaseURL: getEnvAny([]string{"CHATAPP_JAVA_OTHER_BASE_URL", "GAME_JAVA_OTHER_BASE_URL", "INVITE_JAVA_APP_BASE_URL"}, javaAppBaseURL), WalletBaseURL: getEnvAny([]string{"CHATAPP_JAVA_WALLET_BASE_URL", "GAME_JAVA_WALLET_BASE_URL"}, "http://127.0.0.1:2300"), diff --git a/internal/integration/java.go b/internal/integration/java.go index f079722..19f54cb 100644 --- a/internal/integration/java.go +++ b/internal/integration/java.go @@ -296,11 +296,11 @@ func (c *Client) EnsureInviteCode(ctx context.Context, authorization string) (In } func (c *Client) GrantGold(ctx context.Context, req GrantGoldRequest) error { - return c.postJSON(ctx, c.cfg.Java.BridgeBaseURL+"/internal/resident-activity/invite/grant-gold", req, nil, nil) + return c.postJSON(ctx, c.rewardBridgeBaseURL()+"/internal/resident-activity/invite/grant-gold", req, nil, nil) } func (c *Client) GrantProps(ctx context.Context, req GrantPropsRequest) error { - return c.postJSON(ctx, c.cfg.Java.BridgeBaseURL+"/internal/resident-activity/invite/grant-props", req, nil, nil) + return c.postJSON(ctx, c.rewardBridgeBaseURL()+"/internal/resident-activity/invite/grant-props", req, nil, nil) } func (c *Client) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error { @@ -336,6 +336,13 @@ func (c *Client) GetUserProfile(ctx context.Context, userID int64) (UserProfile, return resp.Body, nil } +func (c *Client) rewardBridgeBaseURL() string { + if baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.Java.ConsoleBaseURL), "/"); baseURL != "" { + return baseURL + } + return strings.TrimRight(strings.TrimSpace(c.cfg.Java.BridgeBaseURL), "/") +} + func (c *Client) GetUserLanguage(ctx context.Context, userID int64) (string, error) { endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getLanguage?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10)) var resp resultResponse[string] diff --git a/internal/router/sign_in_reward_routes.go b/internal/router/sign_in_reward_routes.go index 77e05eb..bbccb77 100644 --- a/internal/router/sign_in_reward_routes.go +++ b/internal/router/sign_in_reward_routes.go @@ -3,6 +3,8 @@ package router import ( "chatapp3-golang/internal/service/signinreward" "net/http" + "strconv" + "strings" "github.com/gin-gonic/gin" ) @@ -54,6 +56,24 @@ func registerSignInRewardRoutes( } writeOK(c, resp) }) + residentGroup.GET("/record/page", func(c *gin.Context) { + cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1"))) + limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20"))) + resp, err := signInRewardService.PageRecords( + c.Request.Context(), + c.Query("sysOrigin"), + c.Query("claimDate"), + c.Query("status"), + signinreward.ParseSignInRewardUserID(c.Query("userId")), + cursor, + limit, + ) + if err != nil { + writeError(c, err) + return + } + writeOK(c, resp) + }) residentGroup.POST("/config/save", func(c *gin.Context) { var req signinreward.SaveSignInRewardConfigRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/internal/service/signinreward/admin.go b/internal/service/signinreward/admin.go new file mode 100644 index 0000000..5597d2b --- /dev/null +++ b/internal/service/signinreward/admin.go @@ -0,0 +1,159 @@ +package signinreward + +import ( + "context" + "net/http" + "strconv" + "strings" + "time" + + "chatapp3-golang/internal/model" + + "gorm.io/gorm" +) + +type signInRewardRecordRow struct { + model.SignInRewardLog + Account string `gorm:"column:account"` + UserAvatar string `gorm:"column:user_avatar"` + UserNickname string `gorm:"column:user_nickname"` + CountryCode string `gorm:"column:country_code"` + CountryName string `gorm:"column:country_name"` +} + +// PageRecords 返回后台签到记录分页数据。 +func (s *SignInRewardService) PageRecords( + ctx context.Context, + sysOrigin string, + claimDate string, + status string, + userID int64, + cursor int, + limit int, +) (*SignInRewardRecordPageResponse, error) { + sysOrigin = normalizeSignInRewardSysOrigin(sysOrigin) + if sysOrigin == "" { + return nil, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required") + } + + bundle, err := s.loadConfigBundle(ctx, sysOrigin) + if err != nil { + return nil, err + } + location := resolveSignInRewardLocation(signInRewardDefaultTimezone) + if bundle.Config != nil { + location = resolveSignInRewardLocation(bundle.Config.Timezone) + } + + resolvedDate := normalizeSignInRewardClaimDate(claimDate, location) + page, size := normalizeSignInRewardPage(cursor, limit) + + query := s.newRecordBaseQuery(ctx, sysOrigin, resolvedDate) + if strings.TrimSpace(status) != "" { + query = query.Where("log.status = ?", strings.ToUpper(strings.TrimSpace(status))) + } + if userID > 0 { + query = query.Where("log.user_id = ?", userID) + } + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, err + } + + var signCount int64 + if err := s.newRecordBaseQuery(ctx, sysOrigin, resolvedDate).Count(&signCount).Error; err != nil { + return nil, err + } + var successCount int64 + if err := s.newRecordBaseQuery(ctx, sysOrigin, resolvedDate). + Where("log.status = ?", signInRewardStatusSuccess). + Count(&successCount).Error; err != nil { + return nil, err + } + + records := make([]SignInRewardRecordView, 0) + if total > 0 { + var rows []signInRewardRecordRow + if err := query. + Select("log.*, user.account, user.user_avatar, user.user_nickname, user.country_code, user.country_name"). + Joins("LEFT JOIN user_base_info AS user ON user.id = log.user_id"). + Order("log.create_time desc"). + Order("log.id desc"). + Offset((page - 1) * size). + Limit(size). + Find(&rows).Error; err != nil { + return nil, err + } + records = make([]SignInRewardRecordView, 0, len(rows)) + for _, row := range rows { + records = append(records, SignInRewardRecordView{ + ID: row.ID, + UserID: row.UserID, + Account: row.Account, + UserAvatar: row.UserAvatar, + UserNickname: row.UserNickname, + CountryCode: row.CountryCode, + CountryName: row.CountryName, + ClaimDate: row.ClaimDate, + DayIndex: row.DayIndex, + StreakCount: row.StreakCount, + RewardGroupID: row.RewardGroupID, + RewardGroupName: row.RewardGroupName, + Status: strings.ToUpper(strings.TrimSpace(row.Status)), + ErrorMessage: strings.TrimSpace(row.ErrorMessage), + CreateTime: formatSignInRewardDateTime(row.CreateTime), + UpdateTime: formatSignInRewardDateTime(row.UpdateTime), + }) + } + } + + return &SignInRewardRecordPageResponse{ + Date: resolvedDate, + Timezone: location.String(), + SignCount: signCount, + SuccessCount: successCount, + Records: records, + Total: total, + }, nil +} + +func (s *SignInRewardService) newRecordBaseQuery(ctx context.Context, sysOrigin string, claimDate string) *gorm.DB { + return s.repo.DB.WithContext(ctx). + Table("sign_in_reward_log AS log"). + Where("log.sys_origin = ? AND log.claim_date = ?", sysOrigin, claimDate) +} + +func normalizeSignInRewardClaimDate(claimDate string, location *time.Location) string { + claimDate = strings.TrimSpace(claimDate) + if claimDate != "" { + return claimDate + } + return time.Now().In(location).Format("2006-01-02") +} + +func normalizeSignInRewardPage(cursor int, limit int) (int, int) { + if cursor <= 0 { + cursor = 1 + } + if limit <= 0 { + limit = 20 + } + if limit > 100 { + limit = 100 + } + return cursor, limit +} + +// ParseSignInRewardUserID 解析后台分页查询里传入的 userId。 +func ParseSignInRewardUserID(raw string) int64 { + value := strings.TrimSpace(raw) + if value == "" { + return 0 + } + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return 0 + } + return parsed +} diff --git a/internal/service/signinreward/app.go b/internal/service/signinreward/app.go index 3f783ec..d69bb99 100644 --- a/internal/service/signinreward/app.go +++ b/internal/service/signinreward/app.go @@ -42,7 +42,6 @@ func (s *SignInRewardService) GetStatus(ctx context.Context, user AuthUser) (*Si for _, day := range bundle.Days { days = append(days, SignInRewardDayPayload{ DayIndex: day.DayIndex, - GoldAmount: day.GoldAmount, RewardGroupID: day.RewardGroupID, RewardGroupName: day.RewardGroupName, RewardItems: itemsMap[rewardGroupKey(day.RewardGroupID)], @@ -115,7 +114,7 @@ func (s *SignInRewardService) CheckIn(ctx context.Context, user AuthUser) (*Sign if err != nil { return nil, err } - if dayConfig.GoldAmount <= 0 && dayConfig.RewardGroupID == nil { + if dayConfig.RewardGroupID == nil { return nil, NewAppError(http.StatusBadRequest, "sign_in_reward_empty", "reward for current day is not configured") } @@ -263,9 +262,6 @@ func (s *SignInRewardService) ensureTodayLog( if existing.StreakCount <= 0 { existing.StreakCount = state.CurrentStreak + 1 } - if existing.GoldAmount <= 0 { - existing.GoldAmount = day.GoldAmount - } if existing.RewardGroupID == nil { existing.RewardGroupID = day.RewardGroupID existing.RewardGroupName = day.RewardGroupName @@ -276,7 +272,7 @@ func (s *SignInRewardService) ensureTodayLog( Updates(map[string]any{ "day_index": existing.DayIndex, "streak_count": existing.StreakCount, - "gold_amount": existing.GoldAmount, + "gold_amount": 0, "reward_group_id": existing.RewardGroupID, "reward_group_name": strings.TrimSpace(existing.RewardGroupName), "status": signInRewardStatusPending, @@ -303,7 +299,7 @@ func (s *SignInRewardService) ensureTodayLog( ClaimDate: state.Today, DayIndex: day.DayIndex, StreakCount: state.CurrentStreak + 1, - GoldAmount: day.GoldAmount, + GoldAmount: 0, RewardGroupID: day.RewardGroupID, RewardGroupName: day.RewardGroupName, Status: signInRewardStatusPending, @@ -325,29 +321,6 @@ func (s *SignInRewardService) dispatchCheckInReward( user AuthUser, logRecord *model.SignInRewardLog, ) error { - if logRecord.GoldAmount > 0 { - goldEventID := logRecord.EventID + ":gold" - exists, err := s.java.ExistsGoldEvent(ctx, goldEventID) - if err != nil { - return NewAppError(http.StatusBadGateway, "sign_in_reward_wallet_exists_failed", err.Error()) - } - if !exists { - if err := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{ - ReceiptType: "INCOME", - UserID: user.UserID, - SysOrigin: config.SysOrigin, - EventID: goldEventID, - Remark: fmt.Sprintf("signInDate=%s dayIndex=%d", logRecord.ClaimDate, logRecord.DayIndex), - Amount: integration.NewPennyAmountPayloadFromDollar(logRecord.GoldAmount), - CloseDelayAsset: false, - OpUserType: "APP", - CustomizeOrigin: signInRewardGoldOrigin, - CustomizeOriginDesc: signInRewardGoldDesc, - }); err != nil { - return NewAppError(http.StatusBadGateway, "sign_in_reward_wallet_failed", err.Error()) - } - } - } if logRecord.RewardGroupID != nil { if err := s.java.GrantProps(ctx, integration.GrantPropsRequest{ TrackID: logRecord.ID, @@ -422,7 +395,6 @@ func (s *SignInRewardService) buildCheckInResponse( ClaimDate: logRecord.ClaimDate, DayIndex: logRecord.DayIndex, StreakCount: logRecord.StreakCount, - GoldAmount: logRecord.GoldAmount, RewardGroupID: logRecord.RewardGroupID, RewardGroupName: logRecord.RewardGroupName, RewardItems: rewardItems, diff --git a/internal/service/signinreward/config.go b/internal/service/signinreward/config.go index 05b5152..c3afe42 100644 --- a/internal/service/signinreward/config.go +++ b/internal/service/signinreward/config.go @@ -102,8 +102,8 @@ func (s *SignInRewardService) SaveConfig(ctx context.Context, req SaveSignInRewa ID: dayID, ConfigID: configRow.ID, DayIndex: item.DayIndex, - GoldAmount: item.GoldAmount, - RewardGroupID: item.RewardGroupID, + GoldAmount: 0, + RewardGroupID: item.RewardGroupID.Int64Ptr(), RewardGroupName: strings.TrimSpace(item.RewardGroupName), CreateTime: now, UpdateTime: now, @@ -139,15 +139,11 @@ func normalizeSaveDays(days []SignInRewardDayConfigInput, enabled bool) ([]SignI return nil, NewAppError(http.StatusBadRequest, "duplicate_day_index", "dayIndex must be unique") } seen[item.DayIndex] = struct{}{} - if item.GoldAmount < 0 { - return nil, NewAppError(http.StatusBadRequest, "invalid_gold_amount", "goldAmount must be greater than or equal to 0") - } - if enabled && item.GoldAmount <= 0 && item.RewardGroupID == nil { - return nil, NewAppError(http.StatusBadRequest, "empty_day_reward", "each enabled day must configure goldAmount or rewardGroupId") + if enabled && item.RewardGroupID.Int64Ptr() == nil { + return nil, NewAppError(http.StatusBadRequest, "empty_day_reward", "each enabled day must configure rewardGroupId") } normalized = append(normalized, SignInRewardDayConfigInput{ DayIndex: item.DayIndex, - GoldAmount: item.GoldAmount, RewardGroupID: item.RewardGroupID, RewardGroupName: strings.TrimSpace(item.RewardGroupName), }) @@ -195,7 +191,6 @@ func (s *SignInRewardService) loadConfigBundle(ctx context.Context, sysOrigin st result.Days = append(result.Days, signInRewardDaySnapshot{ ID: row.ID, DayIndex: row.DayIndex, - GoldAmount: row.GoldAmount, RewardGroupID: row.RewardGroupID, RewardGroupName: strings.TrimSpace(row.RewardGroupName), }) @@ -220,7 +215,6 @@ func buildAdminDayPayloads(days []signInRewardDaySnapshot) []SignInRewardDayPayl for _, item := range days { payloads = append(payloads, SignInRewardDayPayload{ DayIndex: item.DayIndex, - GoldAmount: item.GoldAmount, RewardGroupID: item.RewardGroupID, RewardGroupName: item.RewardGroupName, RewardItems: []SignInRewardRewardItem{}, diff --git a/internal/service/signinreward/signinreward_test.go b/internal/service/signinreward/signinreward_test.go index fb4b930..ac5e5c6 100644 --- a/internal/service/signinreward/signinreward_test.go +++ b/internal/service/signinreward/signinreward_test.go @@ -4,13 +4,13 @@ import "testing" func TestNormalizeSaveDays(t *testing.T) { validDays := []SignInRewardDayConfigInput{ - {DayIndex: 7, GoldAmount: 70}, - {DayIndex: 1, GoldAmount: 10}, - {DayIndex: 3, GoldAmount: 30}, - {DayIndex: 2, GoldAmount: 20}, - {DayIndex: 4, GoldAmount: 40}, - {DayIndex: 5, GoldAmount: 50}, - {DayIndex: 6, GoldAmount: 60}, + {DayIndex: 7, RewardGroupID: mustFlexibleOptionalInt64(70)}, + {DayIndex: 1, RewardGroupID: mustFlexibleOptionalInt64(10)}, + {DayIndex: 3, RewardGroupID: mustFlexibleOptionalInt64(30)}, + {DayIndex: 2, RewardGroupID: mustFlexibleOptionalInt64(20)}, + {DayIndex: 4, RewardGroupID: mustFlexibleOptionalInt64(40)}, + {DayIndex: 5, RewardGroupID: mustFlexibleOptionalInt64(50)}, + {DayIndex: 6, RewardGroupID: mustFlexibleOptionalInt64(60)}, } normalized, err := normalizeSaveDays(validDays, true) @@ -26,7 +26,7 @@ func TestNormalizeSaveDays(t *testing.T) { t.Run("enabled config rejects empty reward day", func(t *testing.T) { days := append([]SignInRewardDayConfigInput(nil), validDays...) - days[0].GoldAmount = 0 + days[0].RewardGroupID = flexibleOptionalInt64{} if _, err := normalizeSaveDays(days, true); err == nil { t.Fatalf("normalizeSaveDays() error = nil, want error") } @@ -34,13 +34,47 @@ func TestNormalizeSaveDays(t *testing.T) { t.Run("disabled config allows empty reward day", func(t *testing.T) { days := append([]SignInRewardDayConfigInput(nil), validDays...) - days[0].GoldAmount = 0 + days[0].RewardGroupID = flexibleOptionalInt64{} if _, err := normalizeSaveDays(days, false); err != nil { t.Fatalf("normalizeSaveDays() error = %v, want nil", err) } }) } +func TestFlexibleOptionalInt64(t *testing.T) { + testCases := []struct { + name string + input string + wantNil bool + want int64 + }{ + {name: "null", input: "null", wantNil: true}, + {name: "empty string", input: `""`, wantNil: true}, + {name: "number", input: "123", want: 123}, + {name: "string number", input: `"456"`, want: 456}, + } + + for _, testCase := range testCases { + var value flexibleOptionalInt64 + if err := value.UnmarshalJSON([]byte(testCase.input)); err != nil { + t.Fatalf("%s: UnmarshalJSON() error = %v", testCase.name, err) + } + if testCase.wantNil { + if value.Int64Ptr() != nil { + t.Fatalf("%s: Int64Ptr() = %v, want nil", testCase.name, *value.Int64Ptr()) + } + continue + } + if value.Int64Ptr() == nil || *value.Int64Ptr() != testCase.want { + t.Fatalf("%s: Int64Ptr() = %v, want %d", testCase.name, value.Int64Ptr(), testCase.want) + } + } +} + +func mustFlexibleOptionalInt64(value int64) flexibleOptionalInt64 { + return flexibleOptionalInt64{value: &value} +} + func TestSignInRewardCycleHelpers(t *testing.T) { testCases := []struct { streak int diff --git a/internal/service/signinreward/types.go b/internal/service/signinreward/types.go index da5dde4..fedaadc 100644 --- a/internal/service/signinreward/types.go +++ b/internal/service/signinreward/types.go @@ -2,6 +2,9 @@ package signinreward import ( "context" + "encoding/json" + "fmt" + "strconv" "strings" "time" @@ -20,11 +23,9 @@ const ( signInRewardStatusSuccess = "SUCCESS" signInRewardStatusFailed = "FAILED" - signInRewardGoldOrigin = "SIGN_IN_REWARD" - signInRewardGoldDesc = "Sign-in reward" signInRewardPropsOrigin = "SIGN_IN_REWARD" - signInRewardDefaultTimezone = "Asia/Shanghai" + signInRewardDefaultTimezone = "Asia/Riyadh" ) type AppError = common.AppError @@ -37,8 +38,6 @@ type signInRewardDB interface { } type signInRewardGateway interface { - ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error - ExistsGoldEvent(ctx context.Context, eventID string) (bool, error) GrantProps(ctx context.Context, req integration.GrantPropsRequest) error GetRewardGroupDetail(ctx context.Context, groupID int64) (integration.RewardGroupDetail, error) } @@ -74,10 +73,9 @@ type SaveSignInRewardConfigRequest struct { // SignInRewardDayConfigInput 是单日配置的入参。 type SignInRewardDayConfigInput struct { - DayIndex int `json:"dayIndex"` - GoldAmount int64 `json:"goldAmount"` - RewardGroupID *int64 `json:"rewardGroupId"` - RewardGroupName string `json:"rewardGroupName"` + DayIndex int `json:"dayIndex"` + RewardGroupID flexibleOptionalInt64 `json:"rewardGroupId"` + RewardGroupName string `json:"rewardGroupName"` } // SignInRewardConfigResponse 是后台配置页读取结果。 @@ -117,17 +115,45 @@ type SignInRewardCheckInResponse struct { ClaimDate string `json:"claimDate"` DayIndex int `json:"dayIndex"` StreakCount int `json:"streakCount"` - GoldAmount int64 `json:"goldAmount"` RewardGroupID *int64 `json:"rewardGroupId,omitempty"` RewardGroupName string `json:"rewardGroupName,omitempty"` RewardItems []SignInRewardRewardItem `json:"rewardItems"` Status string `json:"status"` } +// SignInRewardRecordPageResponse 是后台签到记录分页返回体。 +type SignInRewardRecordPageResponse struct { + Date string `json:"date"` + Timezone string `json:"timezone"` + SignCount int64 `json:"signCount"` + SuccessCount int64 `json:"successCount"` + Records []SignInRewardRecordView `json:"records"` + Total int64 `json:"total"` +} + +// SignInRewardRecordView 是后台签到记录单条展示结构。 +type SignInRewardRecordView struct { + ID int64 `json:"id"` + UserID int64 `json:"userId"` + Account string `json:"account,omitempty"` + UserAvatar string `json:"userAvatar,omitempty"` + UserNickname string `json:"userNickname,omitempty"` + CountryCode string `json:"countryCode,omitempty"` + CountryName string `json:"countryName,omitempty"` + ClaimDate string `json:"claimDate"` + DayIndex int `json:"dayIndex"` + StreakCount int `json:"streakCount"` + RewardGroupID *int64 `json:"rewardGroupId,omitempty"` + RewardGroupName string `json:"rewardGroupName,omitempty"` + Status string `json:"status"` + ErrorMessage string `json:"errorMessage,omitempty"` + CreateTime string `json:"createTime,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` +} + // SignInRewardDayPayload 是单日配置对外展示结构。 type SignInRewardDayPayload struct { DayIndex int `json:"dayIndex"` - GoldAmount int64 `json:"goldAmount"` RewardGroupID *int64 `json:"rewardGroupId,omitempty"` RewardGroupName string `json:"rewardGroupName,omitempty"` RewardItems []SignInRewardRewardItem `json:"rewardItems"` @@ -162,7 +188,6 @@ type signInRewardConfigSnapshot struct { type signInRewardDaySnapshot struct { ID int64 DayIndex int - GoldAmount int64 RewardGroupID *int64 RewardGroupName string } @@ -187,3 +212,41 @@ func normalizeSignInRewardTimezone(timezone string) string { } return timezone } + +type flexibleOptionalInt64 struct { + value *int64 +} + +func (v *flexibleOptionalInt64) UnmarshalJSON(data []byte) error { + raw := strings.TrimSpace(string(data)) + switch raw { + case "", "null", `""`: + v.value = nil + return nil + } + if strings.HasPrefix(raw, `"`) && strings.HasSuffix(raw, `"`) { + raw = strings.Trim(raw, `"`) + raw = strings.TrimSpace(raw) + if raw == "" { + v.value = nil + return nil + } + } + parsed, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return fmt.Errorf("invalid int64 value: %w", err) + } + v.value = &parsed + return nil +} + +func (v flexibleOptionalInt64) MarshalJSON() ([]byte, error) { + if v.value == nil { + return []byte("null"), nil + } + return json.Marshal(*v.value) +} + +func (v flexibleOptionalInt64) Int64Ptr() *int64 { + return v.value +} diff --git a/migrations/006_sign_in_reward.sql b/migrations/006_sign_in_reward.sql index 0eda7e6..34caba9 100644 --- a/migrations/006_sign_in_reward.sql +++ b/migrations/006_sign_in_reward.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS `sign_in_reward_config` ( `id` bigint NOT NULL COMMENT '主键ID', `sys_origin` varchar(32) NOT NULL COMMENT '来源系统', `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用', - `timezone` varchar(64) NOT NULL DEFAULT 'Asia/Shanghai' COMMENT '签到时区', + `timezone` varchar(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '签到时区', `create_time` datetime(3) DEFAULT NULL COMMENT '创建时间', `update_time` datetime(3) DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`),