package signinreward import ( "context" "errors" "net/http" "sort" "strings" "time" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "gorm.io/gorm" ) // GetConfig 返回后台配置页使用的签到奖励配置。 func (s *SignInRewardService) GetConfig(ctx context.Context, sysOrigin string) (*SignInRewardConfigResponse, error) { bundle, err := s.loadConfigBundle(ctx, sysOrigin) if err != nil { return nil, err } if bundle.Config == nil { return &SignInRewardConfigResponse{ Configured: false, SysOrigin: normalizeSignInRewardSysOrigin(sysOrigin), Timezone: signInRewardDefaultTimezone, CycleDays: signInRewardCycleLength, Days: buildDefaultSignInRewardDays(), }, nil } return &SignInRewardConfigResponse{ Configured: true, ID: bundle.Config.ID, SysOrigin: bundle.Config.SysOrigin, Enabled: bundle.Config.Enabled, Timezone: bundle.Config.Timezone, CycleDays: signInRewardCycleLength, Days: buildAdminDayPayloads(bundle.Days), UpdateTime: formatSignInRewardDateTime(bundle.Config.UpdateTime), }, nil } // SaveConfig 保存签到奖励后台配置。 func (s *SignInRewardService) SaveConfig(ctx context.Context, req SaveSignInRewardConfigRequest) (*SignInRewardConfigResponse, error) { sysOrigin := normalizeSignInRewardSysOrigin(req.SysOrigin) if sysOrigin == "" { return nil, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required") } timezone := normalizeSignInRewardTimezone(req.Timezone) if _, err := time.LoadLocation(timezone); err != nil { return nil, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error()) } normalizedDays, err := normalizeSaveDays(req.Days, req.Enabled) if err != nil { return nil, err } now := time.Now() var configID int64 if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var configRow model.SignInRewardConfig if err := tx.Where("sys_origin = ?", sysOrigin).First(&configRow).Error; errors.Is(err, gorm.ErrRecordNotFound) { nextID, idErr := utils.NextID() if idErr != nil { return idErr } configRow = model.SignInRewardConfig{ ID: nextID, SysOrigin: sysOrigin, CreateTime: now, } } else if err != nil { return err } configRow.Enabled = req.Enabled configRow.Timezone = timezone configRow.UpdateTime = now if configRow.CreateTime.IsZero() { configRow.CreateTime = now } if err := tx.Save(&configRow).Error; err != nil { return err } if err := tx.Where("config_id = ?", configRow.ID).Delete(&model.SignInRewardDayConfig{}).Error; err != nil { return err } rows := make([]model.SignInRewardDayConfig, 0, len(normalizedDays)) for _, item := range normalizedDays { dayID, idErr := utils.NextID() if idErr != nil { return idErr } rows = append(rows, model.SignInRewardDayConfig{ ID: dayID, ConfigID: configRow.ID, DayIndex: item.DayIndex, GoldAmount: 0, RewardGroupID: item.RewardGroupID.Int64Ptr(), RewardGroupName: strings.TrimSpace(item.RewardGroupName), CreateTime: now, UpdateTime: now, }) } if len(rows) > 0 { if err := tx.Create(&rows).Error; err != nil { return err } } configID = configRow.ID return nil }); err != nil { return nil, err } _ = configID return s.GetConfig(ctx, sysOrigin) } func normalizeSaveDays(days []SignInRewardDayConfigInput, enabled bool) ([]SignInRewardDayConfigInput, error) { if len(days) != signInRewardCycleLength { return nil, NewAppError(http.StatusBadRequest, "invalid_days", "days must contain exactly 7 items") } seen := make(map[int]struct{}, len(days)) normalized := make([]SignInRewardDayConfigInput, 0, len(days)) for _, item := range days { if item.DayIndex < 1 || item.DayIndex > signInRewardCycleLength { return nil, NewAppError(http.StatusBadRequest, "invalid_day_index", "dayIndex must be between 1 and 7") } if _, exists := seen[item.DayIndex]; exists { return nil, NewAppError(http.StatusBadRequest, "duplicate_day_index", "dayIndex must be unique") } seen[item.DayIndex] = struct{}{} 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, RewardGroupID: item.RewardGroupID, RewardGroupName: strings.TrimSpace(item.RewardGroupName), }) } sort.Slice(normalized, func(i, j int) bool { return normalized[i].DayIndex < normalized[j].DayIndex }) return normalized, nil } func (s *SignInRewardService) loadConfigBundle(ctx context.Context, sysOrigin string) (*signInRewardConfigBundle, error) { sysOrigin = normalizeSignInRewardSysOrigin(sysOrigin) if sysOrigin == "" { return nil, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required") } var configRow model.SignInRewardConfig err := s.repo.DB.WithContext(ctx).Where("sys_origin = ?", sysOrigin).First(&configRow).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &signInRewardConfigBundle{}, nil } if err != nil { return nil, err } var dayRows []model.SignInRewardDayConfig if err := s.repo.DB.WithContext(ctx). Where("config_id = ?", configRow.ID). Order("day_index asc"). Find(&dayRows).Error; err != nil { return nil, err } result := &signInRewardConfigBundle{ Config: &signInRewardConfigSnapshot{ ID: configRow.ID, SysOrigin: configRow.SysOrigin, Enabled: configRow.Enabled, Timezone: normalizeSignInRewardTimezone(configRow.Timezone), UpdateTime: configRow.UpdateTime, }, Days: make([]signInRewardDaySnapshot, 0, len(dayRows)), } for _, row := range dayRows { result.Days = append(result.Days, signInRewardDaySnapshot{ ID: row.ID, DayIndex: row.DayIndex, RewardGroupID: row.RewardGroupID, RewardGroupName: strings.TrimSpace(row.RewardGroupName), }) } fillMissingSignInRewardDays(&result.Days) return result, nil } func buildDefaultSignInRewardDays() []SignInRewardDayPayload { days := make([]SignInRewardDayPayload, 0, signInRewardCycleLength) for dayIndex := 1; dayIndex <= signInRewardCycleLength; dayIndex++ { days = append(days, SignInRewardDayPayload{ DayIndex: dayIndex, RewardItems: []SignInRewardRewardItem{}, }) } return days } func buildAdminDayPayloads(days []signInRewardDaySnapshot) []SignInRewardDayPayload { payloads := make([]SignInRewardDayPayload, 0, len(days)) for _, item := range days { payloads = append(payloads, SignInRewardDayPayload{ DayIndex: item.DayIndex, RewardGroupID: item.RewardGroupID, RewardGroupName: item.RewardGroupName, RewardItems: []SignInRewardRewardItem{}, }) } return payloads } func fillMissingSignInRewardDays(days *[]signInRewardDaySnapshot) { dayMap := make(map[int]signInRewardDaySnapshot, len(*days)) for _, item := range *days { dayMap[item.DayIndex] = item } filled := make([]signInRewardDaySnapshot, 0, signInRewardCycleLength) for dayIndex := 1; dayIndex <= signInRewardCycleLength; dayIndex++ { if item, ok := dayMap[dayIndex]; ok { filled = append(filled, item) continue } filled = append(filled, signInRewardDaySnapshot{DayIndex: dayIndex}) } *days = filled } func formatSignInRewardDateTime(t time.Time) string { if t.IsZero() { return "" } return t.Format("2006-01-02 15:04:05") }