package app import ( "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/util/remark" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/repository/mysql/model" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "strings" "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"` Image string `json:"image"` } 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 } rmk := remark.Parse(ord.Remark) iss := rmk.IssueID dc := rmk.Count // 3. 解析活动模式 mode := "scheduled" if iss > 0 { issue, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(iss)).First() if issue != nil { act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(issue.ActivityID)).First() if act != nil && act.DrawMode != "" { mode = act.DrawMode } } } // 4. 执行开奖补齐 (如果尚未全部完成) if ord.Status == 2 && mode == "instant" { // 直接调用统一开奖服务,它会处理所有缺失的抽奖序号 _ = h.activity.ProcessOrderLottery(ctx.RequestContext(), ord.ID) } // 5. 重新获取最新的开奖记录 (包含刚刚可能补齐的内容) logs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()). Where(h.readDB.ActivityDrawLogs.OrderID.Eq(ord.ID)). Order(h.readDB.ActivityDrawLogs.DrawIndex). Find() // 批量预加载所有奖品配置 rewardIDs := make([]int64, 0, len(logs)) for _, lg := range logs { if lg.RewardID > 0 { rewardIDs = append(rewardIDs, lg.RewardID) } } rewardMap := make(map[int64]*model.ActivityRewardSettings) if len(rewardIDs) > 0 { rwList, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.In(rewardIDs...)).Find() for _, rw := range rwList { rewardMap[rw.ID] = rw } } // 收集所有需要查询的产品ID(包括开奖记录和翻倍记录) productIDs := make([]int64, 0) productIDSet := make(map[int64]struct{}) for _, rw := range rewardMap { if rw.ProductID > 0 { if _, ok := productIDSet[rw.ProductID]; !ok { productIDSet[rw.ProductID] = struct{}{} productIDs = append(productIDs, rw.ProductID) } } } // 6. 查询道具卡翻倍的奖励(remark包含"倍数") doubledInvs, _ := h.readDB.UserInventory.WithContext(ctx.RequestContext()). Where(h.readDB.UserInventory.OrderID.Eq(ord.ID)). Find() var validDoubledInvs []*model.UserInventory for _, inv := range doubledInvs { if inv.Remark != "" && strings.Contains(inv.Remark, "(倍数)") { validDoubledInvs = append(validDoubledInvs, inv) if inv.ProductID > 0 { if _, ok := productIDSet[inv.ProductID]; !ok { productIDSet[inv.ProductID] = struct{}{} productIDs = append(productIDs, inv.ProductID) } } } } // 批量预加载所有产品信息 productMap := make(map[int64]*model.Products) if len(productIDs) > 0 { pList, _ := h.readDB.Products.WithContext(ctx.RequestContext()).Where(h.readDB.Products.ID.In(productIDs...)).Find() for _, p := range pList { productMap[p.ID] = p } } // 构建结果列表 completed := int64(len(logs)) items := make([]orderResultItem, 0, len(logs)+len(validDoubledInvs)) drawLogIDs := make([]int64, 0, len(logs)) // 处理普通奖励 for _, lg := range logs { drawLogIDs = append(drawLogIDs, lg.ID) image := "" name := "" if rw, ok := rewardMap[lg.RewardID]; ok { if p, ok := productMap[rw.ProductID]; ok { name = p.Name if p.ImagesJSON != "" { var imgs []string if json.Unmarshal([]byte(p.ImagesJSON), &imgs) == nil && len(imgs) > 0 { image = imgs[0] } } } } items = append(items, orderResultItem{ RewardID: lg.RewardID, RewardName: name, Level: lg.Level, DrawIndex: int32(lg.DrawIndex + 1), Image: image, }) } // 处理翻倍奖励 for _, inv := range validDoubledInvs { image := "" name := inv.Remark if p, ok := productMap[inv.ProductID]; ok { name = p.Name if p.ImagesJSON != "" { var imgs []string if json.Unmarshal([]byte(p.ImagesJSON), &imgs) == nil && len(imgs) > 0 { image = imgs[0] } } } items = append(items, orderResultItem{ RewardID: inv.RewardID, RewardName: name + "(翻倍)", Level: 0, DrawIndex: int32(len(logs) + 1), // 翻倍奖励排在最后 Image: image, }) } st := "pending" if ord.Status == 4 { st = "refunded" } else if ord.Status == 2 { if completed < dc { 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) } }