bindbox-game/internal/api/activity/lottery_result_order_app.go
win 8d1eef2f7f fix(channel): 修复渠道统计GMV重复计数和商城直购误计入
1. 排除商城直购(source_type=1):GMV和成本过滤条件从IN(1,2,3,4)改为IN(2,3,4)
2. 排除次卡免费使用订单(actual_amount=0):避免购买次卡和使用次卡双重计入GMV
   - source_type=4 一番赏使用次卡:1578单 44032元重复
   - source_type=3 对对碰使用次卡:422单 7042元重复
   - 合计去除51074元虚增GMV(29.1%)
3. 成本过滤条件同步修正:source_type IN(2,3,4),total_amount>0

修正后:GMV从175600降至124527元,毛利率从37.4%回到真实的11.8%
2026-03-16 21:41:39 +08:00

305 lines
9.6 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package app
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"strings"
"time"
"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"
)
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)
}
}