feat: add sign in reward admin flow
This commit is contained in:
parent
2ebdd680f5
commit
df9a779b62
@ -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"),
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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 {
|
||||
|
||||
159
internal/service/signinreward/admin.go
Normal file
159
internal/service/signinreward/admin.go
Normal file
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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{},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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`),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user