2026-07-18 15:32:58 +08:00

275 lines
9.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package yumigiftchallenge
import (
"context"
"errors"
"net/http"
"sort"
"strings"
"time"
"chatapp3-golang/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func (s *Service) normalizeSysOrigin(value string) string {
value = strings.ToUpper(strings.TrimSpace(value))
if value == "" {
value = strings.ToUpper(strings.TrimSpace(s.cfg.YumiGiftChallenge.DefaultSysOrigin))
}
if value == "" {
value = defaultSysOrigin
}
return value
}
func (s *Service) requireYumiOrigin(value string) (string, error) {
origin := s.normalizeSysOrigin(value)
// Yumi 是产品品牌,真实 Java SysOriginPlatformEnum 为 LIKEI。显式拒绝其他来源
// 防止后台误建一个永远收不到 Yumi 送礼事件的孤立活动。
if origin != defaultSysOrigin {
return "", NewAppError(http.StatusBadRequest, "invalid_sys_origin", "Yumi gift challenge sysOrigin must be LIKEI")
}
return origin, nil
}
func resolveLocation(value string) (*time.Location, error) {
value = strings.TrimSpace(value)
if value == "" {
value = defaultTimezone
}
location, err := time.LoadLocation(value)
if err != nil {
return nil, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error())
}
return location, nil
}
func dateOnly(value time.Time, location *time.Location) time.Time {
local := value.In(location)
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, location)
}
func dateKey(value time.Time, location *time.Location) string {
return value.In(location).Format("2006-01-02")
}
func parseDateKey(value string, location *time.Location) (time.Time, error) {
parsed, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(value), location)
if err != nil {
return time.Time{}, NewAppError(http.StatusBadRequest, "invalid_stat_date", "statDate must be yyyy-MM-dd")
}
return parsed, nil
}
func activityStatus(activity model.YumiGiftChallengeActivity, now time.Time) string {
if !activity.Enabled {
return "DISABLED"
}
if now.Before(activity.StartTime) {
return "NOT_STARTED"
}
if !now.Before(activity.EndTime) {
return "ENDED"
}
return "ONGOING"
}
func (s *Service) loadActivity(ctx context.Context, id int64) (*model.YumiGiftChallengeActivity, error) {
if id <= 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_activity_id", "activityId is required")
}
var row model.YumiGiftChallengeActivity
err := s.db.WithContext(ctx).Where("id = ?", id).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, NewAppError(http.StatusNotFound, "activity_not_found", "activity not found")
}
if err != nil {
return nil, err
}
return &row, nil
}
func (s *Service) selectActivity(ctx context.Context, origin string, id int64, enabledOnly bool) (*model.YumiGiftChallengeActivity, error) {
if id > 0 {
row, err := s.loadActivity(ctx, id)
if err != nil {
return nil, err
}
if row.SysOrigin != origin || (enabledOnly && !row.Enabled) {
return nil, NewAppError(http.StatusNotFound, "activity_not_found", "activity not found")
}
return row, nil
}
now := time.Now()
query := s.db.WithContext(ctx).Model(&model.YumiGiftChallengeActivity{}).
Where("sys_origin = ?", origin)
if enabledOnly {
query = query.Where("enabled = ?", true)
}
// 进行中优先其次最近待开始最后最近结束CASE 只在单一来源的少量活动配置上执行,
// 热榜和送礼链路不会走该排序。
var row model.YumiGiftChallengeActivity
err := query.Order(clause.Expr{SQL: "CASE WHEN start_time <= ? AND end_time > ? THEN 0 WHEN start_time > ? THEN 1 ELSE 2 END", Vars: []any{now, now, now}}).
Order(clause.Expr{SQL: "CASE WHEN start_time > ? THEN start_time END ASC", Vars: []any{now}}).
Order("end_time DESC").First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, NewAppError(http.StatusNotFound, "activity_not_found", "activity not found")
}
if err != nil {
return nil, err
}
return &row, nil
}
func (s *Service) loadActivityChildren(ctx context.Context, activityID int64) ([]model.YumiGiftChallengeTaskConfig, []model.YumiGiftChallengeRankReward, error) {
var tasks []model.YumiGiftChallengeTaskConfig
if err := s.db.WithContext(ctx).Where("activity_id = ?", activityID).Order("sort_order ASC, id ASC").Find(&tasks).Error; err != nil {
return nil, nil, err
}
var rewards []model.YumiGiftChallengeRankReward
if err := s.db.WithContext(ctx).Where("activity_id = ?", activityID).
Order("period_type ASC, start_rank ASC, id ASC").Find(&rewards).Error; err != nil {
return nil, nil, err
}
return tasks, rewards, nil
}
func (s *Service) loadRewardSnapshots(ctx context.Context, activityID int64) (map[int64][]model.YumiGiftChallengeRewardSnapshot, error) {
var rows []model.YumiGiftChallengeRewardSnapshot
if err := s.db.WithContext(ctx).Where("activity_id = ?", activityID).
Order("resource_group_id ASC, sort_order ASC, reward_config_id ASC").Find(&rows).Error; err != nil {
return nil, err
}
result := make(map[int64][]model.YumiGiftChallengeRewardSnapshot)
for _, row := range rows {
result[row.ResourceGroupID] = append(result[row.ResourceGroupID], row)
}
return result, nil
}
func rewardItemViews(rows []model.YumiGiftChallengeRewardSnapshot) []RewardItemView {
items := make([]RewardItemView, 0, len(rows))
for _, row := range rows {
items = append(items, RewardItemView{
ID: row.RewardConfigID, Type: row.RewardType, DetailType: row.DetailType,
Content: row.Content, Quantity: row.Quantity, Cover: row.Cover,
SourceURL: row.SourceURL, Name: row.DisplayName, Sort: row.SortOrder,
})
}
return items
}
func buildActivityView(row model.YumiGiftChallengeActivity) ActivityView {
return ActivityView{
ID: row.ID, ActivityCode: row.ActivityCode, ActivityName: row.ActivityName,
ActivityDesc: row.ActivityDesc, SysOrigin: row.SysOrigin, Timezone: row.Timezone,
StartTime: row.StartTime.UnixMilli(), EndTime: row.EndTime.UnixMilli(),
DailySettlementDelayMinutes: row.DailySettlementDelayMinutes,
OverallSettlementDelayMinutes: row.OverallSettlementDelayMinutes,
OverallSettlementTime: row.OverallSettlementTime.UnixMilli(), DisplayTopN: row.DisplayTopN,
Enabled: row.Enabled, OverallSettlementStatus: row.OverallSettlementStatus,
Version: row.Version, CreateTime: row.CreateTime.UnixMilli(), UpdateTime: row.UpdateTime.UnixMilli(),
}
}
func (s *Service) buildDetail(ctx context.Context, activity model.YumiGiftChallengeActivity) (*DetailResponse, error) {
tasks, rewards, err := s.loadActivityChildren(ctx, activity.ID)
if err != nil {
return nil, err
}
snapshots, err := s.loadRewardSnapshots(ctx, activity.ID)
if err != nil {
return nil, err
}
taskViews := make([]TaskView, 0, len(tasks))
for _, row := range tasks {
view := TaskView{
ID: row.ID, ActivityID: row.ActivityID, TaskCode: row.TaskCode, TaskType: row.TaskType,
TaskTitle: row.TaskTitle, TaskDesc: row.TaskDesc, TargetValue: row.TargetValue,
ResourceGroupID: row.ResourceGroupID, Enabled: row.Enabled, SortOrder: row.SortOrder,
RewardItems: []RewardItemView{},
}
if row.ResourceGroupID != nil {
view.RewardItems = rewardItemViews(snapshots[*row.ResourceGroupID])
}
taskViews = append(taskViews, view)
}
rewardViews := make([]RankRewardView, 0, len(rewards))
for _, row := range rewards {
rewardViews = append(rewardViews, RankRewardView{
ID: row.ID, ActivityID: row.ActivityID, PeriodType: row.PeriodType,
StartRank: row.StartRank, EndRank: row.EndRank, ResourceGroupID: row.ResourceGroupID,
RewardName: row.RewardName, RewardItems: rewardItemViews(snapshots[row.ResourceGroupID]),
})
}
return &DetailResponse{
Activity: buildActivityView(activity), Tasks: taskViews, RankRewards: rewardViews,
SupportedPeriods: []string{PeriodDaily, PeriodOverall},
SupportedTaskTypes: []string{TaskEnterPage, TaskSendGiftGold},
ActivityStatus: activityStatus(activity, time.Now()), ServerTime: time.Now().UnixMilli(),
}, nil
}
// GetDetail 返回 Yumi H5 活动详情,用户身份只用于来源隔离,不接受客户端伪造 userId。
func (s *Service) GetDetail(ctx context.Context, user AuthUser, activityID int64) (*DetailResponse, error) {
origin, err := s.requireYumiOrigin(user.SysOrigin)
if err != nil {
return nil, err
}
activity, err := s.selectActivity(ctx, origin, activityID, true)
if err != nil {
return nil, err
}
return s.buildDetail(ctx, *activity)
}
// GetAdminDetail 返回后台完整配置详情。
func (s *Service) GetAdminDetail(ctx context.Context, activityID int64) (*DetailResponse, error) {
activity, err := s.loadActivity(ctx, activityID)
if err != nil {
return nil, err
}
return s.buildDetail(ctx, *activity)
}
// ListActivities 分页读取配置;查询始终命中 sys_origin+时间索引,不扫描送礼明细。
func (s *Service) ListActivities(ctx context.Context, origin string, page, size int) (map[string]any, error) {
origin, err := s.requireYumiOrigin(origin)
if err != nil {
return nil, err
}
if page < 1 {
page = 1
}
if size < 1 || size > 100 {
size = 20
}
query := s.db.WithContext(ctx).Model(&model.YumiGiftChallengeActivity{}).Where("sys_origin = ?", origin)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, err
}
var rows []model.YumiGiftChallengeActivity
if err := query.Order("start_time DESC, id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil {
return nil, err
}
records := make([]ActivityView, 0, len(rows))
for _, row := range rows {
records = append(records, buildActivityView(row))
}
return map[string]any{"records": records, "total": total, "current": page, "size": size}, nil
}
func sortTaskRows(rows []model.YumiGiftChallengeTaskConfig) {
sort.Slice(rows, func(i, j int) bool {
if rows[i].SortOrder == rows[j].SortOrder {
return rows[i].ID < rows[j].ID
}
return rows[i].SortOrder < rows[j].SortOrder
})
}