423 lines
13 KiB
Go

package rechargereward
import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"strings"
"time"
)
var personalMonthlyRechargeTypes = []string{
"UNDEFINED",
"HUAWEI",
"GOOGLE",
"APPLE",
"PAYER_MAX",
"SHIPPING_AGENT",
"STRIPE",
"SALARY_EXCHANGE",
"PAY_PAL",
"CLIPSPAY",
"SELLER_AGENT",
}
type rechargeRewardClaimRecordRow struct {
UserID int64 `gorm:"column:user_id"`
RechargeAmount string `gorm:"column:recharge_amount"`
LastRechargeTime string `gorm:"column:last_recharge_time"`
LastClaimTime string `gorm:"column:last_claim_time"`
Account string `gorm:"column:account"`
UserAvatar string `gorm:"column:user_avatar"`
UserNickname string `gorm:"column:user_nickname"`
CountryCode string `gorm:"column:country_code"`
CountryName string `gorm:"column:country_name"`
}
type rechargeRewardClaimDetailRow struct {
UserID int64 `gorm:"column:user_id"`
ActivityID int64 `gorm:"column:activity_id"`
Level int `gorm:"column:level"`
ResourceGroupID *int64 `gorm:"column:resource_group_id"`
RuleDescription string `gorm:"column:rule_description"`
JSONData string `gorm:"column:json_data"`
ClaimTime string `gorm:"column:claim_time"`
}
// PageClaimRecords 分页返回真实领取过累计充值奖励的用户。
func (s *Service) PageClaimRecords(ctx context.Context, sysOrigin string, month string, userID int64, cursor int, limit int) (*RechargeRewardClaimRecordPageResponse, error) {
sysOrigin = s.normalizeSysOrigin(sysOrigin)
cursor, limit = NormalizePage(cursor, limit)
bundle, err := s.loadConfigBundle(ctx, sysOrigin)
if err != nil {
return nil, err
}
location := s.location
if bundle.Config != nil {
location = resolveLocation(bundle.Config.Timezone, s.location)
}
rechargeDate, displayMonth, err := normalizeRechargeRewardRecordMonth(month, time.Now().In(location))
if err != nil {
return nil, err
}
var total int64
countSQL := `
SELECT COUNT(DISTINCT receive.user_id)
FROM user_activity_reward_receive_record AS receive
INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id
WHERE receive.recharge_date = ?
AND rule.sys_origin = ?
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
AND COALESCE(rule.is_del, 0) = 0
AND (? = 0 OR receive.user_id = ?)`
if err := s.db.WithContext(ctx).Raw(
countSQL,
rechargeDate,
sysOrigin,
userID,
userID,
).Scan(&total).Error; err != nil {
return nil, err
}
records := make([]RechargeRewardClaimRecordView, 0)
if total > 0 {
var rows []rechargeRewardClaimRecordRow
listSQL := `
SELECT
claims.user_id,
COALESCE(recharge.recharge_amount, 0) AS recharge_amount,
COALESCE(recharge.last_recharge_time, '') AS last_recharge_time,
claims.last_claim_time,
user.account,
user.user_avatar,
user.user_nickname,
user.country_code,
user.country_name
FROM (
SELECT
receive.user_id,
MAX(receive.create_time) AS last_claim_time
FROM user_activity_reward_receive_record AS receive
INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id
WHERE receive.recharge_date = ?
AND rule.sys_origin = ?
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
AND COALESCE(rule.is_del, 0) = 0
AND (? = 0 OR receive.user_id = ?)
GROUP BY receive.user_id
ORDER BY MAX(receive.create_time) DESC, receive.user_id ASC
LIMIT ? OFFSET ?
) AS claims
INNER JOIN user_base_info AS user ON user.id = claims.user_id
LEFT JOIN (
SELECT
recharge.user_id,
SUM(recharge.amount) AS recharge_amount,
MAX(recharge.update_time) AS last_recharge_time
FROM user_monthly_recharge_v2 AS recharge
WHERE recharge.recharge_date = ?
AND recharge.type IN ?
GROUP BY recharge.user_id
) AS recharge ON recharge.user_id = claims.user_id
ORDER BY claims.last_claim_time DESC, claims.user_id ASC`
if err := s.db.WithContext(ctx).Raw(
listSQL,
rechargeDate,
sysOrigin,
userID,
userID,
limit,
(cursor-1)*limit,
rechargeDate,
personalMonthlyRechargeTypes,
).Scan(&rows).Error; err != nil {
return nil, err
}
details, err := s.loadClaimDetails(ctx, sysOrigin, rechargeDate, claimRecordUserIDs(rows))
if err != nil {
return nil, err
}
rewardItems := s.loadRewardItemsMap(ctx, claimDetailLevels(details))
records = make([]RechargeRewardClaimRecordView, 0, len(rows))
for _, row := range rows {
records = append(records, rechargeRewardClaimRecordView(row, displayMonth, rechargeDate, details[row.UserID], rewardItems))
}
}
return &RechargeRewardClaimRecordPageResponse{
Month: displayMonth,
RechargeDate: rechargeDate,
Timezone: location.String(),
Records: records,
Total: total,
Current: cursor,
Size: limit,
}, nil
}
func emptyRechargeRewardClaimRecordPage(month string, rechargeDate int, timezone string, cursor int, limit int) *RechargeRewardClaimRecordPageResponse {
return &RechargeRewardClaimRecordPageResponse{
Month: month,
RechargeDate: rechargeDate,
Timezone: timezone,
Records: []RechargeRewardClaimRecordView{},
Total: 0,
Current: cursor,
Size: limit,
}
}
func normalizeRechargeRewardRecordMonth(raw string, now time.Time) (int, string, error) {
value := strings.TrimSpace(raw)
if value == "" {
value = now.Format("2006-01")
}
if len(value) == 6 && !strings.Contains(value, "-") {
value = value[:4] + "-" + value[4:]
}
parsed, err := time.Parse("2006-01", value)
if err != nil {
return 0, "", NewAppError(http.StatusBadRequest, "invalid_month", "month must be YYYY-MM")
}
rechargeDate := parsed.Year()*100 + int(parsed.Month())
return rechargeDate, parsed.Format("2006-01"), nil
}
func enabledRechargeRewardLevels(levels []rechargeRewardLevelSnapshot) []rechargeRewardLevelSnapshot {
result := make([]rechargeRewardLevelSnapshot, 0, len(levels))
for _, level := range levels {
if !level.Enabled || level.RechargeAmountCents <= 0 {
continue
}
result = append(result, level)
}
sortLevels(result)
return result
}
func rechargeRewardClaimRecordView(
row rechargeRewardClaimRecordRow,
month string,
rechargeDate int,
details []rechargeRewardClaimDetailRow,
rewardItems map[int64][]RechargeRewardRewardItem,
) RechargeRewardClaimRecordView {
rechargeCents := parseIntegrationAmountCents(integrationAmount(row.RechargeAmount))
reached := claimDetailPayloads(details, rewardItems)
return RechargeRewardClaimRecordView{
RecordKey: fmt.Sprintf("%d-%d", row.UserID, rechargeDate),
UserID: row.UserID,
Account: strings.TrimSpace(row.Account),
UserAvatar: strings.TrimSpace(row.UserAvatar),
UserNickname: strings.TrimSpace(row.UserNickname),
CountryCode: strings.TrimSpace(row.CountryCode),
CountryName: strings.TrimSpace(row.CountryName),
RechargeMonth: month,
RechargeDate: rechargeDate,
RechargeAmount: formatAmountCents(rechargeCents),
RechargeAmountCents: rechargeCents,
ReachedLevels: reached,
ClaimedRewards: rechargeRewardClaimedRewards(reached),
LastRechargeTime: formatRechargeRewardRecordTime(row.LastRechargeTime),
}
}
func (s *Service) loadClaimDetails(ctx context.Context, sysOrigin string, rechargeDate int, userIDs []int64) (map[int64][]rechargeRewardClaimDetailRow, error) {
if len(userIDs) == 0 {
return map[int64][]rechargeRewardClaimDetailRow{}, nil
}
var rows []rechargeRewardClaimDetailRow
detailSQL := `
SELECT
receive.user_id,
receive.activity_id,
COALESCE(rule.sort, 0) AS level,
rule.resource_group_id,
rule.rule_description,
rule.json_data,
receive.create_time AS claim_time
FROM user_activity_reward_receive_record AS receive
INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id
WHERE receive.recharge_date = ?
AND rule.sys_origin = ?
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
AND COALESCE(rule.is_del, 0) = 0
AND receive.user_id IN ?
ORDER BY receive.user_id ASC, COALESCE(rule.sort, 0) ASC, receive.create_time ASC`
if err := s.db.WithContext(ctx).Raw(detailSQL, rechargeDate, sysOrigin, userIDs).Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[int64][]rechargeRewardClaimDetailRow, len(userIDs))
for _, row := range rows {
result[row.UserID] = append(result[row.UserID], row)
}
return result, nil
}
func claimRecordUserIDs(rows []rechargeRewardClaimRecordRow) []int64 {
result := make([]int64, 0, len(rows))
seen := make(map[int64]struct{}, len(rows))
for _, row := range rows {
if row.UserID <= 0 {
continue
}
if _, exists := seen[row.UserID]; exists {
continue
}
seen[row.UserID] = struct{}{}
result = append(result, row.UserID)
}
return result
}
func claimDetailLevels(details map[int64][]rechargeRewardClaimDetailRow) []rechargeRewardLevelSnapshot {
levels := make([]rechargeRewardLevelSnapshot, 0)
seen := make(map[int64]struct{})
for _, rows := range details {
for _, row := range rows {
if row.ResourceGroupID == nil || *row.ResourceGroupID <= 0 {
continue
}
if _, exists := seen[*row.ResourceGroupID]; exists {
continue
}
seen[*row.ResourceGroupID] = struct{}{}
levels = append(levels, rechargeRewardLevelSnapshot{
RewardGroupID: row.ResourceGroupID,
Enabled: true,
})
}
}
return levels
}
func claimDetailPayloads(
details []rechargeRewardClaimDetailRow,
rewardItems map[int64][]RechargeRewardRewardItem,
) []RechargeRewardLevelPayload {
result := make([]RechargeRewardLevelPayload, 0, len(details))
for index, detail := range details {
level := detail.Level
if level <= 0 {
level = index + 1
}
var items []RechargeRewardRewardItem
if detail.ResourceGroupID != nil {
items = rewardItems[*detail.ResourceGroupID]
}
thresholdCents := rechargeRewardRuleQuantityCents(detail.JSONData)
result = append(result, RechargeRewardLevelPayload{
ID: detail.ActivityID,
Level: level,
RechargeAmount: formatAmountCents(thresholdCents),
RechargeAmountCents: thresholdCents,
RewardGroupID: detail.ResourceGroupID,
RewardGroupName: strings.TrimSpace(detail.RuleDescription),
RewardItems: items,
Enabled: true,
Reached: true,
})
}
return result
}
func rechargeRewardRuleQuantityCents(raw string) int64 {
var payload map[string]any
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil {
return 0
}
value, ok := payload["quantity"]
if !ok {
return 0
}
switch typed := value.(type) {
case float64:
return int64(math.Round(typed * 100))
case string:
cents, err := parseAmountCents(typed)
if err != nil {
return 0
}
return cents
default:
cents, err := parseAmountCents(fmt.Sprint(typed))
if err != nil {
return 0
}
return cents
}
}
func reachedRechargeRewardLevels(
levels []rechargeRewardLevelSnapshot,
rewardItems map[int64][]RechargeRewardRewardItem,
rechargeCents int64,
) []RechargeRewardLevelPayload {
result := make([]RechargeRewardLevelPayload, 0)
for _, level := range levels {
if rechargeCents < level.RechargeAmountCents {
continue
}
var items []RechargeRewardRewardItem
if level.RewardGroupID != nil {
items = rewardItems[*level.RewardGroupID]
}
result = append(result, RechargeRewardLevelPayload{
ID: level.ID,
Level: level.Level,
RechargeAmount: formatAmountCents(level.RechargeAmountCents),
RechargeAmountCents: level.RechargeAmountCents,
RewardGold: level.RewardGold,
RewardGroupID: level.RewardGroupID,
RewardGroupName: level.RewardGroupName,
RewardItems: items,
Enabled: level.Enabled,
Reached: true,
})
}
return result
}
func rechargeRewardClaimedRewards(levels []RechargeRewardLevelPayload) []RechargeRewardClaimedRewardPayload {
result := make([]RechargeRewardClaimedRewardPayload, 0, len(levels))
for _, level := range levels {
result = append(result, RechargeRewardClaimedRewardPayload{
Level: level.Level,
RechargeAmount: level.RechargeAmount,
RechargeAmountCents: level.RechargeAmountCents,
RewardGold: level.RewardGold,
RewardGroupID: level.RewardGroupID,
RewardGroupName: level.RewardGroupName,
RewardItems: level.RewardItems,
})
}
return result
}
func formatRechargeRewardRecordTime(raw string) string {
value := strings.TrimSpace(raw)
if value == "" {
return ""
}
for _, layout := range []string{
"2006-01-02 15:04:05.999999999-07:00",
"2006-01-02 15:04:05.999999999",
"2006-01-02 15:04:05",
time.RFC3339Nano,
time.RFC3339,
} {
if parsed, err := time.Parse(layout, value); err == nil {
return formatDateTime(parsed)
}
}
if len(value) > len("2006-01-02 15:04:05") {
return value[:len("2006-01-02 15:04:05")]
}
return value
}