250 lines
7.5 KiB
Go
250 lines
7.5 KiB
Go
package app
|
|
|
|
import (
|
|
"bindbox-game/configs"
|
|
"bindbox-game/internal/pkg/wechat"
|
|
"bindbox-game/internal/repository/mysql/model"
|
|
activitysvc "bindbox-game/internal/service/activity"
|
|
usersvc "bindbox-game/internal/service/user"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// grantRewardHelper 发放奖励辅助函数
|
|
func (h *handler) grantRewardHelper(ctx context.Context, userID, orderID int64, r *model.ActivityRewardSettings, quantity int, remark string) error {
|
|
// 1. Grant to Order (Delegating stock check to user service)
|
|
issue, _ := h.readDB.ActivityIssues.WithContext(ctx).Where(h.readDB.ActivityIssues.ID.Eq(r.IssueID)).First()
|
|
var actID int64
|
|
if issue != nil {
|
|
actID = issue.ActivityID
|
|
}
|
|
|
|
rid := r.ID
|
|
_, err := h.user.GrantRewardToOrder(ctx, userID, usersvc.GrantRewardToOrderRequest{
|
|
OrderID: orderID,
|
|
ProductID: r.ProductID,
|
|
Quantity: quantity,
|
|
ActivityID: &actID,
|
|
RewardID: &rid,
|
|
Remark: remark,
|
|
})
|
|
if err != nil {
|
|
h.logger.Error("Failed to grant reward to order", zap.Int64("order_id", orderID), zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
// 2. Update Draw Log (IsWinner = 1)
|
|
_, err = h.writeDB.ActivityDrawLogs.WithContext(ctx).Where(
|
|
h.writeDB.ActivityDrawLogs.OrderID.Eq(orderID),
|
|
).Updates(&model.ActivityDrawLogs{
|
|
IsWinner: 1,
|
|
RewardID: r.ID,
|
|
Level: r.Level,
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
var matchingCleanupOnce sync.Once
|
|
|
|
func (h *handler) startMatchingGameCleanup() {
|
|
matchingCleanupOnce.Do(func() {
|
|
go func() {
|
|
h.logger.Info("对对碰自动开奖: 后台任务已启动")
|
|
ticker := time.NewTicker(3 * time.Minute) // 每 3 分钟扫描一次
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
h.autoCheckExpiredGames()
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
|
|
// autoCheckExpiredGames 扫描超时未结算的对对碰游戏并自动开奖
|
|
func (h *handler) autoCheckExpiredGames() {
|
|
ctx := context.Background()
|
|
|
|
// 1. 扫描 Redis 中所有 matching_game key
|
|
keys, err := h.redis.Keys(ctx, activitysvc.MatchingGameKeyPrefix+"*").Result()
|
|
if err != nil {
|
|
h.logger.Error("对对碰自动开奖: 扫描 Redis 失败", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
if len(keys) == 0 {
|
|
return
|
|
}
|
|
|
|
h.logger.Debug("对对碰自动开奖: 扫描到游戏会话", zap.Int("count", len(keys)))
|
|
|
|
for _, key := range keys {
|
|
gameID := strings.TrimPrefix(key, activitysvc.MatchingGameKeyPrefix)
|
|
if gameID == "" {
|
|
continue
|
|
}
|
|
|
|
game, err := h.activity.GetMatchingGameFromRedis(ctx, gameID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// 2. 检查是否超过 5 分钟
|
|
if game.CreatedAt.IsZero() || time.Since(game.CreatedAt) < 5*time.Minute {
|
|
continue
|
|
}
|
|
|
|
// 3. 检查用户是否还在活跃(最近 2 分钟内有操作则跳过)
|
|
if !game.LastActivity.IsZero() && time.Since(game.LastActivity) < 2*time.Minute {
|
|
h.logger.Debug("对对碰自动开奖: 用户仍在活跃,跳过",
|
|
zap.String("game_id", gameID),
|
|
zap.Time("last_activity", game.LastActivity))
|
|
continue
|
|
}
|
|
|
|
// 3. 检查订单是否已支付
|
|
order, err := h.readDB.Orders.WithContext(ctx).Where(h.readDB.Orders.ID.Eq(game.OrderID)).First()
|
|
if err != nil || order == nil || order.Status != 2 {
|
|
continue
|
|
}
|
|
|
|
// 4. 检查是否已结算(幂等)
|
|
existingLog, _ := h.readDB.ActivityDrawLogs.WithContext(ctx).Where(
|
|
h.readDB.ActivityDrawLogs.OrderID.Eq(game.OrderID),
|
|
h.readDB.ActivityDrawLogs.IsWinner.Eq(1),
|
|
).First()
|
|
if existingLog != nil {
|
|
// 已结算,清理 Redis
|
|
_ = h.redis.Del(ctx, key)
|
|
continue
|
|
}
|
|
|
|
// 5. 执行自动结算
|
|
h.logger.Info("对对碰自动开奖: 触发超时自动结算",
|
|
zap.String("game_id", gameID),
|
|
zap.Int64("order_id", game.OrderID),
|
|
zap.Int64("user_id", game.UserID),
|
|
zap.Time("created_at", game.CreatedAt),
|
|
zap.Duration("elapsed", time.Since(game.CreatedAt)))
|
|
|
|
h.doAutoCheck(ctx, gameID, game, order)
|
|
}
|
|
}
|
|
|
|
// doAutoCheck 执行自动开奖结算(复用 CheckMatchingGame 核心逻辑)
|
|
func (h *handler) doAutoCheck(ctx context.Context, gameID string, game *activitysvc.MatchingGame, order *model.Orders) {
|
|
// 1. 并发锁
|
|
lockKey := fmt.Sprintf("lock:matching_game:check:%s", gameID)
|
|
locked, err := h.redis.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
|
|
if err != nil || !locked {
|
|
return
|
|
}
|
|
defer h.redis.Del(ctx, lockKey)
|
|
|
|
// 2. 检查活动状态
|
|
activity, err := h.activity.GetActivity(ctx, game.ActivityID)
|
|
if err != nil || activity == nil || activity.Status != 1 {
|
|
h.logger.Warn("对对碰自动开奖: 活动状态异常", zap.Int64("activity_id", game.ActivityID))
|
|
return
|
|
}
|
|
|
|
// 3. 服务端模拟计算对数
|
|
serverSimulatedPairs := game.SimulateMaxPairs()
|
|
h.logger.Debug("对对碰自动开奖: 服务端模拟",
|
|
zap.Int64("simulated_pairs", serverSimulatedPairs),
|
|
zap.String("position", game.Position))
|
|
|
|
game.TotalPairs = serverSimulatedPairs
|
|
|
|
// 4. 发放奖励
|
|
var rewardInfo *MatchingRewardInfo
|
|
rewards, err := h.activity.ListIssueRewards(ctx, game.IssueID)
|
|
if err == nil && len(rewards) > 0 {
|
|
var candidate *model.ActivityRewardSettings
|
|
for _, r := range rewards {
|
|
if r.Quantity <= 0 {
|
|
continue
|
|
}
|
|
if serverSimulatedPairs == r.MinScore {
|
|
candidate = r
|
|
break
|
|
}
|
|
}
|
|
|
|
if candidate != nil {
|
|
productName := ""
|
|
if p, _ := h.readDB.Products.WithContext(ctx).Where(h.readDB.Products.ID.Eq(candidate.ProductID)).First(); p != nil {
|
|
productName = p.Name
|
|
}
|
|
|
|
finalRemark := fmt.Sprintf("%s %s (自动开奖)", order.OrderNo, productName)
|
|
|
|
if err := h.grantRewardHelper(ctx, game.UserID, game.OrderID, candidate, 1, finalRemark); err != nil {
|
|
h.logger.Error("对对碰自动开奖: 发放奖励失败", zap.Int64("order_id", game.OrderID), zap.Error(err))
|
|
} else {
|
|
prodImage := ""
|
|
if p, _ := h.readDB.Products.WithContext(ctx).Where(h.readDB.Products.ID.Eq(candidate.ProductID)).First(); p != nil {
|
|
prodImage = getFirstImage(p.ImagesJSON)
|
|
}
|
|
|
|
rewardInfo = &MatchingRewardInfo{
|
|
RewardID: candidate.ID,
|
|
Name: productName,
|
|
ProductName: productName,
|
|
ProductImage: prodImage,
|
|
Level: candidate.Level,
|
|
}
|
|
|
|
h.logger.Info("对对碰自动开奖: 奖励发放成功",
|
|
zap.Int64("order_id", game.OrderID),
|
|
zap.String("product_name", productName),
|
|
zap.Int32("level", candidate.Level))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. 虚拟发货
|
|
rewardName := "无奖励"
|
|
if rewardInfo != nil {
|
|
rewardName = rewardInfo.Name
|
|
}
|
|
|
|
go func(orderID int64, orderNo string, userID int64, rName string) {
|
|
bgCtx := context.Background()
|
|
tx, _ := h.readDB.PaymentTransactions.WithContext(bgCtx).Where(h.readDB.PaymentTransactions.OrderNo.Eq(orderNo)).First()
|
|
if tx == nil || tx.TransactionID == "" {
|
|
return
|
|
}
|
|
|
|
u, _ := h.readDB.Users.WithContext(bgCtx).Where(h.readDB.Users.ID.Eq(userID)).First()
|
|
payerOpenid := ""
|
|
if u != nil {
|
|
payerOpenid = u.Openid
|
|
}
|
|
|
|
itemsDesc := fmt.Sprintf("对对碰 %s 赏品: %s (自动开奖)", orderNo, rName)
|
|
if len(itemsDesc) > 120 {
|
|
itemsDesc = itemsDesc[:120]
|
|
}
|
|
|
|
c := configs.Get()
|
|
if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}, tx.TransactionID, orderNo, payerOpenid, itemsDesc); err != nil {
|
|
h.logger.Error("对对碰自动开奖: 虚拟发货失败", zap.Error(err))
|
|
} else {
|
|
h.logger.Info("对对碰自动开奖: 虚拟发货成功", zap.String("order_no", orderNo))
|
|
}
|
|
}(game.OrderID, order.OrderNo, game.UserID, rewardName)
|
|
|
|
// 6. 清理 Redis 会话
|
|
_ = h.redis.Del(ctx, activitysvc.MatchingGameKeyPrefix+gameID)
|
|
|
|
h.logger.Info("对对碰自动开奖: 结算完成",
|
|
zap.String("game_id", gameID),
|
|
zap.Int64("order_id", game.OrderID),
|
|
zap.Int64("total_pairs", serverSimulatedPairs))
|
|
}
|