bindbox-game/internal/api/activity/lottery_result_order_app.go
邹方成 e2782a69d3 feat: 添加对对碰游戏功能与Redis支持
refactor: 重构抽奖逻辑以支持可验证凭据
feat(redis): 集成Redis客户端并添加配置支持
fix: 修复订单取消时的优惠券和库存处理逻辑
docs: 添加对对碰游戏前端对接指南和示例JSON
test: 添加对对碰游戏模拟测试和验证逻辑
2025-12-21 17:31:32 +08:00

335 lines
11 KiB
Go

package app
import (
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
strat "bindbox-game/internal/service/activity/strategy"
usersvc "bindbox-game/internal/service/user"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"time"
)
type orderResultQuery struct {
OrderNo string `form:"order_no"`
}
type orderResultItem struct {
RewardID int64 `json:"reward_id"`
RewardName string `json:"reward_name"`
Level int32 `json:"level"`
DrawIndex int32 `json:"draw_index"`
}
type orderResultResponse struct {
Status string `json:"status"`
DrawMode string `json:"draw_mode"`
Count int64 `json:"count"`
Completed int64 `json:"completed"`
Results []orderResultItem `json:"results"`
Receipt map[string]any `json:"receipt,omitempty"`
NextPollMs int `json:"nextPollMs"`
SeedHex string `json:"seed_hex,omitempty"`
NextDrawTime string `json:"next_draw_time,omitempty"`
}
// LotteryResultByOrder 抽奖订单结果查询
// @Summary 抽奖订单结果查询
// @Description 根据订单号查询抽奖结果与进度,返回结果明细与可验证凭证;需登录
// @Tags APP端.抽奖
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param order_no query string true "订单号"
// @Success 200 {object} orderResultResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/lottery/result [get]
func (h *handler) LotteryResultByOrder() core.HandlerFunc {
return func(ctx core.Context) {
req := new(orderResultQuery)
if err := ctx.ShouldBindQuery(req); err != nil || req.OrderNo == "" {
ctx.AbortWithError(core.Error(400, code.ParamBindError, validation.Error(err)))
return
}
userID := int64(ctx.SessionUserInfo().Id)
ord, err := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(req.OrderNo), h.readDB.Orders.UserID.Eq(userID)).First()
if err != nil || ord == nil {
ctx.AbortWithError(core.Error(400, code.ParamBindError, "order not found"))
return
}
iss := parseIssueIDFromRemark(ord.Remark)
dc := parseCountFromRemark(ord.Remark)
if dc <= 0 {
dc = 1
}
logs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(ord.ID)).Find()
completed := int64(len(logs))
items := make([]orderResultItem, 0, len(logs))
rewardNameCache := map[int64]string{}
drawLogIDs := make([]int64, 0, len(logs))
for i := 0; i < len(logs); i++ {
lg := logs[i]
drawLogIDs = append(drawLogIDs, lg.ID)
rid := lg.RewardID
name := rewardNameCache[rid]
if name == "" && rid > 0 {
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
if rw != nil {
name = rw.Name
rewardNameCache[rid] = name
}
}
items = append(items, orderResultItem{RewardID: rid, RewardName: name, Level: lg.Level, DrawIndex: int32(i + 1)})
}
mode := "scheduled"
if iss > 0 {
issue, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(iss)).First()
aid := int64(0)
if issue != nil {
aid = issue.ActivityID
}
if aid > 0 {
act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(aid)).First()
if act != nil && act.DrawMode != "" {
mode = act.DrawMode
}
}
}
st := "pending"
if ord.Status == 4 {
st = "refunded"
} else if ord.Status == 2 {
if completed < dc {
// 即时模式支持在结果查询中补开奖
if mode == "instant" && iss > 0 {
// 执行一次开奖
// 读取活动与策略
act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(func() int64 {
if iss > 0 {
issue, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(iss)).First()
if issue != nil {
return issue.ActivityID
}
}
return 0
}())).First()
if act != nil {
// 一次补抽
rid := int64(0)
var e2 error
if act.PlayType == "ichiban" {
slot := parseSlotFromRemark(ord.Remark)
if slot >= 0 {
var cnt int64
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index=?", iss, slot).Scan(&cnt).Error
if cnt > 0 {
st = "slot_unavailable"
} else if err := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", iss, slot, userID, ord.ID).Error; err == nil {
var proof map[string]any
rid, proof, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), act.ID, iss, slot)
if e2 == nil && rid > 0 {
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
if rw != nil {
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
drawLog := &model.ActivityDrawLogs{UserID: userID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, iss, userID, proof)
completed++
items = append(items, orderResultItem{RewardID: rid, RewardName: rw.Name, Level: rw.Level, DrawIndex: int32(completed)})
}
}
}
}
} else {
sel := strat.NewDefault(h.readDB, h.writeDB)
var proof map[string]any
rid, proof, e2 = sel.SelectItem(ctx.RequestContext(), act.ID, iss, userID)
if e2 == nil && rid > 0 {
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
if rw != nil {
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &act.ID, RewardID: &rid, Remark: rw.Name})
drawLog := &model.ActivityDrawLogs{UserID: userID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, iss, userID, proof)
completed++
items = append(items, orderResultItem{RewardID: rid, RewardName: rw.Name, Level: rw.Level, DrawIndex: int32(completed)})
}
}
}
}
}
if st == "pending" {
st = "paid_waiting"
}
} else {
st = "settled"
}
}
var receipt map[string]any
var randomReceipts []map[string]any
if len(drawLogIDs) > 0 {
if recs, _ := h.readDB.ActivityDrawReceipts.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawReceipts.DrawLogID.In(drawLogIDs...)).Find(); len(recs) > 0 {
recMap := make(map[int64]*model.ActivityDrawReceipts, len(recs))
for _, r := range recs {
recMap[r.DrawLogID] = r
}
randomReceipts = make([]map[string]any, 0, len(recs))
for i := 0; i < len(logs); i++ {
lg := logs[i]
if r, ok := recMap[lg.ID]; ok {
randomReceipts = append(randomReceipts, map[string]any{
"draw_log_id": lg.ID,
"reward_id": lg.RewardID,
"draw_index": i + 1,
"algo_version": r.AlgoVersion,
"round_id": r.RoundID,
"draw_id": r.DrawID,
"client_id": r.ClientID,
"timestamp": r.Timestamp,
"server_seed_hash": r.ServerSeedHash,
"server_sub_seed": r.ServerSubSeed,
"client_seed": r.ClientSeed,
"nonce": r.Nonce,
"items_root": r.ItemsRoot,
"weights_total": r.WeightsTotal,
"selected_index": r.SelectedIndex,
"rand_proof": r.RandProof,
"signature": r.Signature,
"items_snapshot": r.ItemsSnapshot,
})
}
}
}
}
aid := int64(0)
if iss > 0 {
issue, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(iss)).First()
if issue != nil {
aid = issue.ActivityID
}
}
if aid > 0 {
act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(aid)).First()
if act != nil && len(act.CommitmentSeedMaster) > 0 {
ts := time.Now().UnixMilli()
nonce := ts
mac := hmac.New(sha256.New, act.CommitmentSeedMaster)
mac.Write([]byte(h.joinSigPayload(userID, iss, ts, nonce)))
sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
receipt = map[string]any{
"issue_id": iss,
"seed_version": act.CommitmentStateVersion,
"timestamp": ts,
"nonce": nonce,
"signature": sig,
"algorithm": "HMAC-SHA256",
"inputs": map[string]any{"user_id": userID, "issue_id": iss, "order_id": ord.ID, "timestamp": ts, "nonce": nonce},
}
}
}
if len(randomReceipts) > 0 {
if receipt == nil {
receipt = map[string]any{}
}
receipt["draw_receipts"] = randomReceipts
}
// 构建响应,包含随机种子和下次开奖时间
var seedHex string
var nextDrawTime string
if aid > 0 {
act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(aid)).First()
if act != nil {
// 问题2: 返回随机种子(十六进制格式)
if len(act.CommitmentSeedMaster) > 0 {
seedHex = hex.EncodeToString(act.CommitmentSeedMaster)
}
// 问题3: 定时抽奖返回下一次开奖时间
if mode == "scheduled" && !act.ScheduledTime.IsZero() {
nextDrawTime = act.ScheduledTime.Format(time.RFC3339)
}
}
}
rsp := &orderResultResponse{Status: st, DrawMode: mode, Count: dc, Completed: completed, Results: items, Receipt: receipt, NextPollMs: 2000, SeedHex: seedHex, NextDrawTime: nextDrawTime}
ctx.Payload(rsp)
}
}
func parseIssueIDFromRemark(remark string) int64 {
if remark == "" {
return 0
}
// Try "issue:" or "matching_game:issue:"
// Split by |
segs := make([]string, 0)
last := 0
for i := 0; i < len(remark); i++ {
if remark[i] == '|' {
segs = append(segs, remark[last:i])
last = i + 1
}
}
if last < len(remark) {
segs = append(segs, remark[last:])
}
for _, seg := range segs {
// handle 'issue:123'
if len(seg) > 6 && seg[:6] == "issue:" {
var n int64
for j := 6; j < len(seg); j++ {
c := seg[j]
if c < '0' || c > '9' {
break
}
n = n*10 + int64(c-'0')
}
return n
}
// handle 'matching_game:issue:123'
if len(seg) > 20 && seg[:20] == "matching_game:issue:" {
var n int64
for j := 20; j < len(seg); j++ {
c := seg[j]
if c < '0' || c > '9' {
break
}
n = n*10 + int64(c-'0')
}
return n
}
}
return 0
}
func parseCountFromRemark(remark string) int64 {
if remark == "" {
return 1
}
p := 0
for i := 0; i < len(remark); i++ {
if remark[i] == '|' {
seg := remark[p:i]
if len(seg) > 6 && seg[:6] == "count:" {
var n int64
for j := 6; j < len(seg); j++ {
c := seg[j]
if c < '0' || c > '9' {
break
}
n = n*10 + int64(c-'0')
}
if n <= 0 {
return 1
}
return n
}
p = i + 1
}
}
return 1
}