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 { // 一次补抽 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 { // Daily Seed removed to enforce CommitmentSeedMaster 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) // Daily Seed removed 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) } }