275 lines
9.7 KiB
Go
275 lines
9.7 KiB
Go
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
|
||
})
|
||
}
|