邹方成 45815bfb7d chore: 清理无用文件与优化代码结构
refactor(utils): 修复密码哈希比较逻辑错误
feat(user): 新增按状态筛选优惠券接口
docs: 添加虚拟发货与任务中心相关文档
fix(wechat): 修正Code2Session上下文传递问题
test: 补充订单折扣与积分转换测试用例
build: 更新配置文件与构建脚本
style: 清理多余的空行与注释
2025-12-18 17:35:55 +08:00

1180 lines
53 KiB
Go
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 (
"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"
"fmt"
"math/rand"
"net/http"
"time"
)
type joinLotteryRequest struct {
ActivityID int64 `json:"activity_id"`
IssueID int64 `json:"issue_id"`
Count int64 `json:"count"`
Channel string `json:"channel"`
SlotIndex []int64 `json:"slot_index"`
CouponID *int64 `json:"coupon_id"`
ItemCardID *int64 `json:"item_card_id"`
UsePoints *int64 `json:"use_points"`
}
type joinLotteryResponse struct {
JoinID string `json:"join_id"`
OrderNo string `json:"order_no"`
Queued bool `json:"queued"`
DrawMode string `json:"draw_mode"`
RewardID int64 `json:"reward_id,omitempty"`
RewardName string `json:"reward_name,omitempty"`
}
// JoinLottery 用户参与抽奖
// @Summary 用户参与抽奖
// @Description 提交活动ID与期ID创建参与记录与订单支持积分抵扣返回参与ID、订单号、抽奖模式及是否进入队列
// @Tags APP端.抽奖
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param RequestBody body joinLotteryRequest true "请求参数"
// @Success 200 {object} joinLotteryResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/lottery/join [post]
func (h *handler) JoinLottery() core.HandlerFunc {
return func(ctx core.Context) {
req := new(joinLotteryRequest)
rsp := new(joinLotteryResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID := int64(ctx.SessionUserInfo().Id)
activity, err := h.activity.GetActivity(ctx.RequestContext(), req.ActivityID)
if err != nil || activity == nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170001, "activity not found"))
return
}
if !activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170009, "本活动不支持优惠券"))
return
}
if !activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170010, "本活动不支持道具卡"))
return
}
cfgMode := "scheduled"
if activity.DrawMode != "" {
cfgMode = activity.DrawMode
}
fmt.Printf("[抽奖下单] 用户=%d 活动ID=%d 期ID=%d 次数=%d 渠道=%s 优惠券ID=%v 道具卡ID=%v\n", userID, req.ActivityID, req.IssueID, req.Count, req.Channel, req.CouponID, req.ItemCardID)
fmt.Printf("[抽奖下单] 活动票价(分)=%d 允许优惠券=%t 允许道具卡=%t 抽奖模式=%s 玩法=%s\n", activity.PriceDraw, activity.AllowCoupons, activity.AllowItemCards, cfgMode, activity.PlayType)
// 定时一番赏开奖前20秒禁止下单防止订单抖动
if activity.PlayType == "ichiban" && cfgMode == "scheduled" && !activity.ScheduledTime.IsZero() {
now := time.Now()
cutoff := activity.ScheduledTime.Add(-20 * time.Second)
if now.After(cutoff) && now.Before(activity.ScheduledTime.Add(30*time.Second)) {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170012, "距离开奖时间不足20秒暂停下单"))
return
}
}
if activity.PlayType == "ichiban" {
if e := h.validateIchibanSlots(ctx, req); e != nil {
ctx.AbortWithError(e)
return
}
}
joinID := h.randomID("J")
orderNo := h.randomID("O")
c := req.Count
if c <= 0 {
c = 1
}
total := activity.PriceDraw * c
order := h.orderModel(userID, orderNo, total, req.ActivityID, req.IssueID, c)
if len(req.SlotIndex) > 0 {
order.Remark = fmt.Sprintf("lottery:activity:%d|issue:%d|count:%d|slots:%s", req.ActivityID, req.IssueID, c, buildSlotsRemarkWithScalarCount(req.SlotIndex))
}
if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
if order.Remark == "" {
order.Remark = fmt.Sprintf("lottery:activity:%d|issue:%d|count:%d", req.ActivityID, req.IssueID, c)
}
order.Remark = order.Remark + fmt.Sprintf("|itemcard:%d", *req.ItemCardID)
}
order.PointsAmount = 0
order.PointsLedgerID = 0
order.ActualAmount = order.TotalAmount
fmt.Printf("[抽奖下单] 订单总额(分)=%d 初始实付(分)=%d 备注=%s\n", order.TotalAmount, order.ActualAmount, order.Remark)
applied := int64(0)
if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
fmt.Printf("[抽奖下单] 尝试优惠券 用户券ID=%d 应用前实付(分)=%d 累计优惠(分)=%d\n", *req.CouponID, order.ActualAmount, order.DiscountAmount)
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
fmt.Printf("[抽奖下单] 优惠后 实付(分)=%d 累计优惠(分)=%d 备注=%s\n", order.ActualAmount, order.DiscountAmount, order.Remark)
}
if req.UsePoints != nil && *req.UsePoints > 0 {
bal, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
usePts := *req.UsePoints
if bal > 0 && usePts > bal {
usePts = bal
}
ratePtsPerCent, _ := h.user.CentsToPoints(ctx.RequestContext(), 1)
if ratePtsPerCent <= 0 {
ratePtsPerCent = 1
}
deductCents := usePts / ratePtsPerCent
if deductCents > order.ActualAmount {
deductCents = order.ActualAmount
}
if deductCents > 0 {
needPts := deductCents * ratePtsPerCent
ledgerID, errConsume := h.user.ConsumePointsFor(ctx.RequestContext(), userID, needPts, "orders", orderNo, "order points consume", "consume_order")
if errConsume != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170020, errConsume.Error()))
return
}
order.PointsAmount = deductCents
order.PointsLedgerID = ledgerID
order.ActualAmount = order.ActualAmount - deductCents
}
}
err = h.writeDB.Orders.WithContext(ctx.RequestContext()).Omit(h.writeDB.Orders.PaidAt, h.writeDB.Orders.CancelledAt).Create(order)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 170002, err.Error()))
return
}
// 写结构化优惠券使用明细(兼容保留 remark
if applied > 0 && req.CouponID != nil && *req.CouponID > 0 {
_ = h.user.RecordOrderCouponUsage(ctx.RequestContext(), order.ID, *req.CouponID, applied)
}
// 优惠券扣减与核销在支付回调中执行(避免未支付时扣减)
rsp.JoinID = joinID
rsp.OrderNo = orderNo
rsp.DrawMode = cfgMode
fmt.Printf("[抽奖下单] 汇总 订单号=%s 总额(分)=%d 优惠(分)=%d 实付(分)=%d 队列=true 模式=%s\n", orderNo, order.TotalAmount, order.DiscountAmount, order.ActualAmount, cfgMode)
if order.ActualAmount == 0 {
now := time.Now()
_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{h.writeDB.Orders.Status.ColumnName().String(): 2, h.writeDB.Orders.PaidAt.ColumnName().String(): now})
// 0元订单统一在“已支付”后扣券解析 remark 中优惠券使用片段并扣减或核销)
{
ord, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(orderNo)).First()
if ord != nil {
parts := func(s string) []string {
p := make([]string, 0, 8)
b := 0
for i := 0; i < len(s); i++ {
if s[i] == '|' {
if i > b {
p = append(p, s[b:i])
}
b = i + 1
}
}
if b < len(s) {
p = append(p, s[b:])
}
return p
}(ord.Remark)
for _, seg := range parts {
if len(seg) > 2 && seg[:2] == "c:" {
// 解析 id 与 amount
j := 2
var cid int64
for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' {
cid = cid*10 + int64(seg[j]-'0')
j++
}
var applied int64
if j < len(seg) && seg[j] == ':' {
j++
for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' {
applied = applied*10 + int64(seg[j]-'0')
j++
}
}
uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(cid), h.readDB.UserCoupons.UserID.Eq(userID)).First()
if uc != nil {
if uc.Status == 2 && uc.UsedOrderID == ord.ID {
continue
}
sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).First()
if sc != nil {
if sc.DiscountType == 1 {
var bal int64
_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", uc.ID).Scan(&bal).Error
nb := bal - applied
if nb < 0 {
nb = 0
}
if nb == 0 {
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(uc.ID)).Updates(map[string]any{"balance_amount": nb, h.readDB.UserCoupons.Status.ColumnName().String(): 2, h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): ord.ID, h.readDB.UserCoupons.UsedAt.ColumnName().String(): now})
} else {
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(uc.ID)).Updates(map[string]any{"balance_amount": nb, h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): ord.ID, h.readDB.UserCoupons.UsedAt.ColumnName().String(): now})
}
} else {
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(uc.ID)).Updates(map[string]any{h.readDB.UserCoupons.Status.ColumnName().String(): 2, h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): ord.ID, h.readDB.UserCoupons.UsedAt.ColumnName().String(): now})
}
}
}
}
}
}
}
if cfgMode == "instant" {
ord, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(orderNo)).First()
if ord != nil {
// 解析次数
slotsIdx, slotsCnt := parseSlotsCountsFromRemark(ord.Remark)
dc := func() int64 {
if len(slotsIdx) > 0 && len(slotsIdx) == len(slotsCnt) {
var s int64
for i := range slotsCnt {
if slotsCnt[i] > 0 {
s += slotsCnt[i]
}
}
if s > 0 {
return s
}
}
remark := ord.Remark
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
}()
sel := strat.NewDefault(h.readDB, h.writeDB)
logs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(ord.ID)).Find()
done := int64(len(logs))
rem := make([]int64, len(slotsCnt))
copy(rem, slotsCnt)
cur := 0
for i := done; i < dc; i++ {
rid := int64(0)
var e2 error
if activity.PlayType == "ichiban" {
slot := func() int64 {
if len(slotsIdx) > 0 && len(slotsIdx) == len(rem) {
for cur < len(rem) && rem[cur] == 0 {
cur++
}
if cur >= len(rem) {
return -1
}
rem[cur]--
return slotsIdx[cur] - 1
}
return 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=?", req.IssueID, slot).Scan(&cnt).Error
if cnt > 0 {
break
}
e := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", req.IssueID, slot, userID, ord.ID).Error
if e != nil {
break
}
rid, _, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), req.ActivityID, req.IssueID, slot)
} else {
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), req.ActivityID, req.IssueID, userID)
}
} else {
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), req.ActivityID, req.IssueID, userID)
}
if e2 != nil || rid <= 0 {
break
}
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
if rw == nil {
break
}
if activity.PlayType == "ichiban" {
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
} else {
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid, Remark: rw.Name})
}
// 创建抽奖日志获取ID用于道具卡核销
log := &model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
// 道具卡效果(奖励倍数/概率提升的简单实现:奖励倍数=额外发同奖品;概率提升=尝试升级到更高等级)
fmt.Printf("[道具卡-JoinLottery] 开始检查 活动允许道具卡=%t 请求道具卡ID=%v\n", activity.AllowItemCards, req.ItemCardID)
if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
fmt.Printf("[道具卡-JoinLottery] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *req.ItemCardID)
uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*req.ItemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).First()
if uic != nil {
fmt.Printf("[道具卡-JoinLottery] 找到用户道具卡 ID=%d CardID=%d Status=%d ValidStart=%s ValidEnd=%s\n", uic.ID, uic.CardID, uic.Status, uic.ValidStart.Format(time.RFC3339), uic.ValidEnd.Format(time.RFC3339))
ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
now := time.Now()
if ic != nil {
fmt.Printf("[道具卡-JoinLottery] 找到系统道具卡 ID=%d Name=%s EffectType=%d RewardMultiplierX1000=%d ScopeType=%d ActivityID=%d IssueID=%d Status=%d\n", ic.ID, ic.Name, ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, ic.ActivityID, ic.IssueID, ic.Status)
fmt.Printf("[道具卡-JoinLottery] 时间检查 当前时间=%s ValidStart.After(now)=%t ValidEnd.Before(now)=%t\n", now.Format(time.RFC3339), uic.ValidStart.After(now), uic.ValidEnd.Before(now))
if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == req.ActivityID) || (ic.ScopeType == 4 && ic.IssueID == req.IssueID)
fmt.Printf("[道具卡-JoinLottery] 范围检查 ScopeType=%d 请求ActivityID=%d 请求IssueID=%d scopeOK=%t\n", ic.ScopeType, req.ActivityID, req.IssueID, scopeOK)
if scopeOK {
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 { // ×2及以上额外发一次相同奖品
fmt.Printf("[道具卡-JoinLottery] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s PlayType=%s\n", ic.RewardMultiplierX1000, rid, rw.Name, activity.PlayType)
if activity.PlayType == "ichiban" {
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
} else {
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
}
fmt.Printf("[道具卡-JoinLottery] ✅ 双倍奖励发放完成\n")
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 { // 概率提升:尝试升级到更高等级的可用奖品
fmt.Printf("[道具卡-JoinLottery] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(req.IssueID)).Find()
var better *model.ActivityRewardSettings
for _, r := range uprw {
if r.Level < rw.Level && r.Quantity != 0 {
if better == nil || r.Level < better.Level {
better = r
}
}
}
if better != nil {
// 以boost率决定升级
if rand.Int31n(1000) < ic.BoostRateX1000 {
rid2 := better.ID
fmt.Printf("[道具卡-JoinLottery] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
if activity.PlayType == "ichiban" {
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid2)
} else {
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: better.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
}
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
} else {
fmt.Printf("[道具卡-JoinLottery] 概率提升未触发\n")
}
} else {
fmt.Printf("[道具卡-JoinLottery] 没有找到更好的奖品可升级\n")
}
} else {
fmt.Printf("[道具卡-JoinLottery] ⚠️ 道具卡效果类型不匹配或倍数不足 EffectType=%d RewardMultiplierX1000=%d\n", ic.EffectType, ic.RewardMultiplierX1000)
}
// 核销道具卡
fmt.Printf("[道具卡-JoinLottery] 核销道具卡 用户道具卡ID=%d\n", *req.ItemCardID)
_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*req.ItemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).Updates(map[string]any{
h.readDB.UserItemCards.Status.ColumnName().String(): 2,
h.readDB.UserItemCards.UsedDrawLogID.ColumnName().String(): log.ID,
h.readDB.UserItemCards.UsedActivityID.ColumnName().String(): req.ActivityID,
h.readDB.UserItemCards.UsedIssueID.ColumnName().String(): req.IssueID,
h.readDB.UserItemCards.UsedAt.ColumnName().String(): time.Now(),
})
} else {
fmt.Printf("[道具卡-JoinLottery] ❌ 范围检查失败\n")
}
} else {
fmt.Printf("[道具卡-JoinLottery] ❌ 时间检查失败\n")
}
} else {
fmt.Printf("[道具卡-JoinLottery] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
}
} else {
fmt.Printf("[道具卡-JoinLottery] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *req.ItemCardID)
}
} else {
fmt.Printf("[道具卡-JoinLottery] 跳过道具卡检查\n")
}
}
}
}
rsp.Queued = true
} else {
rsp.Queued = true
}
ctx.Payload(rsp)
}
}
type resultQueryRequest struct {
JoinID string `form:"join_id"`
OrderID int64 `form:"order_id"`
}
type resultResponse struct {
Result map[string]any `json:"result"`
Receipt map[string]any `json:"receipt"`
}
// GetLotteryResult 抽奖结果查询
// @Summary 抽奖结果查询
// @Description 根据参与ID与期ID查询抽奖结果即时模式会立即开奖并发奖同时返回服务端签名凭证用于校验
// @Tags APP端.抽奖
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param join_id query string true "参与ID"
// @Param order_id query int false "订单ID用于区分同一用户多次参与"
// @Param issue_id query int false "期ID"
// @Success 200 {object} resultResponse
// @Failure 400 {object} code.Failure
func (h *handler) GetLotteryResult() core.HandlerFunc {
return func(ctx core.Context) {
req := new(resultQueryRequest)
rsp := new(resultResponse)
if err := ctx.ShouldBindQuery(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
issueIDStr := ctx.RequestInputParams().Get("issue_id")
var issueID int64
if issueIDStr != "" {
for i := 0; i < len(issueIDStr); i++ {
c := issueIDStr[i]
if c < '0' || c > '9' {
issueID = 0
break
}
issueID = issueID*10 + int64(c-'0')
}
}
var orderID int64
var ord *model.Orders
if req.OrderID > 0 {
orderID = req.OrderID
ord, _ = h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(orderID)).First()
}
userID := int64(ctx.SessionUserInfo().Id)
issue, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(issueID)).First()
activityID := func() int64 {
if issue != nil {
return issue.ActivityID
}
return 0
}()
actCommit, err := func() (*model.Activities, error) {
if activityID <= 0 {
return nil, nil
}
return h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(activityID)).First()
}()
if err != nil || actCommit == nil || len(actCommit.CommitmentSeedMaster) == 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170003, "commitment not found"))
return
}
cfgMode := "scheduled"
if act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(activityID)).First(); act != nil && act.DrawMode != "" {
cfgMode = act.DrawMode
}
// 结果优先返回历史抽奖日志
var existed *model.ActivityDrawLogs
if orderID > 0 {
existed, _ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(orderID)).First()
} else {
existed, _ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.UserID.Eq(userID), h.readDB.ActivityDrawLogs.IssueID.Eq(issueID)).First()
}
if existed != nil && existed.RewardID > 0 {
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(existed.RewardID)).First()
rsp.Result = map[string]any{"reward_id": existed.RewardID, "reward_name": func() string {
if rw != nil {
return rw.Name
}
return ""
}()}
} else if cfgMode == "instant" {
// 即时开奖必须绑定订单且校验归属与支付状态
if orderID == 0 || ord == nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170005, "order required for instant draw"))
return
}
if ord.UserID != userID {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170005, "order not owned by user"))
return
}
if ord.Status != 2 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170006, "order not paid"))
return
}
if actCommit.PlayType == "ichiban" {
slot := parseSlotFromRemark(ord.Remark)
if slot >= 0 {
if err := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", issueID, slot, userID, orderID).Error; err == nil {
rid, _, e2 := strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), activityID, issueID, 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: issueID, OrderID: orderID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1})
rsp.Result = map[string]any{"reward_id": rid, "reward_name": rw.Name}
}
}
}
}
} else {
sel := strat.NewDefault(h.readDB, h.writeDB)
rid, _, e2 := sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
if e2 == nil && rid > 0 {
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
_ = sel.GrantReward(ctx.RequestContext(), userID, rid)
log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid, IsWinner: 1, Level: func() int32 {
if rw != nil {
return rw.Level
}
return 1
}(), CurrentLevel: 1}
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
icID := parseItemCardIDFromRemark(ord.Remark)
fmt.Printf("[道具卡-GetLotteryResult] 从订单备注解析道具卡ID icID=%d 订单备注=%s\n", icID, ord.Remark)
if icID > 0 {
fmt.Printf("[道具卡-GetLotteryResult] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, icID)
uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(icID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).First()
if uic != nil {
fmt.Printf("[道具卡-GetLotteryResult] 找到用户道具卡 ID=%d CardID=%d Status=%d ValidStart=%s ValidEnd=%s\n", uic.ID, uic.CardID, uic.Status, uic.ValidStart.Format(time.RFC3339), uic.ValidEnd.Format(time.RFC3339))
ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
now := time.Now()
if ic != nil {
fmt.Printf("[道具卡-GetLotteryResult] 找到系统道具卡 ID=%d Name=%s EffectType=%d RewardMultiplierX1000=%d ScopeType=%d ActivityID=%d IssueID=%d Status=%d\n", ic.ID, ic.Name, ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, ic.ActivityID, ic.IssueID, ic.Status)
fmt.Printf("[道具卡-GetLotteryResult] 时间检查 当前时间=%s ValidStart.After(now)=%t ValidEnd.Before(now)=%t\n", now.Format(time.RFC3339), uic.ValidStart.After(now), uic.ValidEnd.Before(now))
if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == activityID) || (ic.ScopeType == 4 && ic.IssueID == issueID)
fmt.Printf("[道具卡-GetLotteryResult] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, activityID, issueID, scopeOK)
if scopeOK {
eff := &model.ActivityDrawEffects{
DrawLogID: log.ID,
UserID: userID,
UserItemCardID: uic.ID,
SystemItemCardID: ic.ID,
Applied: 1,
CardType: ic.CardType,
EffectType: ic.EffectType,
RewardMultiplierX1000: ic.RewardMultiplierX1000,
ProbabilityDeltaX1000: ic.BoostRateX1000,
ScopeType: ic.ScopeType,
ActivityCategoryID: actCommit.ActivityCategoryID,
ActivityID: activityID,
IssueID: issueID,
}
_ = h.writeDB.ActivityDrawEffects.WithContext(ctx.RequestContext()).Create(eff)
fmt.Printf("[道具卡-GetLotteryResult] 创建道具卡效果记录 EffectID=%d\n", eff.ID)
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
fmt.Printf("[道具卡-GetLotteryResult] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s\n", ic.RewardMultiplierX1000, rid, rw.Name)
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: orderID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
fmt.Printf("[道具卡-GetLotteryResult] ✅ 双倍奖励发放完成\n")
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
fmt.Printf("[道具卡-GetLotteryResult] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
var better *model.ActivityRewardSettings
for _, r := range uprw {
if r.Level < rw.Level && r.Quantity != 0 {
if better == nil || r.Level < better.Level {
better = r
}
}
}
if better != nil && rand.Int31n(1000) < ic.BoostRateX1000 {
rid2 := better.ID
fmt.Printf("[道具卡-GetLotteryResult] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: orderID, ProductID: better.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
} else {
fmt.Printf("[道具卡-GetLotteryResult] 概率提升未触发\n")
}
} else {
fmt.Printf("[道具卡-GetLotteryResult] ⚠️ 道具卡效果类型不匹配或倍数不足 EffectType=%d RewardMultiplierX1000=%d\n", ic.EffectType, ic.RewardMultiplierX1000)
}
fmt.Printf("[道具卡-GetLotteryResult] 核销道具卡 用户道具卡ID=%d\n", icID)
_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(icID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).Updates(map[string]any{h.readDB.UserItemCards.Status.ColumnName().String(): 2, h.readDB.UserItemCards.UsedDrawLogID.ColumnName().String(): log.ID, h.readDB.UserItemCards.UsedActivityID.ColumnName().String(): activityID, h.readDB.UserItemCards.UsedIssueID.ColumnName().String(): issueID, h.readDB.UserItemCards.UsedAt.ColumnName().String(): time.Now()})
} else {
fmt.Printf("[道具卡-GetLotteryResult] ❌ 范围检查失败\n")
}
} else {
fmt.Printf("[道具卡-GetLotteryResult] ❌ 时间检查失败\n")
}
} else {
fmt.Printf("[道具卡-GetLotteryResult] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
}
} else {
fmt.Printf("[道具卡-GetLotteryResult] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, icID)
}
} else {
fmt.Printf("[道具卡-GetLotteryResult] 订单备注中没有道具卡ID\n")
}
rsp.Result = map[string]any{"reward_id": rid, "reward_name": func() string {
if rw != nil {
return rw.Name
}
return ""
}()}
}
}
}
ts := time.Now().UnixMilli()
nonce := rand.Int63()
mac := hmac.New(sha256.New, actCommit.CommitmentSeedMaster)
mac.Write([]byte(h.joinSigPayload(userID, issueID, ts, nonce)))
sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
if rsp.Result == nil {
rsp.Result = map[string]any{}
}
rsp.Receipt = map[string]any{
"issue_id": issueID,
"seed_version": actCommit.CommitmentStateVersion,
"timestamp": ts,
"nonce": nonce,
"signature": sig,
"algorithm": "HMAC-SHA256",
"inputs": map[string]any{"user_id": userID, "issue_id": issueID, "order_id": orderID, "timestamp": ts, "nonce": nonce},
}
ctx.Payload(rsp)
}
}
func (h *handler) randomID(prefix string) string {
now := time.Now()
return prefix + now.Format("20060102150405")
}
func (h *handler) orderModel(userID int64, orderNo string, amount int64, activityID int64, issueID int64, count int64) *model.Orders {
return &model.Orders{UserID: userID, OrderNo: orderNo, SourceType: 2, TotalAmount: amount, DiscountAmount: 0, PointsAmount: 0, ActualAmount: amount, Status: 1, IsConsumed: 0, Remark: fmt.Sprintf("lottery:activity:%d|issue:%d|count:%d", activityID, issueID, count)}
}
func parseSlotFromRemark(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) > 5 && seg[:5] == "slot:" {
var n int64
for j := 5; j < len(seg); j++ {
c := seg[j]
if c < '0' || c > '9' {
break
}
n = n*10 + int64(c-'0')
}
return n
}
p = i + 1
}
}
if p < len(remark) {
seg := remark[p:]
if len(seg) > 5 && seg[:5] == "slot:" {
var n int64
for j := 5; j < len(seg); j++ {
c := seg[j]
if c < '0' || c > '9' {
break
}
n = n*10 + int64(c-'0')
}
return n
}
}
return -1
}
func parseItemCardIDFromRemark(remark string) int64 {
// remark segments separated by '|', find segment starting with "itemcard:"
p := 0
for i := 0; i < len(remark); i++ {
if remark[i] == '|' {
seg := remark[p:i]
if len(seg) > 9 && seg[:9] == "itemcard:" {
var n int64
for j := 9; j < len(seg); j++ {
c := seg[j]
if c < '0' || c > '9' {
break
}
n = n*10 + int64(c-'0')
}
if n > 0 {
return n
}
}
p = i + 1
}
}
return 0
}
func (h *handler) joinSigPayload(userID int64, issueID int64, ts int64, nonce int64) string {
return fmt.Sprintf("%d|%d|%d|%d", userID, issueID, ts, nonce)
}
func buildSlotsRemarkWithScalarCount(slots []int64) string {
s := ""
for i := range slots {
if i > 0 {
s += ","
}
s += fmt.Sprintf("%d:%d", slots[i]-1, 1)
}
return s
}
func parseSlotsCountsFromRemark(remark string) ([]int64, []int64) {
if remark == "" {
return nil, nil
}
p := 0
for i := 0; i < len(remark); i++ {
if remark[i] == '|' {
seg := remark[p:i]
if len(seg) > 6 && seg[:6] == "slots:" {
pairs := seg[6:]
idxs := make([]int64, 0)
cnts := make([]int64, 0)
start := 0
for start <= len(pairs) {
end := start
for end < len(pairs) && pairs[end] != ',' {
end++
}
if end > start {
a := pairs[start:end]
// a format: num:num
x, y := int64(0), int64(0)
j := 0
for j < len(a) && a[j] >= '0' && a[j] <= '9' {
x = x*10 + int64(a[j]-'0')
j++
}
if j < len(a) && a[j] == ':' {
j++
for j < len(a) && a[j] >= '0' && a[j] <= '9' {
y = y*10 + int64(a[j]-'0')
j++
}
}
if y > 0 {
idxs = append(idxs, x+1)
cnts = append(cnts, y)
}
}
start = end + 1
}
return idxs, cnts
}
p = i + 1
}
}
return nil, nil
}
// validateIchibanSlots 一番赏格位校验
// 功能:校验请求中的格位选择是否有效(数量匹配、范围合法、未被占用)
// 参数:
// - ctx请求上下文
// - reqJoinLottery 请求体,使用其中的 issue_id、count、slot_index
//
// 返回core.BusinessError 用于直接传递给 AbortWithError合法时返回 nil
func (h *handler) validateIchibanSlots(ctx core.Context, req *joinLotteryRequest) core.BusinessError {
var totalSlots int64
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(original_qty),0) FROM activity_reward_settings WHERE issue_id=?", req.IssueID).Scan(&totalSlots).Error
if totalSlots <= 0 {
return core.Error(http.StatusBadRequest, 170008, "no slots")
}
if len(req.SlotIndex) > 0 {
if req.Count <= 0 || req.Count != int64(len(req.SlotIndex)) {
return core.Error(http.StatusBadRequest, code.ParamBindError, "参数错误")
}
seen := make(map[int64]struct{})
for i := range req.SlotIndex {
si := req.SlotIndex[i]
if _, ok := seen[si]; ok {
return core.Error(http.StatusBadRequest, 170011, "duplicate slots not allowed")
}
seen[si] = struct{}{}
if si < 1 || si > totalSlots {
return core.Error(http.StatusBadRequest, 170008, "slot out of range")
}
var cnt int64
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index=?", req.IssueID, si-1).Scan(&cnt).Error
if cnt > 0 {
return core.Error(http.StatusBadRequest, 170007, "位置已被占用")
}
}
}
return nil
}
// applyCouponWithCap 优惠券抵扣含50%封顶与金额券部分使用)
// 功能在订单上应用一张用户券实施总价50%封顶;金额券支持“部分使用”,在 remark 记录明细
// 参数:
// - ctx请求上下文
// - userID用户ID
// - order待更新的订单对象入参引用被本函数更新 discount_amount/actual_amount/remark
// - activityID活动ID用于范围校验
// - userCouponID用户持券ID
//
// 返回本次实际应用的抵扣金额若不适用或受封顶为0则返回0
func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *model.Orders, activityID int64, userCouponID int64) int64 {
uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(userCouponID), h.readDB.UserCoupons.UserID.Eq(userID), h.readDB.UserCoupons.Status.Eq(1)).First()
if uc == nil {
return 0
}
fmt.Printf("[优惠券] 用户券ID=%d 开始=%s 结束=%s\n", userCouponID, uc.ValidStart.Format(time.RFC3339), func() string {
if uc.ValidEnd.IsZero() {
return "无截止"
}
return uc.ValidEnd.Format(time.RFC3339)
}())
sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).First()
now := time.Now()
if sc == nil {
fmt.Printf("[优惠券] 模板不存在 用户券ID=%d\n", userCouponID)
return 0
}
if uc.ValidStart.After(now) {
fmt.Printf("[优惠券] 未到开始时间 用户券ID=%d 当前=%s\n", userCouponID, now.Format(time.RFC3339))
return 0
}
if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
fmt.Printf("[优惠券] 已过期 用户券ID=%d 当前=%s\n", userCouponID, now.Format(time.RFC3339))
return 0
}
scopeOK := (sc.ScopeType == 1) || (sc.ScopeType == 2 && sc.ActivityID == activityID)
if !scopeOK {
fmt.Printf("[优惠券] 范围不匹配 用户券ID=%d scope_type=%d 模板活动ID=%d 当前活动ID=%d\n", userCouponID, sc.ScopeType, sc.ActivityID, activityID)
return 0
}
if order.TotalAmount < sc.MinSpend {
fmt.Printf("[优惠券] 未达使用门槛 用户券ID=%d min_spend(分)=%d 订单总额(分)=%d\n", userCouponID, sc.MinSpend, order.TotalAmount)
return 0
}
cap := order.TotalAmount / 2
remainingCap := cap - order.DiscountAmount
if remainingCap <= 0 {
fmt.Printf("[优惠券] 已达封顶 封顶(分)=%d 剩余封顶(分)=0\n", cap)
return 0
}
fmt.Printf("[优惠券] 计算前 类型=%d 面值/折扣=%d 封顶(分)=%d 剩余封顶(分)=%d 当前实付(分)=%d 累计优惠(分)=%d\n", sc.DiscountType, sc.DiscountValue, cap, remainingCap, order.ActualAmount, order.DiscountAmount)
applied := int64(0)
switch sc.DiscountType {
case 1:
var bal int64
_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
if bal <= 0 {
bal = sc.DiscountValue
}
fmt.Printf("[优惠券] 金额券余额(分)=%d 模板面值(分)=%d\n", bal, sc.DiscountValue)
if bal > 0 {
if bal > remainingCap {
applied = remainingCap
} else {
applied = bal
}
}
case 2:
applied = sc.DiscountValue
if applied > remainingCap {
applied = remainingCap
}
fmt.Printf("[优惠券] 满减券 应用金额(分)=%d\n", applied)
case 3:
rate := sc.DiscountValue
if rate < 0 {
rate = 0
}
if rate > 1000 {
rate = 1000
}
newAmt := order.ActualAmount * rate / 1000
d := order.ActualAmount - newAmt
if d > remainingCap {
applied = remainingCap
} else {
applied = d
}
fmt.Printf("[优惠券] 折扣券 折扣千分比=%d 抵扣(分)=%d\n", rate, applied)
}
if applied > order.ActualAmount {
applied = order.ActualAmount
}
fmt.Printf("[优惠券] 本次抵扣(分)=%d\n", applied)
if applied <= 0 {
return 0
}
order.DiscountAmount += applied
order.ActualAmount -= applied
order.Remark = order.Remark + fmt.Sprintf("|c:%d:%d", userCouponID, applied)
fmt.Printf("[优惠券] 应用后 累计优惠(分)=%d 订单实付(分)=%d\n", order.DiscountAmount, order.ActualAmount)
return applied
}
// updateUserCouponAfterApply 应用后更新用户券(扣减余额或核销)
// 功能:根据订单 remark 中记录的 applied_amount
//
// 对直金额券扣减余额并在余额为0时核销满减/折扣券一次性核销
//
// 参数:
// - ctx请求上下文
// - userID用户ID
// - order订单用于读取 remark 和写入 used_order_id
// - userCouponID用户持券ID
//
// 返回:无
func (h *handler) updateUserCouponAfterApply(ctx core.Context, userID int64, order *model.Orders, userCouponID int64) {
uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(userCouponID), h.readDB.UserCoupons.UserID.Eq(userID), h.readDB.UserCoupons.Status.Eq(1)).First()
if uc == nil {
return
}
sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).First()
if sc == nil {
return
}
applied := int64(0)
remark := order.Remark
p := 0
for i := 0; i < len(remark); i++ {
if remark[i] == '|' {
seg := remark[p:i]
if len(seg) > 2 && seg[:2] == "c:" {
j := 2
var id int64
for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' {
id = id*10 + int64(seg[j]-'0')
j++
}
if j < len(seg) && seg[j] == ':' {
j++
var amt int64
for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' {
amt = amt*10 + int64(seg[j]-'0')
j++
}
if id == userCouponID {
applied = amt
}
}
}
p = i + 1
}
}
if sc.DiscountType == 1 {
var bal int64
_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
newBal := bal - applied
if newBal < 0 {
newBal = 0
}
if newBal == 0 {
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{"balance_amount": newBal, h.writeDB.UserCoupons.Status.ColumnName().String(): 2, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()})
} else {
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{"balance_amount": newBal, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()})
}
} else {
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{h.writeDB.UserCoupons.Status.ColumnName().String(): 2, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()})
}
}
// markOrderPaid 将订单标记为已支付
// 功能用于0元订单直接置为已支付并写入支付时间
// 参数:
// - ctx请求上下文
// - orderNo订单号
//
// 返回:无
func (h *handler) markOrderPaid(ctx core.Context, orderNo string) {
now := time.Now()
_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{h.writeDB.Orders.Status.ColumnName().String(): 2, h.writeDB.Orders.PaidAt.ColumnName().String(): now})
}
// processInstantDraw 即时抽奖流程
// 功能:在订单已支付情况下,执行抽奖与发奖;支持一番赏固定格位与普通模式,处理道具卡效果
// 参数:
// - ctx请求上下文
// - userID用户ID
// - activity活动实体用于判断玩法
// - activityID活动ID
// - issueID期ID
// - orderNo订单号
// - itemCardID道具卡ID可选
//
// 返回:无
func (h *handler) processInstantDraw(ctx core.Context, userID int64, activity *model.Activities, activityID int64, issueID int64, orderNo string, itemCardID *int64) {
ord, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(orderNo)).First()
if ord == nil {
return
}
slotsIdx, slotsCnt := parseSlotsCountsFromRemark(ord.Remark)
dc := func() int64 {
if len(slotsIdx) > 0 && len(slotsIdx) == len(slotsCnt) {
var s int64
for i := range slotsCnt {
if slotsCnt[i] > 0 {
s += slotsCnt[i]
}
}
if s > 0 {
return s
}
}
remark := ord.Remark
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
}()
sel := strat.NewDefault(h.readDB, h.writeDB)
logs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(ord.ID)).Find()
done := int64(len(logs))
rem := make([]int64, len(slotsCnt))
copy(rem, slotsCnt)
cur := 0
for i := done; i < dc; i++ {
rid := int64(0)
var e2 error
if activity.PlayType == "ichiban" {
slot := func() int64 {
if len(slotsIdx) > 0 && len(slotsIdx) == len(rem) {
for cur < len(rem) && rem[cur] == 0 {
cur++
}
if cur >= len(rem) {
return -1
}
rem[cur]--
return slotsIdx[cur] - 1
}
return 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=?", issueID, slot).Scan(&cnt).Error
if cnt > 0 {
break
}
e := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", issueID, slot, userID, ord.ID).Error
if e != nil {
break
}
rid, _, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), activityID, issueID, slot)
} else {
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
}
} else {
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
}
if e2 != nil || rid <= 0 {
break
}
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
if rw == nil {
break
}
if activity.PlayType == "ichiban" {
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
} else {
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name})
}
log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
fmt.Printf("[道具卡-processInstantDraw] 开始检查 活动允许道具卡=%t itemCardID=%v\n", activity.AllowItemCards, itemCardID)
if activity.AllowItemCards && itemCardID != nil && *itemCardID > 0 {
fmt.Printf("[道具卡-processInstantDraw] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*itemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).First()
if uic != nil {
fmt.Printf("[道具卡-processInstantDraw] 找到用户道具卡 ID=%d CardID=%d Status=%d ValidStart=%s ValidEnd=%s\n", uic.ID, uic.CardID, uic.Status, uic.ValidStart.Format(time.RFC3339), uic.ValidEnd.Format(time.RFC3339))
ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
now := time.Now()
if ic != nil {
fmt.Printf("[道具卡-processInstantDraw] 找到系统道具卡 ID=%d Name=%s EffectType=%d RewardMultiplierX1000=%d ScopeType=%d ActivityID=%d IssueID=%d Status=%d\n", ic.ID, ic.Name, ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, ic.ActivityID, ic.IssueID, ic.Status)
fmt.Printf("[道具卡-processInstantDraw] 时间检查 当前时间=%s ValidStart.After(now)=%t ValidEnd.Before(now)=%t\n", now.Format(time.RFC3339), uic.ValidStart.After(now), uic.ValidEnd.Before(now))
if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == activityID) || (ic.ScopeType == 4 && ic.IssueID == issueID)
fmt.Printf("[道具卡-processInstantDraw] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, activityID, issueID, scopeOK)
if scopeOK {
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
fmt.Printf("[道具卡-processInstantDraw] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s PlayType=%s\n", ic.RewardMultiplierX1000, rid, rw.Name, activity.PlayType)
if activity.PlayType == "ichiban" {
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
} else {
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
}
fmt.Printf("[道具卡-processInstantDraw] ✅ 双倍奖励发放完成\n")
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
fmt.Printf("[道具卡-processInstantDraw] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
var better *model.ActivityRewardSettings
for _, r := range uprw {
if r.Level < rw.Level && r.Quantity != 0 {
if better == nil || r.Level < better.Level {
better = r
}
}
}
if better != nil && rand.Int31n(1000) < ic.BoostRateX1000 {
rid2 := better.ID
fmt.Printf("[道具卡-processInstantDraw] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
if activity.PlayType == "ichiban" {
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid2)
} else {
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: better.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
}
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
} else {
fmt.Printf("[道具卡-processInstantDraw] 概率提升未触发\n")
}
} else {
fmt.Printf("[道具卡-processInstantDraw] ⚠️ 道具卡效果类型不匹配或倍数不足 EffectType=%d RewardMultiplierX1000=%d\n", ic.EffectType, ic.RewardMultiplierX1000)
}
fmt.Printf("[道具卡-processInstantDraw] 核销道具卡 用户道具卡ID=%d\n", *itemCardID)
_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*itemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).Updates(map[string]any{h.readDB.UserItemCards.Status.ColumnName().String(): 2, h.readDB.UserItemCards.UsedDrawLogID.ColumnName().String(): log.ID, h.readDB.UserItemCards.UsedActivityID.ColumnName().String(): activityID, h.readDB.UserItemCards.UsedIssueID.ColumnName().String(): issueID, h.readDB.UserItemCards.UsedAt.ColumnName().String(): time.Now()})
} else {
fmt.Printf("[道具卡-processInstantDraw] ❌ 范围检查失败\n")
}
} else {
fmt.Printf("[道具卡-processInstantDraw] ❌ 时间检查失败\n")
}
} else {
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
}
} else {
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
}
} else {
fmt.Printf("[道具卡-processInstantDraw] 跳过道具卡检查\n")
}
}
}