bindbox-game/internal/api/activity/matching_game_helper.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))
}