refactor(utils): 修复密码哈希比较逻辑错误 feat(user): 新增按状态筛选优惠券接口 docs: 添加虚拟发货与任务中心相关文档 fix(wechat): 修正Code2Session上下文传递问题 test: 补充订单折扣与积分转换测试用例 build: 更新配置文件与构建脚本 style: 清理多余的空行与注释
307 lines
11 KiB
Go
307 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 {
|
|
rid, _, 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)
|
|
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1})
|
|
completed++
|
|
items = append(items, orderResultItem{RewardID: rid, RewardName: rw.Name, Level: rw.Level, DrawIndex: int32(completed)})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
sel := strat.NewDefault(h.readDB, h.writeDB)
|
|
rid, _, 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})
|
|
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1})
|
|
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
|
|
}
|
|
p := 0
|
|
for i := 0; i < len(remark); i++ {
|
|
if remark[i] == '|' {
|
|
seg := remark[p:i]
|
|
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
|
|
}
|
|
p = i + 1
|
|
}
|
|
}
|
|
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
|
|
}
|