455 lines
14 KiB
Go
455 lines
14 KiB
Go
package wheel
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// PageRecords returns draw records without admin-only enrichment.
|
|
func (s *Service) PageRecords(
|
|
ctx context.Context,
|
|
sysOrigin string,
|
|
category string,
|
|
userID int64,
|
|
status string,
|
|
cursor int,
|
|
limit int,
|
|
) (*RecordPageResponse, error) {
|
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
if sysOrigin == "" {
|
|
return nil, NewAppError(400, "bad_request", "sysOrigin is required")
|
|
}
|
|
cursor, limit = normalizeRecordPage(cursor, limit)
|
|
|
|
query := s.db.WithContext(ctx).Model(&model.WheelDrawRecord{}).
|
|
Where("sys_origin = ?", sysOrigin)
|
|
if strings.TrimSpace(category) != "" {
|
|
query = query.Where("category = ?", normalizeCategory(category))
|
|
}
|
|
if userID > 0 {
|
|
query = query.Where("user_id = ?", userID)
|
|
}
|
|
if status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rows []model.WheelDrawRecord
|
|
if err := query.Order("create_time desc, id desc").
|
|
Offset((cursor - 1) * limit).
|
|
Limit(limit).
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
records := make([]DrawRecordPayload, 0, len(rows))
|
|
for _, row := range rows {
|
|
records = append(records, drawRecordPayload(row))
|
|
}
|
|
return &RecordPageResponse{
|
|
Records: records,
|
|
Total: total,
|
|
Current: cursor,
|
|
Size: limit,
|
|
}, nil
|
|
}
|
|
|
|
// PageAdminRecords returns admin draw records with user info and range summary.
|
|
func (s *Service) PageAdminRecords(
|
|
ctx context.Context,
|
|
sysOrigin string,
|
|
category string,
|
|
userID int64,
|
|
status string,
|
|
cursor int,
|
|
limit int,
|
|
startTime string,
|
|
endTime string,
|
|
) (*RecordPageResponse, error) {
|
|
cursor, limit = normalizeRecordPage(cursor, limit)
|
|
filter, err := newWheelAdminRecordFilter(sysOrigin, category, userID, status, startTime, endTime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if filter.SysOrigin == "" {
|
|
return nil, NewAppError(400, "bad_request", "sysOrigin is required")
|
|
}
|
|
|
|
query := s.adminRecordQuery(ctx, filter)
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rows []model.WheelDrawRecord
|
|
if err := s.adminRecordQuery(ctx, filter).
|
|
Order("create_time desc, id desc").
|
|
Offset((cursor - 1) * limit).
|
|
Limit(limit).
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
summary, err := s.adminRecordSummary(ctx, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userInfos, err := s.adminRecordUserInfos(ctx, rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userDrawTimes, err := s.adminRecordUserDrawTimes(ctx, filter, rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
records := make([]DrawRecordPayload, 0, len(rows))
|
|
for _, row := range rows {
|
|
payload := drawRecordPayload(row)
|
|
if user, ok := userInfos[row.UserID]; ok {
|
|
payload.UserShortID = user.Account
|
|
payload.UserAvatar = user.UserAvatar
|
|
payload.UserNickname = user.UserNickname
|
|
}
|
|
payload.UserTotalDrawTimes = userDrawTimes[row.UserID]
|
|
records = append(records, payload)
|
|
}
|
|
return &RecordPageResponse{
|
|
Records: records,
|
|
Total: total,
|
|
Current: cursor,
|
|
Size: limit,
|
|
TotalPaidGold: summary.TotalPaidGold,
|
|
TotalRewardGold: summary.TotalRewardGold,
|
|
TotalGiftRewardGold: summary.TotalGiftRewardGold,
|
|
TotalDecorationRewardGold: summary.TotalDecorationRewardGold,
|
|
TotalDrawTimes: summary.TotalDrawTimes,
|
|
ParticipantCount: summary.ParticipantCount,
|
|
StartTime: formatDateTime(filter.StartTime),
|
|
EndTime: formatDateTime(filter.EndTime),
|
|
}, nil
|
|
}
|
|
|
|
// PageUserHistory returns app draw history for the authenticated user.
|
|
func (s *Service) PageUserHistory(ctx context.Context, user AuthUser, cursor int, limit int) (*RecordPageResponse, error) {
|
|
return s.PageRecords(ctx, user.SysOrigin, "", user.UserID, drawStatusSuccess, cursor, limit)
|
|
}
|
|
|
|
// Hints returns recent successful rewards for ticker displays.
|
|
func (s *Service) Hints(ctx context.Context, user AuthUser, category string, limit int) ([]DrawRewardPayload, error) {
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 50 {
|
|
limit = 50
|
|
}
|
|
query := s.db.WithContext(ctx).
|
|
Where("sys_origin = ? AND status = ?", normalizeSysOrigin(user.SysOrigin), drawStatusSuccess).
|
|
Order("create_time desc, id desc").
|
|
Limit(limit)
|
|
if strings.TrimSpace(category) != "" {
|
|
query = query.Where("category = ?", normalizeCategory(category))
|
|
}
|
|
var rows []model.WheelDrawRecord
|
|
if err := query.Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
rewards := make([]DrawRewardPayload, 0, len(rows))
|
|
for _, row := range rows {
|
|
name := row.ResourceName
|
|
if name == "" && row.RewardType == rewardTypeGold {
|
|
name = "金币"
|
|
}
|
|
rewards = append(rewards, DrawRewardPayload{
|
|
RewardType: row.RewardType,
|
|
ResourceID: resourceIDValue(row.ResourceID),
|
|
ResourceType: row.ResourceType,
|
|
ResourceName: row.ResourceName,
|
|
ResourceURL: row.ResourceURL,
|
|
CoverURL: row.CoverURL,
|
|
DurationDays: row.DurationDays,
|
|
DisplayGoldAmount: row.DisplayGoldAmount,
|
|
GoldAmount: row.GoldAmount,
|
|
Probability: row.Probability,
|
|
TotalLimit: 0,
|
|
UserLimit: 0,
|
|
IssuedCount: 0,
|
|
Count: 1,
|
|
Name: name,
|
|
Value: displayValueForRecord(row),
|
|
})
|
|
}
|
|
return rewards, nil
|
|
}
|
|
|
|
// WalletBalances returns current gold balance in the shape needed by the H5 compatibility API.
|
|
func (s *Service) WalletBalances(ctx context.Context, user AuthUser) (int64, error) {
|
|
if s.java == nil {
|
|
return 0, nil
|
|
}
|
|
balances, err := s.java.MapGoldBalance(ctx, []int64{user.UserID})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return balances[user.UserID], nil
|
|
}
|
|
|
|
func drawRecordPayload(row model.WheelDrawRecord) DrawRecordPayload {
|
|
return DrawRecordPayload{
|
|
ID: row.ID,
|
|
DrawNo: row.DrawNo,
|
|
EventID: row.EventID,
|
|
UserID: row.UserID,
|
|
SysOrigin: row.SysOrigin,
|
|
Category: row.Category,
|
|
DrawTimes: row.DrawTimes,
|
|
PaidGold: row.PaidGold,
|
|
RewardConfigID: row.RewardConfigID,
|
|
RewardType: row.RewardType,
|
|
ResourceID: resourceIDValue(row.ResourceID),
|
|
ResourceType: row.ResourceType,
|
|
ResourceName: row.ResourceName,
|
|
ResourceURL: row.ResourceURL,
|
|
CoverURL: row.CoverURL,
|
|
DurationDays: row.DurationDays,
|
|
DisplayGoldAmount: row.DisplayGoldAmount,
|
|
GoldAmount: row.GoldAmount,
|
|
Probability: row.Probability,
|
|
Status: row.Status,
|
|
RetryCount: row.RetryCount,
|
|
ErrorMessage: row.ErrorMessage,
|
|
GrantTime: formatOptionalDateTime(row.GrantTime),
|
|
CreateTime: formatDateTime(row.CreateTime),
|
|
UpdateTime: formatDateTime(row.UpdateTime),
|
|
}
|
|
}
|
|
|
|
func displayValueForRecord(row model.WheelDrawRecord) int64 {
|
|
if normalizeRewardType(row.RewardType) == rewardTypeGold {
|
|
return row.GoldAmount
|
|
}
|
|
return row.DisplayGoldAmount
|
|
}
|
|
|
|
type wheelAdminRecordFilter struct {
|
|
SysOrigin string
|
|
Category string
|
|
UserID int64
|
|
Status string
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
}
|
|
|
|
type wheelAdminRecordSummary struct {
|
|
TotalPaidGold int64
|
|
TotalRewardGold int64
|
|
TotalGiftRewardGold int64
|
|
TotalDecorationRewardGold int64
|
|
TotalDrawTimes int64
|
|
ParticipantCount int64
|
|
}
|
|
|
|
func newWheelAdminRecordFilter(sysOrigin string, category string, userID int64, status string, startRaw string, endRaw string) (wheelAdminRecordFilter, error) {
|
|
start, end, err := parseWheelAdminRecordTimeRange(startRaw, endRaw)
|
|
if err != nil {
|
|
return wheelAdminRecordFilter{}, err
|
|
}
|
|
return wheelAdminRecordFilter{
|
|
SysOrigin: normalizeSysOrigin(sysOrigin),
|
|
Category: strings.ToUpper(strings.TrimSpace(category)),
|
|
UserID: userID,
|
|
Status: strings.ToUpper(strings.TrimSpace(status)),
|
|
StartTime: start,
|
|
EndTime: end,
|
|
}, nil
|
|
}
|
|
|
|
func parseWheelAdminRecordTimeRange(startRaw string, endRaw string) (time.Time, time.Time, error) {
|
|
startRaw = strings.TrimSpace(startRaw)
|
|
endRaw = strings.TrimSpace(endRaw)
|
|
now := time.Now()
|
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
if startRaw == "" && endRaw == "" {
|
|
return todayStart, todayStart.Add(24*time.Hour - time.Nanosecond), nil
|
|
}
|
|
|
|
var start time.Time
|
|
var end time.Time
|
|
var err error
|
|
if startRaw != "" {
|
|
start, err = parseWheelAdminRecordTime(startRaw, false)
|
|
if err != nil {
|
|
return time.Time{}, time.Time{}, err
|
|
}
|
|
}
|
|
if endRaw != "" {
|
|
end, err = parseWheelAdminRecordTime(endRaw, true)
|
|
if err != nil {
|
|
return time.Time{}, time.Time{}, err
|
|
}
|
|
}
|
|
if startRaw == "" {
|
|
start = time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, end.Location())
|
|
}
|
|
if endRaw == "" {
|
|
end = time.Date(start.Year(), start.Month(), start.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), start.Location())
|
|
}
|
|
if end.Before(start) {
|
|
return time.Time{}, time.Time{}, NewAppError(http.StatusBadRequest, "bad_time_range", "endTime must be greater than startTime")
|
|
}
|
|
return start, end, nil
|
|
}
|
|
|
|
func parseWheelAdminRecordTime(raw string, endOfDay bool) (time.Time, error) {
|
|
for _, layout := range []string{
|
|
"2006-01-02 15:04:05",
|
|
time.RFC3339,
|
|
"2006-01-02T15:04:05.000Z07:00",
|
|
"2006-01-02",
|
|
} {
|
|
var parsed time.Time
|
|
var err error
|
|
if strings.Contains(layout, "Z07:00") {
|
|
parsed, err = time.Parse(layout, raw)
|
|
} else {
|
|
parsed, err = time.ParseInLocation(layout, raw, time.Local)
|
|
}
|
|
if err == nil {
|
|
if layout == "2006-01-02" && endOfDay {
|
|
parsed = parsed.Add(24*time.Hour - time.Nanosecond)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
}
|
|
return time.Time{}, NewAppError(http.StatusBadRequest, "bad_time", fmt.Sprintf("invalid time: %s", raw))
|
|
}
|
|
|
|
func (s *Service) adminRecordQuery(ctx context.Context, filter wheelAdminRecordFilter) *gorm.DB {
|
|
query := s.db.WithContext(ctx).Model(&model.WheelDrawRecord{}).
|
|
Where("sys_origin = ? AND create_time >= ? AND create_time <= ?", filter.SysOrigin, filter.StartTime, filter.EndTime)
|
|
if strings.TrimSpace(filter.Category) != "" {
|
|
query = query.Where("category = ?", normalizeCategory(filter.Category))
|
|
}
|
|
if filter.UserID > 0 {
|
|
query = query.Where("user_id = ?", filter.UserID)
|
|
}
|
|
if filter.Status != "" {
|
|
query = query.Where("status = ?", filter.Status)
|
|
}
|
|
return query
|
|
}
|
|
|
|
func (s *Service) adminRecordSummary(ctx context.Context, filter wheelAdminRecordFilter) (wheelAdminRecordSummary, error) {
|
|
var summary wheelAdminRecordSummary
|
|
// totalRewardGold keeps the historical dashboard number, including direct gold rewards.
|
|
// The two split fields only count RESOURCE rewards: GIFT is the gift value, every other
|
|
// resourceType is treated as decoration value for the admin cards.
|
|
if err := s.adminRecordQuery(ctx, filter).
|
|
Select(`
|
|
COALESCE(SUM(CASE WHEN status = ? THEN CASE WHEN reward_type = ? THEN gold_amount ELSE display_gold_amount END ELSE 0 END), 0) AS total_reward_gold,
|
|
COALESCE(SUM(CASE WHEN status = ? AND reward_type = ? AND UPPER(resource_type) = ? THEN display_gold_amount ELSE 0 END), 0) AS total_gift_reward_gold,
|
|
COALESCE(SUM(CASE WHEN status = ? AND reward_type = ? AND UPPER(resource_type) <> ? THEN display_gold_amount ELSE 0 END), 0) AS total_decoration_reward_gold
|
|
`, drawStatusSuccess, rewardTypeGold, drawStatusSuccess, rewardTypeResource, resourceTypeGift, drawStatusSuccess, rewardTypeResource, resourceTypeGift).
|
|
Scan(&summary).Error; err != nil {
|
|
return summary, err
|
|
}
|
|
if err := s.adminRecordQuery(ctx, filter).
|
|
Select("COUNT(DISTINCT user_id)").
|
|
Scan(&summary.ParticipantCount).Error; err != nil {
|
|
return summary, err
|
|
}
|
|
|
|
drawRows := s.adminRecordQuery(ctx, filter).
|
|
Select("draw_no, MAX(paid_gold) AS paid_gold, MAX(draw_times) AS draw_times").
|
|
Group("draw_no")
|
|
if err := s.db.WithContext(ctx).
|
|
Table("(?) AS draw_rows", drawRows).
|
|
Select("COALESCE(SUM(paid_gold), 0), COALESCE(SUM(draw_times), 0)").
|
|
Row().
|
|
Scan(&summary.TotalPaidGold, &summary.TotalDrawTimes); err != nil {
|
|
return summary, err
|
|
}
|
|
return summary, nil
|
|
}
|
|
|
|
func (s *Service) adminRecordUserInfos(ctx context.Context, rows []model.WheelDrawRecord) (map[int64]model.UserBaseInfo, error) {
|
|
ids := make([]int64, 0, len(rows))
|
|
seen := make(map[int64]struct{}, len(rows))
|
|
for _, row := range rows {
|
|
if row.UserID <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[row.UserID]; ok {
|
|
continue
|
|
}
|
|
seen[row.UserID] = struct{}{}
|
|
ids = append(ids, row.UserID)
|
|
}
|
|
if len(ids) == 0 {
|
|
return map[int64]model.UserBaseInfo{}, nil
|
|
}
|
|
var users []model.UserBaseInfo
|
|
if err := s.db.WithContext(ctx).
|
|
Where("id IN ?", ids).
|
|
Find(&users).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[int64]model.UserBaseInfo, len(users))
|
|
for _, user := range users {
|
|
result[user.ID] = user
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Service) adminRecordUserDrawTimes(ctx context.Context, filter wheelAdminRecordFilter, pageRows []model.WheelDrawRecord) (map[int64]int64, error) {
|
|
ids := make([]int64, 0, len(pageRows))
|
|
seen := make(map[int64]struct{}, len(pageRows))
|
|
for _, row := range pageRows {
|
|
if row.UserID <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[row.UserID]; ok {
|
|
continue
|
|
}
|
|
seen[row.UserID] = struct{}{}
|
|
ids = append(ids, row.UserID)
|
|
}
|
|
if len(ids) == 0 {
|
|
return map[int64]int64{}, nil
|
|
}
|
|
|
|
drawRows := s.adminRecordQuery(ctx, filter).
|
|
Where("user_id IN ?", ids).
|
|
Select("user_id, draw_no, MAX(draw_times) AS draw_times").
|
|
Group("user_id, draw_no")
|
|
|
|
type userDrawTotal struct {
|
|
UserID int64
|
|
TotalDrawTimes int64
|
|
}
|
|
var rows []userDrawTotal
|
|
if err := s.db.WithContext(ctx).
|
|
Table("(?) AS user_draw_rows", drawRows).
|
|
Select("user_id, COALESCE(SUM(draw_times), 0) AS total_draw_times").
|
|
Group("user_id").
|
|
Scan(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[int64]int64, len(rows))
|
|
for _, row := range rows {
|
|
result[row.UserID] = row.TotalDrawTimes
|
|
}
|
|
return result, nil
|
|
}
|