refactor(utils): 修复密码哈希比较逻辑错误 feat(user): 新增按状态筛选优惠券接口 docs: 添加虚拟发货与任务中心相关文档 fix(wechat): 修正Code2Session上下文传递问题 test: 补充订单折扣与积分转换测试用例 build: 更新配置文件与构建脚本 style: 清理多余的空行与注释
1180 lines
53 KiB
Go
1180 lines
53 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"
|
||
"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:请求上下文
|
||
// - req:JoinLottery 请求体,使用其中的 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")
|
||
}
|
||
}
|
||
}
|