497 lines
15 KiB
Go
497 lines
15 KiB
Go
package signinreward
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/integration"
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// GetStatus 返回签到奖励配置和当前用户签到状态。
|
|
func (s *SignInRewardService) GetStatus(ctx context.Context, user AuthUser) (*SignInRewardStatusResponse, error) {
|
|
bundle, location, state, err := s.loadStatusContext(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bundle.Config == nil {
|
|
return &SignInRewardStatusResponse{
|
|
UserID: user.UserID,
|
|
SysOrigin: normalizeSignInRewardSysOrigin(user.SysOrigin),
|
|
Configured: false,
|
|
Enabled: false,
|
|
Available: false,
|
|
Timezone: signInRewardDefaultTimezone,
|
|
CycleDays: signInRewardCycleLength,
|
|
CurrentDate: time.Now().In(location).Format("2006-01-02"),
|
|
NextDayIndex: 1,
|
|
Days: buildDefaultSignInRewardDays(),
|
|
}, nil
|
|
}
|
|
|
|
itemsMap := s.loadRewardItemsMap(ctx, bundle.Days)
|
|
days := make([]SignInRewardDayPayload, 0, len(bundle.Days))
|
|
for _, day := range bundle.Days {
|
|
days = append(days, SignInRewardDayPayload{
|
|
DayIndex: day.DayIndex,
|
|
RewardGroupID: day.RewardGroupID,
|
|
RewardGroupName: day.RewardGroupName,
|
|
RewardItems: itemsMap[rewardGroupKey(day.RewardGroupID)],
|
|
Signed: day.DayIndex <= state.CycleProgress,
|
|
TodayTarget: !state.SignedToday && day.DayIndex == state.NextDayIndex,
|
|
})
|
|
}
|
|
|
|
return &SignInRewardStatusResponse{
|
|
UserID: user.UserID,
|
|
SysOrigin: bundle.Config.SysOrigin,
|
|
Configured: true,
|
|
Enabled: bundle.Config.Enabled,
|
|
Available: bundle.Config.Enabled,
|
|
Timezone: bundle.Config.Timezone,
|
|
CycleDays: signInRewardCycleLength,
|
|
CurrentDate: state.Today,
|
|
SignedToday: state.SignedToday,
|
|
CurrentStreak: state.CurrentStreak,
|
|
NextDayIndex: state.NextDayIndex,
|
|
Days: days,
|
|
}, nil
|
|
}
|
|
|
|
// CheckIn 执行签到并发放当天奖励。
|
|
func (s *SignInRewardService) CheckIn(ctx context.Context, user AuthUser) (*SignInRewardCheckInResponse, error) {
|
|
bundle, location, state, err := s.loadStatusContext(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bundle.Config == nil || !bundle.Config.Enabled {
|
|
return nil, NewAppError(http.StatusBadRequest, "sign_in_reward_unavailable", "sign-in reward is not available")
|
|
}
|
|
|
|
if state.SignedToday {
|
|
logRecord, err := s.loadTodayLog(ctx, bundle.Config.SysOrigin, user.UserID, state.Today)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.buildCheckInResponse(ctx, bundle.Days, logRecord, true), nil
|
|
}
|
|
|
|
lockKey := fmt.Sprintf("signinreward:checkin:%s:%d:%s", bundle.Config.SysOrigin, user.UserID, state.Today)
|
|
lockOK, err := s.repo.Redis.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
|
|
if err != nil {
|
|
return nil, NewAppError(http.StatusServiceUnavailable, "sign_in_reward_lock_failed", err.Error())
|
|
}
|
|
if !lockOK {
|
|
return nil, NewAppError(http.StatusConflict, "sign_in_reward_in_progress", "sign-in reward is in progress")
|
|
}
|
|
defer s.repo.Redis.Del(context.Background(), lockKey)
|
|
|
|
// 锁内再查一次,避免并发请求重复签到。
|
|
bundle, location, state, err = s.loadStatusContext(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bundle.Config == nil || !bundle.Config.Enabled {
|
|
return nil, NewAppError(http.StatusBadRequest, "sign_in_reward_unavailable", "sign-in reward is not available")
|
|
}
|
|
if state.SignedToday {
|
|
logRecord, err := s.loadTodayLog(ctx, bundle.Config.SysOrigin, user.UserID, state.Today)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.buildCheckInResponse(ctx, bundle.Days, logRecord, true), nil
|
|
}
|
|
|
|
dayConfig, err := resolveSignInRewardDay(bundle.Days, state.NextDayIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if dayConfig.RewardGroupID == nil {
|
|
return nil, NewAppError(http.StatusBadRequest, "sign_in_reward_empty", "reward for current day is not configured")
|
|
}
|
|
|
|
logRecord, err := s.ensureTodayLog(ctx, bundle.Config, user, state, dayConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.dispatchCheckInReward(ctx, bundle.Config, user, logRecord); err != nil {
|
|
_ = s.markLogFailed(ctx, logRecord.ID, err.Error())
|
|
return nil, err
|
|
}
|
|
if err := s.markLogSuccess(ctx, logRecord.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
logRecord.Status = signInRewardStatusSuccess
|
|
logRecord.ErrorMessage = ""
|
|
logRecord.UpdateTime = time.Now().In(location)
|
|
|
|
return s.buildCheckInResponse(ctx, bundle.Days, logRecord, false), nil
|
|
}
|
|
|
|
func (s *SignInRewardService) loadStatusContext(
|
|
ctx context.Context,
|
|
user AuthUser,
|
|
) (*signInRewardConfigBundle, *time.Location, signInRewardRuntimeState, error) {
|
|
bundle, err := s.loadConfigBundle(ctx, user.SysOrigin)
|
|
if err != nil {
|
|
return nil, time.Local, signInRewardRuntimeState{}, err
|
|
}
|
|
|
|
location := resolveSignInRewardLocation(signInRewardDefaultTimezone)
|
|
if bundle.Config != nil {
|
|
location = resolveSignInRewardLocation(bundle.Config.Timezone)
|
|
bundle.Config.Timezone = location.String()
|
|
}
|
|
|
|
state, err := s.resolveRuntimeState(ctx, user.UserID, normalizeSignInRewardSysOrigin(user.SysOrigin), location)
|
|
if err != nil {
|
|
return nil, location, signInRewardRuntimeState{}, err
|
|
}
|
|
return bundle, location, state, nil
|
|
}
|
|
|
|
func (s *SignInRewardService) resolveRuntimeState(
|
|
ctx context.Context,
|
|
userID int64,
|
|
sysOrigin string,
|
|
location *time.Location,
|
|
) (signInRewardRuntimeState, error) {
|
|
now := time.Now().In(location)
|
|
today := now.Format("2006-01-02")
|
|
latestLog, err := s.loadLatestSuccessLog(ctx, sysOrigin, userID)
|
|
if err != nil {
|
|
return signInRewardRuntimeState{}, err
|
|
}
|
|
if latestLog == nil {
|
|
return signInRewardRuntimeState{
|
|
Today: today,
|
|
CurrentStreak: 0,
|
|
NextDayIndex: 1,
|
|
}, nil
|
|
}
|
|
|
|
latestDate, err := time.ParseInLocation("2006-01-02", latestLog.ClaimDate, location)
|
|
if err != nil {
|
|
return signInRewardRuntimeState{}, err
|
|
}
|
|
yesterday := now.AddDate(0, 0, -1).Format("2006-01-02")
|
|
if latestLog.ClaimDate == today {
|
|
cycleProgress := streakToCycleProgress(latestLog.StreakCount)
|
|
return signInRewardRuntimeState{
|
|
Today: today,
|
|
SignedToday: true,
|
|
CurrentStreak: latestLog.StreakCount,
|
|
NextDayIndex: nextSignInRewardDayIndex(latestLog.StreakCount),
|
|
CycleProgress: cycleProgress,
|
|
LatestSuccessID: latestLog.ID,
|
|
}, nil
|
|
}
|
|
if latestLog.ClaimDate == yesterday || latestDate.AddDate(0, 0, 1).Format("2006-01-02") == today {
|
|
return signInRewardRuntimeState{
|
|
Today: today,
|
|
CurrentStreak: latestLog.StreakCount,
|
|
NextDayIndex: nextSignInRewardDayIndex(latestLog.StreakCount),
|
|
CycleProgress: streakToCycleProgress(latestLog.StreakCount),
|
|
LatestSuccessID: latestLog.ID,
|
|
}, nil
|
|
}
|
|
return signInRewardRuntimeState{
|
|
Today: today,
|
|
CurrentStreak: 0,
|
|
NextDayIndex: 1,
|
|
}, nil
|
|
}
|
|
|
|
func (s *SignInRewardService) loadLatestSuccessLog(ctx context.Context, sysOrigin string, userID int64) (*model.SignInRewardLog, error) {
|
|
var row model.SignInRewardLog
|
|
err := s.repo.DB.WithContext(ctx).
|
|
Where("sys_origin = ? AND user_id = ? AND status = ?", sysOrigin, userID, signInRewardStatusSuccess).
|
|
Order("claim_date desc").
|
|
Order("id desc").
|
|
First(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func (s *SignInRewardService) loadTodayLog(ctx context.Context, sysOrigin string, userID int64, claimDate string) (*model.SignInRewardLog, error) {
|
|
var row model.SignInRewardLog
|
|
err := s.repo.DB.WithContext(ctx).
|
|
Where("sys_origin = ? AND user_id = ? AND claim_date = ?", sysOrigin, userID, claimDate).
|
|
First(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func (s *SignInRewardService) ensureTodayLog(
|
|
ctx context.Context,
|
|
config *signInRewardConfigSnapshot,
|
|
user AuthUser,
|
|
state signInRewardRuntimeState,
|
|
day signInRewardDaySnapshot,
|
|
) (*model.SignInRewardLog, error) {
|
|
existing, err := s.loadTodayLog(ctx, config.SysOrigin, user.UserID, state.Today)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existing != nil {
|
|
if existing.Status == signInRewardStatusSuccess {
|
|
return existing, nil
|
|
}
|
|
if existing.DayIndex <= 0 {
|
|
existing.DayIndex = day.DayIndex
|
|
}
|
|
if existing.StreakCount <= 0 {
|
|
existing.StreakCount = state.CurrentStreak + 1
|
|
}
|
|
if existing.RewardGroupID == nil {
|
|
existing.RewardGroupID = day.RewardGroupID
|
|
existing.RewardGroupName = day.RewardGroupName
|
|
}
|
|
if err := s.repo.DB.WithContext(ctx).
|
|
Model(&model.SignInRewardLog{}).
|
|
Where("id = ?", existing.ID).
|
|
Updates(map[string]any{
|
|
"day_index": existing.DayIndex,
|
|
"streak_count": existing.StreakCount,
|
|
"gold_amount": 0,
|
|
"reward_group_id": existing.RewardGroupID,
|
|
"reward_group_name": strings.TrimSpace(existing.RewardGroupName),
|
|
"status": signInRewardStatusPending,
|
|
"error_message": "",
|
|
"update_time": time.Now(),
|
|
}).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
existing.Status = signInRewardStatusPending
|
|
existing.ErrorMessage = ""
|
|
return existing, nil
|
|
}
|
|
|
|
id, err := utils.NextID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
record := &model.SignInRewardLog{
|
|
ID: id,
|
|
EventID: buildSignInRewardEventID(config.SysOrigin, user.UserID, state.Today),
|
|
SysOrigin: config.SysOrigin,
|
|
UserID: user.UserID,
|
|
ClaimDate: state.Today,
|
|
DayIndex: day.DayIndex,
|
|
StreakCount: state.CurrentStreak + 1,
|
|
GoldAmount: 0,
|
|
RewardGroupID: day.RewardGroupID,
|
|
RewardGroupName: day.RewardGroupName,
|
|
Status: signInRewardStatusPending,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
if err := s.repo.DB.WithContext(ctx).Create(record).Error; err != nil {
|
|
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
|
|
return s.loadTodayLog(ctx, config.SysOrigin, user.UserID, state.Today)
|
|
}
|
|
return nil, err
|
|
}
|
|
return record, nil
|
|
}
|
|
|
|
func (s *SignInRewardService) dispatchCheckInReward(
|
|
ctx context.Context,
|
|
config *signInRewardConfigSnapshot,
|
|
user AuthUser,
|
|
logRecord *model.SignInRewardLog,
|
|
) error {
|
|
if logRecord.RewardGroupID != nil {
|
|
if err := s.java.GrantProps(ctx, integration.GrantPropsRequest{
|
|
TrackID: logRecord.ID,
|
|
AcceptUserID: user.UserID,
|
|
SysOrigin: config.SysOrigin,
|
|
Origin: signInRewardPropsOrigin,
|
|
SourceGroupID: *logRecord.RewardGroupID,
|
|
}); err != nil {
|
|
return NewAppError(http.StatusBadGateway, "sign_in_reward_props_failed", err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SignInRewardService) markLogSuccess(ctx context.Context, logID int64) error {
|
|
return s.repo.DB.WithContext(ctx).
|
|
Model(&model.SignInRewardLog{}).
|
|
Where("id = ?", logID).
|
|
Updates(map[string]any{
|
|
"status": signInRewardStatusSuccess,
|
|
"error_message": "",
|
|
"update_time": time.Now(),
|
|
}).Error
|
|
}
|
|
|
|
func (s *SignInRewardService) markLogFailed(ctx context.Context, logID int64, errorMessage string) error {
|
|
return s.repo.DB.WithContext(ctx).
|
|
Model(&model.SignInRewardLog{}).
|
|
Where("id = ?", logID).
|
|
Updates(map[string]any{
|
|
"status": signInRewardStatusFailed,
|
|
"error_message": truncateSignInRewardMessage(errorMessage),
|
|
"update_time": time.Now(),
|
|
}).Error
|
|
}
|
|
|
|
func (s *SignInRewardService) buildCheckInResponse(
|
|
ctx context.Context,
|
|
days []signInRewardDaySnapshot,
|
|
logRecord *model.SignInRewardLog,
|
|
alreadySigned bool,
|
|
) *SignInRewardCheckInResponse {
|
|
if logRecord == nil {
|
|
return &SignInRewardCheckInResponse{
|
|
Success: false,
|
|
AlreadySigned: alreadySigned,
|
|
RewardItems: []SignInRewardRewardItem{},
|
|
}
|
|
}
|
|
|
|
dayRewardMap := s.loadRewardItemsMap(ctx, []signInRewardDaySnapshot{{
|
|
RewardGroupID: logRecord.RewardGroupID,
|
|
}})
|
|
rewardItems := dayRewardMap[rewardGroupKey(logRecord.RewardGroupID)]
|
|
if len(rewardItems) == 0 {
|
|
for _, day := range days {
|
|
if day.DayIndex == logRecord.DayIndex {
|
|
rewardItems = s.loadRewardItemsMap(ctx, []signInRewardDaySnapshot{day})[rewardGroupKey(day.RewardGroupID)]
|
|
if strings.TrimSpace(logRecord.RewardGroupName) == "" {
|
|
logRecord.RewardGroupName = day.RewardGroupName
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return &SignInRewardCheckInResponse{
|
|
Success: strings.EqualFold(logRecord.Status, signInRewardStatusSuccess),
|
|
AlreadySigned: alreadySigned,
|
|
UserID: logRecord.UserID,
|
|
SysOrigin: logRecord.SysOrigin,
|
|
ClaimDate: logRecord.ClaimDate,
|
|
DayIndex: logRecord.DayIndex,
|
|
StreakCount: logRecord.StreakCount,
|
|
RewardGroupID: logRecord.RewardGroupID,
|
|
RewardGroupName: logRecord.RewardGroupName,
|
|
RewardItems: rewardItems,
|
|
Status: logRecord.Status,
|
|
}
|
|
}
|
|
|
|
func (s *SignInRewardService) loadRewardItemsMap(
|
|
ctx context.Context,
|
|
days []signInRewardDaySnapshot,
|
|
) map[string][]SignInRewardRewardItem {
|
|
groupIDs := make([]int64, 0)
|
|
seen := make(map[int64]struct{})
|
|
for _, day := range days {
|
|
if day.RewardGroupID == nil || *day.RewardGroupID <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[*day.RewardGroupID]; ok {
|
|
continue
|
|
}
|
|
seen[*day.RewardGroupID] = struct{}{}
|
|
groupIDs = append(groupIDs, *day.RewardGroupID)
|
|
}
|
|
sort.Slice(groupIDs, func(i, j int) bool { return groupIDs[i] < groupIDs[j] })
|
|
|
|
result := make(map[string][]SignInRewardRewardItem, len(groupIDs))
|
|
for _, groupID := range groupIDs {
|
|
detail, err := s.java.GetRewardGroupDetail(ctx, groupID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
items := make([]SignInRewardRewardItem, 0, len(detail.RewardConfigList))
|
|
for _, item := range detail.RewardConfigList {
|
|
items = append(items, SignInRewardRewardItem{
|
|
ID: int64(item.ID),
|
|
Type: item.Type,
|
|
Name: item.Name,
|
|
Content: item.Content,
|
|
Quantity: int64(item.Quantity),
|
|
Cover: item.Cover,
|
|
Remark: item.Remark,
|
|
})
|
|
}
|
|
result[rewardGroupKey(&groupID)] = items
|
|
}
|
|
return result
|
|
}
|
|
|
|
func resolveSignInRewardDay(days []signInRewardDaySnapshot, dayIndex int) (signInRewardDaySnapshot, error) {
|
|
for _, item := range days {
|
|
if item.DayIndex == dayIndex {
|
|
return item, nil
|
|
}
|
|
}
|
|
return signInRewardDaySnapshot{}, NewAppError(http.StatusBadRequest, "sign_in_reward_day_missing", "reward config for current day is missing")
|
|
}
|
|
|
|
func nextSignInRewardDayIndex(currentStreak int) int {
|
|
return ((currentStreak % signInRewardCycleLength) + 1)
|
|
}
|
|
|
|
func streakToCycleProgress(streak int) int {
|
|
if streak <= 0 {
|
|
return 0
|
|
}
|
|
progress := streak % signInRewardCycleLength
|
|
if progress == 0 {
|
|
return signInRewardCycleLength
|
|
}
|
|
return progress
|
|
}
|
|
|
|
func buildSignInRewardEventID(sysOrigin string, userID int64, claimDate string) string {
|
|
return fmt.Sprintf("SIGN_IN_REWARD:%s:%d:%s", normalizeSignInRewardSysOrigin(sysOrigin), userID, strings.TrimSpace(claimDate))
|
|
}
|
|
|
|
func resolveSignInRewardLocation(timezone string) *time.Location {
|
|
timezone = normalizeSignInRewardTimezone(timezone)
|
|
location, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
location, _ = time.LoadLocation(signInRewardDefaultTimezone)
|
|
}
|
|
return location
|
|
}
|
|
|
|
func rewardGroupKey(groupID *int64) string {
|
|
if groupID == nil || *groupID <= 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%d", *groupID)
|
|
}
|
|
|
|
func truncateSignInRewardMessage(message string) string {
|
|
message = strings.TrimSpace(message)
|
|
if len(message) <= 255 {
|
|
return message
|
|
}
|
|
return message[:255]
|
|
}
|