499 lines
19 KiB
Go
499 lines
19 KiB
Go
package activity
|
||
|
||
import (
|
||
"bindbox-game/internal/pkg/logger"
|
||
paypkg "bindbox-game/internal/pkg/pay"
|
||
"bindbox-game/internal/pkg/util/remark"
|
||
"bindbox-game/internal/repository/mysql"
|
||
"bindbox-game/internal/repository/mysql/dao"
|
||
"bindbox-game/internal/repository/mysql/model"
|
||
usersvc "bindbox-game/internal/service/user"
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
type scheduledConfig struct {
|
||
PlayType string `json:"play_type"`
|
||
DrawMode string `json:"draw_mode"`
|
||
MinParticipants int64 `json:"min_participants"`
|
||
ScheduledTime string `json:"scheduled_time"`
|
||
ScheduledDelayMinutes int64 `json:"scheduled_delay_minutes"`
|
||
IntervalMinutes int64 `json:"interval_minutes"`
|
||
LastSettledAt string `json:"last_settled_at"`
|
||
RefundCouponType string `json:"refund_coupon_type"`
|
||
RefundCouponAmount float64 `json:"refund_coupon_amount"`
|
||
RefundCouponID int64 `json:"refund_coupon_id"`
|
||
}
|
||
|
||
func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo, rdb *redis.Client) {
|
||
r := dao.Use(repo.GetDbR())
|
||
w := dao.Use(repo.GetDbW())
|
||
us := usersvc.New(l, repo)
|
||
activitySvc := New(l, repo, us, rdb)
|
||
|
||
go func() {
|
||
t := time.NewTicker(30 * time.Second)
|
||
defer t.Stop()
|
||
for range t.C {
|
||
ctx := context.Background()
|
||
now := time.Now()
|
||
l.Debug("定时开奖: 开始检查", zap.Time("now", now))
|
||
|
||
// 【独立检查】一番赏格位重置:每30秒检查所有售罄的一番赏期号
|
||
checkAndResetIchibanSlots(ctx, l, repo, r)
|
||
var acts []struct {
|
||
ID int64
|
||
PlayType string
|
||
DrawMode string
|
||
MinParticipants int64
|
||
IntervalMinutes int64
|
||
ScheduledTime *time.Time
|
||
RefundCouponID int64
|
||
LastSettledAt *time.Time
|
||
}
|
||
_ = repo.GetDbR().Raw("SELECT id, play_type, draw_mode, min_participants, interval_minutes, scheduled_time, refund_coupon_id, last_settled_at FROM activities WHERE draw_mode='scheduled' AND (scheduled_time IS NOT NULL OR interval_minutes > 0)").Scan(&acts)
|
||
l.Debug("定时开奖: 查询到活动", zap.Int("count", len(acts)))
|
||
for _, a := range acts {
|
||
l.Debug("定时开奖: 检查活动",
|
||
zap.Int64("id", a.ID),
|
||
zap.String("play_type", a.PlayType),
|
||
zap.Int64("interval", a.IntervalMinutes),
|
||
zap.Reflect("scheduled_time", a.ScheduledTime),
|
||
zap.Reflect("last_settled", a.LastSettledAt))
|
||
|
||
// 计算开奖时间
|
||
st := time.Time{}
|
||
if a.ScheduledTime != nil {
|
||
st = *a.ScheduledTime
|
||
}
|
||
|
||
// 【修复】如果 scheduled_time 是过去的时间且 interval_minutes > 0,
|
||
// 跳过所有过期窗口,推进到最近的一个开奖时间
|
||
if !st.IsZero() && now.After(st) && a.IntervalMinutes > 0 {
|
||
// 计算需要跳过多少个周期
|
||
elapsed := now.Sub(st)
|
||
periods := int64(elapsed.Minutes()) / a.IntervalMinutes
|
||
if periods > 0 {
|
||
// 跳过所有完整的过期周期,st 变成最近的开奖时间点
|
||
st = st.Add(time.Duration(periods*a.IntervalMinutes) * time.Minute)
|
||
l.Info("定时开奖: 跳过过期周期",
|
||
zap.Int64("id", a.ID),
|
||
zap.Int64("periods", periods),
|
||
zap.Time("new_st", st))
|
||
}
|
||
}
|
||
|
||
if st.IsZero() && a.IntervalMinutes > 0 {
|
||
if a.LastSettledAt != nil && !a.LastSettledAt.IsZero() {
|
||
st = a.LastSettledAt.Add(time.Duration(a.IntervalMinutes) * time.Minute)
|
||
} else {
|
||
st = now
|
||
}
|
||
}
|
||
|
||
l.Debug("定时开奖: 计算开奖时间",
|
||
zap.Int64("id", a.ID),
|
||
zap.Time("st", st),
|
||
zap.Time("now", now),
|
||
zap.Bool("skip", st.IsZero() || now.Before(st)))
|
||
|
||
if st.IsZero() || now.Before(st) {
|
||
continue
|
||
}
|
||
|
||
aid := a.ID
|
||
// 【修复】使用 last_settled_at 或很早的时间作为查询起点
|
||
last := time.Time{}
|
||
if a.LastSettledAt != nil && !a.LastSettledAt.IsZero() {
|
||
last = *a.LastSettledAt
|
||
}
|
||
|
||
// 【修复】查询从 last 到 now 的所有订单(而非到 st),确保能找到最新订单
|
||
l.Debug("定时开奖: 查询订单范围",
|
||
zap.Int64("id", aid),
|
||
zap.Time("last", last),
|
||
zap.Time("now", now))
|
||
orders, _ := r.Orders.WithContext(ctx).ReadDB().Where(
|
||
r.Orders.Status.Eq(2),
|
||
r.Orders.SourceType.Eq(2),
|
||
r.Orders.IsConsumed.Eq(0), // 仅处理未履约的订单
|
||
r.Orders.Remark.Like(fmt.Sprintf("lottery:activity:%d|%%", aid)),
|
||
r.Orders.CreatedAt.Gte(last),
|
||
).Find()
|
||
count := int64(len(orders))
|
||
l.Debug("定时开奖: 查询到订单",
|
||
zap.Int64("id", aid),
|
||
zap.Int64("count", count),
|
||
zap.Int64("min", a.MinParticipants))
|
||
|
||
// Initialize Wechat Client if needed
|
||
wc, _ := paypkg.NewWechatPayClient(ctx)
|
||
|
||
refundedIssues := make(map[int64]bool)
|
||
|
||
// 【优化】一番赏定时退款:检查是否售罄
|
||
if a.PlayType == "ichiban" {
|
||
issueIDs := make(map[int64]struct{})
|
||
for _, o := range orders {
|
||
iss := remark.Parse(o.Remark).IssueID
|
||
if iss > 0 {
|
||
issueIDs[iss] = struct{}{}
|
||
}
|
||
}
|
||
|
||
// 【Fix】Also check all active issues for this activity, regardless of recent orders
|
||
// This ensures expired issues with no recent orders are also processed
|
||
var activeIssues []struct {
|
||
ID int64
|
||
}
|
||
// status=1 means active/processing
|
||
_ = r.ActivityIssues.WithContext(ctx).ReadDB().Where(
|
||
r.ActivityIssues.ActivityID.Eq(aid),
|
||
r.ActivityIssues.Status.Eq(1),
|
||
).Scan(&activeIssues)
|
||
|
||
for _, ai := range activeIssues {
|
||
issueIDs[ai.ID] = struct{}{}
|
||
}
|
||
|
||
for iss := range issueIDs {
|
||
// Check Sales
|
||
// 一番赏:每种奖品 = 1个格位
|
||
totalSlots, _ := r.ActivityRewardSettings.WithContext(ctx).Where(r.ActivityRewardSettings.IssueID.Eq(iss)).Count()
|
||
|
||
soldSlots, _ := r.IssuePositionClaims.WithContext(ctx).Where(r.IssuePositionClaims.IssueID.Eq(iss)).Count()
|
||
|
||
l.Debug("定时开奖-一番赏: 检查售罄",
|
||
zap.Int64("issue_id", iss),
|
||
zap.Int64("sold", soldSlots),
|
||
zap.Int64("total", totalSlots))
|
||
|
||
if soldSlots < totalSlots {
|
||
l.Info("定时开奖-一番赏: 未售罄,执行全额退款", zap.Int64("issue_id", iss))
|
||
refundedIssues[iss] = true
|
||
|
||
// Find ALL valid orders for this issue
|
||
issueOrders, _ := r.Orders.WithContext(ctx).Where(
|
||
r.Orders.Status.Eq(2),
|
||
r.Orders.SourceType.Eq(2),
|
||
r.Orders.Remark.Like(fmt.Sprintf("%%issue:%d|%%", iss)),
|
||
).Find()
|
||
|
||
for _, o := range issueOrders {
|
||
// Check if the order has already been drawn (has logs)
|
||
logCount, _ := r.ActivityDrawLogs.WithContext(ctx).Where(
|
||
r.ActivityDrawLogs.OrderID.Eq(o.ID),
|
||
r.ActivityDrawLogs.IssueID.Eq(iss),
|
||
).Count()
|
||
|
||
if logCount > 0 {
|
||
l.Info("定时开奖-一番赏: 订单已开奖,跳过退款", zap.Int64("order_id", o.ID), zap.Int64("issue_id", iss))
|
||
continue
|
||
}
|
||
|
||
refundOrder(ctx, l, o, "ichiban_not_sold_out", wc, r, w, us, a.RefundCouponID)
|
||
}
|
||
} else {
|
||
// 【Fix】已售罄:处理所有未开奖的订单(包括旧时间窗口的订单)
|
||
l.Info("定时开奖-一番赏: 已售罄,检查并处理所有未开奖订单", zap.Int64("issue_id", iss))
|
||
|
||
// Step 1: Get all order IDs from claims for this issue
|
||
var claimOrderIDs []int64
|
||
errClaims := repo.GetDbR().Raw(`
|
||
SELECT DISTINCT c.order_id FROM issue_position_claims c
|
||
LEFT JOIN activity_draw_logs l ON l.order_id = c.order_id AND l.issue_id = c.issue_id
|
||
WHERE c.issue_id = ? AND l.id IS NULL
|
||
`, iss).Scan(&claimOrderIDs).Error
|
||
|
||
if errClaims != nil {
|
||
l.Error("定时开奖-一番赏: 查询未处理订单ID失败", zap.Int64("issue_id", iss), zap.Error(errClaims))
|
||
}
|
||
|
||
l.Debug("定时开奖-一番赏: 查询到未处理订单",
|
||
zap.Int64("issue_id", iss),
|
||
zap.Int("count", len(claimOrderIDs)),
|
||
zap.Reflect("order_ids", claimOrderIDs))
|
||
|
||
if len(claimOrderIDs) > 0 {
|
||
// Step 2: Fetch the actual order records
|
||
unprocessedOrders, errOrders := r.Orders.WithContext(ctx).Where(
|
||
r.Orders.ID.In(claimOrderIDs...),
|
||
r.Orders.Status.Eq(2),
|
||
).Find()
|
||
|
||
if errOrders != nil {
|
||
l.Error("定时开奖-一番赏: 获取订单详情失败", zap.Error(errOrders))
|
||
}
|
||
|
||
l.Info("定时开奖-一番赏: 开始补录未处理订单",
|
||
zap.Int64("issue_id", iss),
|
||
zap.Int("count", len(unprocessedOrders)))
|
||
for _, o := range unprocessedOrders {
|
||
if err := activitySvc.ProcessOrderLottery(ctx, o.ID); err != nil {
|
||
l.Error("定时开奖-一番赏-补录: ProcessOrderLottery 失败",
|
||
zap.Int64("order_id", o.ID),
|
||
zap.Error(err))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 【直接重置】无论是否有待处理订单,检查该期号是否可以重置
|
||
var remainingUnprocessed int64
|
||
_ = repo.GetDbR().Raw(`
|
||
SELECT COUNT(1) FROM issue_position_claims c
|
||
LEFT JOIN activity_draw_logs l ON l.order_id = c.order_id AND l.issue_id = c.issue_id
|
||
WHERE c.issue_id = ? AND l.id IS NULL
|
||
`, iss).Scan(&remainingUnprocessed)
|
||
|
||
l.Debug("定时开奖-一番赏: 剩余未处理订单记录",
|
||
zap.Int64("issue_id", iss),
|
||
zap.Int64("count", remainingUnprocessed))
|
||
|
||
if remainingUnprocessed == 0 {
|
||
// 所有订单都已处理,执行重置
|
||
if err := repo.GetDbW().Exec("DELETE FROM issue_position_claims WHERE issue_id = ?", iss).Error; err != nil {
|
||
l.Error("定时开奖-一番赏: 重置格位失败", zap.Int64("issue_id", iss), zap.Error(err))
|
||
} else {
|
||
l.Info("定时开奖-一番赏: 格位已重置,新一轮可以开始", zap.Int64("issue_id", iss))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
shouldRefund := false
|
||
if a.PlayType != "ichiban" {
|
||
if count < a.MinParticipants {
|
||
shouldRefund = true
|
||
}
|
||
}
|
||
|
||
if shouldRefund {
|
||
l.Info("定时开奖: 人数不足,执行退款完毕", zap.Int64("id", aid))
|
||
for _, o := range orders {
|
||
refundOrder(ctx, l, o, "scheduled_not_enough", wc, r, w, us, a.RefundCouponID)
|
||
}
|
||
} else {
|
||
l.Info("定时开奖: 人数满足,开始开奖处理", zap.Int64("id", aid))
|
||
for _, o := range orders {
|
||
iss := remark.Parse(o.Remark).IssueID
|
||
if a.PlayType == "ichiban" && refundedIssues[iss] {
|
||
l.Debug("定时开奖-一番赏: 订单已退款,跳过开奖", zap.Int64("order_id", o.ID), zap.Int64("issue_id", iss))
|
||
continue
|
||
}
|
||
if err := activitySvc.ProcessOrderLottery(ctx, o.ID); err != nil {
|
||
l.Error("定时开奖: ProcessOrderLottery 失败", zap.Int64("order_id", o.ID), zap.Error(err))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 【修复】无论成功开奖还是退款,都更新活动的结算时间与下次计划
|
||
var nextVal sql.NullTime
|
||
if a.IntervalMinutes > 0 {
|
||
// 更新开奖时间戳
|
||
next := now.Add(time.Duration(a.IntervalMinutes) * time.Minute)
|
||
nextVal = sql.NullTime{Time: next.UTC(), Valid: true}
|
||
l.Info("定时开奖: 更新活动下次结算时间",
|
||
zap.Int64("id", aid),
|
||
zap.Time("last", now),
|
||
zap.Time("next", next))
|
||
} else {
|
||
// 如果没有间隔,则不设置下次计划时间
|
||
nextVal = sql.NullTime{Valid: false}
|
||
l.Info("定时开奖: 活动无间隔,不设置下次计划时间", zap.Int64("id", aid), zap.Time("last", now))
|
||
}
|
||
_ = repo.GetDbW().WithContext(ctx).Exec("UPDATE activities SET last_settled_at=?, scheduled_time=? WHERE id= ?", now.UTC(), nextVal, aid).Error
|
||
}
|
||
|
||
// 即时开奖:处理所有已支付且未记录抽奖日志的订单
|
||
var instantActs []struct {
|
||
ID int64
|
||
}
|
||
_ = repo.GetDbR().WithContext(ctx).Raw("SELECT id FROM activities WHERE draw_mode='instant'").Scan(&instantActs)
|
||
if len(instantActs) > 0 {
|
||
for _, ia := range instantActs {
|
||
orders2, _ := r.Orders.WithContext(ctx).ReadDB().Where(
|
||
r.Orders.Status.Eq(2),
|
||
r.Orders.SourceType.Eq(2),
|
||
r.Orders.IsConsumed.Eq(0), // 仅处理未履约的订单
|
||
r.Orders.Remark.Like(fmt.Sprintf("lottery:activity:%d|%%", ia.ID)),
|
||
r.Orders.CreatedAt.Lt(now.Add(-5*time.Minute)),
|
||
r.Orders.CreatedAt.Gt(now.Add(-24*time.Hour)), // 限制时间窗口为24小时,避免全表扫描
|
||
).Find()
|
||
for _, o2 := range orders2 {
|
||
if err := activitySvc.ProcessOrderLottery(ctx, o2.ID); err != nil {
|
||
l.Error("即时开奖补偿: ProcessOrderLottery 失败", zap.Int64("order_id", o2.ID), zap.Error(err))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// checkAndResetIchibanSlots 检查并重置所有售罄且已完成的一番赏期号
|
||
func checkAndResetIchibanSlots(ctx context.Context, l logger.CustomLogger, repo mysql.Repo, r *dao.Query) {
|
||
// 查找所有一番赏活动下的活跃期号
|
||
var issuesWithClaims []struct {
|
||
IssueID int64
|
||
TotalSlots int64
|
||
SoldSlots int64
|
||
}
|
||
|
||
err := repo.GetDbR().Raw(`
|
||
SELECT
|
||
ai.id as issue_id,
|
||
(SELECT COUNT(*) FROM activity_reward_settings WHERE issue_id = ai.id) as total_slots,
|
||
(SELECT COUNT(*) FROM issue_position_claims WHERE issue_id = ai.id) as sold_slots
|
||
FROM activity_issues ai
|
||
INNER JOIN activities a ON a.id = ai.activity_id
|
||
WHERE a.play_type = 'ichiban' AND a.status = 1 AND ai.status = 1
|
||
HAVING sold_slots > 0 AND sold_slots >= total_slots
|
||
`).Scan(&issuesWithClaims).Error
|
||
|
||
if err != nil {
|
||
l.Error("一番赏重置检查: 查询失败", zap.Error(err))
|
||
return
|
||
}
|
||
|
||
for _, iss := range issuesWithClaims {
|
||
// 检查是否所有订单都已处理(有draw logs)
|
||
var unprocessedCnt int64
|
||
_ = repo.GetDbR().Raw(`
|
||
SELECT COUNT(1) FROM issue_position_claims c
|
||
LEFT JOIN activity_draw_logs l ON l.order_id = c.order_id AND l.issue_id = c.issue_id
|
||
WHERE c.issue_id = ? AND l.id IS NULL
|
||
`, iss.IssueID).Scan(&unprocessedCnt)
|
||
|
||
if unprocessedCnt == 0 {
|
||
// 所有订单都已处理,执行重置
|
||
if err := repo.GetDbW().Exec("DELETE FROM issue_position_claims WHERE issue_id = ?", iss.IssueID).Error; err != nil {
|
||
l.Error("一番赏重置检查: 重置失败", zap.Int64("issue_id", iss.IssueID), zap.Error(err))
|
||
} else {
|
||
l.Info("一番赏重置检查: 重置通过", zap.Int64("issue_id", iss.IssueID))
|
||
}
|
||
} else {
|
||
l.Debug("一番赏重置检查: 仍有未处理订单", zap.Int64("issue_id", iss.IssueID), zap.Int64("unprocessed", unprocessedCnt))
|
||
}
|
||
}
|
||
}
|
||
|
||
func parseTime(s string) time.Time {
|
||
if s == "" {
|
||
return time.Time{}
|
||
}
|
||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||
return t
|
||
}
|
||
if t2, err2 := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local); err2 == nil {
|
||
return t2
|
||
}
|
||
return time.Time{}
|
||
}
|
||
|
||
// uploadVirtualShippingForScheduledDraw 定时开奖后上传虚拟发货
|
||
// 收集中奖产品名称并调用微信虚拟发货API
|
||
// playType: 活动玩法类型,只有 ichiban 时才发送开奖结果通知
|
||
func refundOrder(ctx context.Context, l logger.CustomLogger, o *model.Orders, reason string, wc *paypkg.WechatPayClient, r *dao.Query, w *dao.Query, us usersvc.Service, refundCouponID int64) {
|
||
// 1. Refund Points
|
||
if o.PointsAmount > 0 {
|
||
refundPts := o.PointsAmount / 100
|
||
_, _ = us.RefundPoints(ctx, o.UserID, refundPts, o.OrderNo, reason)
|
||
}
|
||
|
||
// 2. Refund WeChat
|
||
if o.ActualAmount > 0 && wc != nil {
|
||
refundNo := fmt.Sprintf("R%s-%d", o.OrderNo, time.Now().Unix())
|
||
refundID, status, err := wc.RefundOrder(ctx, o.OrderNo, refundNo, o.ActualAmount, o.ActualAmount, reason)
|
||
if err == nil {
|
||
// 修复:raw 字段需要有效的 JSON 值,不能为空
|
||
rawJSON := fmt.Sprintf(`{"refund_id":"%s","status":"%s"}`, refundID, status)
|
||
_ = w.PaymentRefunds.WithContext(ctx).Create(&model.PaymentRefunds{
|
||
OrderID: o.ID,
|
||
OrderNo: o.OrderNo,
|
||
RefundNo: refundNo,
|
||
Channel: "wechat_jsapi",
|
||
Status: status,
|
||
AmountRefund: o.ActualAmount,
|
||
Reason: reason,
|
||
SuccessTime: time.Now(),
|
||
Raw: rawJSON,
|
||
})
|
||
}
|
||
if err != nil {
|
||
l.Error("Refund: WeChat refund failed", zap.String("order_no", o.OrderNo), zap.Error(err))
|
||
}
|
||
}
|
||
|
||
// 3. Refund Used Coupons
|
||
ocs, _ := r.OrderCoupons.WithContext(ctx).Where(r.OrderCoupons.OrderID.Eq(o.ID)).Find()
|
||
for _, oc := range ocs {
|
||
// Restore user coupon status to 1 (Unused) and clear usage info
|
||
_, err := w.UserCoupons.WithContext(ctx).Where(w.UserCoupons.ID.Eq(oc.UserCouponID)).Updates(map[string]interface{}{
|
||
"status": 1,
|
||
"used_order_id": 0,
|
||
"used_at": nil,
|
||
})
|
||
if err != nil {
|
||
l.Error("Refund: Failed to restore coupon", zap.Int64("ucID", oc.UserCouponID), zap.String("order_no", o.OrderNo), zap.Error(err))
|
||
} else {
|
||
l.Info("Refund: Restored coupon", zap.Int64("ucID", oc.UserCouponID), zap.String("order_no", o.OrderNo))
|
||
}
|
||
}
|
||
|
||
// 3.5. 一番赏退款:删除 issue_position_claims 记录,恢复格位
|
||
iss := remark.Parse(o.Remark).IssueID
|
||
if iss > 0 {
|
||
result, err := w.IssuePositionClaims.WithContext(ctx).Where(
|
||
w.IssuePositionClaims.OrderID.Eq(o.ID),
|
||
).Delete()
|
||
if err != nil {
|
||
l.Error("Refund: Failed to delete position claims", zap.Int64("order_id", o.ID), zap.Error(err))
|
||
} else {
|
||
l.Info("Refund: Restored slot positions", zap.Int64("order_id", o.ID), zap.Int64("rows", result.RowsAffected))
|
||
}
|
||
}
|
||
|
||
// 3.6. 退还道具卡(支持两种记录方式)
|
||
// 方式1:从 activity_draw_effects 表查询(无限赏等游戏类型)
|
||
var itemCardIDs []int64
|
||
_ = r.Orders.WithContext(ctx).UnderlyingDB().Raw("SELECT user_item_card_id FROM activity_draw_effects WHERE draw_log_id IN (SELECT id FROM activity_draw_logs WHERE order_id=?)", o.ID).Scan(&itemCardIDs).Error
|
||
// 方式2:从 user_item_cards 表的 used_draw_log_id 直接查询(对对碰等游戏类型)
|
||
var itemCardIDsFromItemCards []int64
|
||
_ = r.Orders.WithContext(ctx).UnderlyingDB().Raw("SELECT id FROM user_item_cards WHERE used_draw_log_id IN (SELECT id FROM activity_draw_logs WHERE order_id=?) AND status=2", o.ID).Scan(&itemCardIDsFromItemCards).Error
|
||
// 合并去重
|
||
idSet := make(map[int64]struct{})
|
||
for _, icID := range itemCardIDs {
|
||
if icID > 0 {
|
||
idSet[icID] = struct{}{}
|
||
}
|
||
}
|
||
for _, icID := range itemCardIDsFromItemCards {
|
||
if icID > 0 {
|
||
idSet[icID] = struct{}{}
|
||
}
|
||
}
|
||
// 执行退还
|
||
for icID := range idSet {
|
||
_ = w.Orders.WithContext(ctx).UnderlyingDB().Exec("UPDATE user_item_cards SET status=1, used_at=NULL, used_draw_log_id=0, used_activity_id=0, used_issue_id=0, updated_at=NOW(3) WHERE id=?", icID).Error
|
||
l.Info("Refund: Restored item card", zap.Int64("icID", icID), zap.String("order_no", o.OrderNo))
|
||
}
|
||
|
||
// 4. Update Order Status
|
||
_, _ = w.Orders.WithContext(ctx).Where(w.Orders.ID.Eq(o.ID)).Updates(map[string]any{w.Orders.Status.ColumnName().String(): 4})
|
||
|
||
// 5. Log Refund
|
||
iss = remark.Parse(o.Remark).IssueID
|
||
_ = w.Orders.WithContext(ctx).UnderlyingDB().Exec("INSERT INTO lottery_refund_logs(issue_id, order_id, user_id, amount, coupon_type, coupon_amount, reason, status) VALUES(?,?,?,?,?,?,?,?)", iss, o.ID, o.UserID, o.ActualAmount, "", 0, reason, "done").Error
|
||
|
||
// 6. Compensation
|
||
if refundCouponID > 0 {
|
||
_ = us.AddCoupon(ctx, o.UserID, refundCouponID)
|
||
}
|
||
}
|