Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 15s
559 lines
23 KiB
Go
559 lines
23 KiB
Go
package app
|
||
|
||
import (
|
||
"bindbox-game/internal/code"
|
||
"bindbox-game/internal/pkg/core"
|
||
"bindbox-game/internal/pkg/validation"
|
||
"bindbox-game/internal/repository/mysql/model"
|
||
strat "bindbox-game/internal/service/activity/strategy"
|
||
usersvc "bindbox-game/internal/service/user"
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math/rand"
|
||
"net/http"
|
||
"time"
|
||
|
||
titlesvc "bindbox-game/internal/service/title"
|
||
)
|
||
|
||
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
|
||
}
|
||
// Ichiban Restriction: No Item Cards allowed (even if Activity logic might technically allow it, enforce strict rule here)
|
||
if activity.PlayType == "ichiban" && req.ItemCardID != nil && *req.ItemCardID > 0 {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170010, "一番赏活动不支持道具卡"))
|
||
return
|
||
}
|
||
cfgMode := "scheduled"
|
||
if activity.DrawMode != "" {
|
||
cfgMode = activity.DrawMode
|
||
}
|
||
// 定时一番赏:开奖前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 {
|
||
order.ItemCardID = *req.ItemCardID
|
||
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
|
||
applied := int64(0)
|
||
if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
|
||
order.CouponID = *req.CouponID
|
||
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
|
||
}
|
||
// Title Discount Logic
|
||
// 1. Fetch active effects for this user, scoped to this activity/issue/category
|
||
// Note: Category ID is not readily available on Activity struct in this scope easily without join, skipping detailed scope for now or fetch if needed.
|
||
// Assuming scope by ActivityID and IssueID is enough.
|
||
titleEffects, _ := h.title.ResolveActiveEffects(ctx.RequestContext(), userID, titlesvc.EffectScope{
|
||
ActivityID: &req.ActivityID,
|
||
IssueID: &req.IssueID,
|
||
})
|
||
|
||
// 2. Apply Type=2 (Discount) effects
|
||
for _, ef := range titleEffects {
|
||
if ef.EffectType == 2 {
|
||
// Parse ParamsJSON: {"discount_type":"percentage","value_x1000":...,"max_discount_x1000":...}
|
||
// Simple parsing here or helper
|
||
var p struct {
|
||
DiscountType string `json:"discount_type"`
|
||
ValueX1000 int64 `json:"value_x1000"`
|
||
MaxDiscountX1000 int64 `json:"max_discount_x1000"`
|
||
}
|
||
if jsonErr := json.Unmarshal([]byte(ef.ParamsJSON), &p); jsonErr == nil {
|
||
var discount int64
|
||
if p.DiscountType == "percentage" {
|
||
// e.g. 900 = 90% (10% off), or value_x1000 is the discount rate?
|
||
// Usually "value" is what you pay? Title "8折" -> value=800?
|
||
// Let's assume value_x1000 is the DISCOUNT amount (e.g. 200 = 20% off).
|
||
// Wait, standard is usually "multiplier". Title "Discount" usually means "Cut".
|
||
// Let's look at `ValidateEffectParams`: "percentage" or "fixed".
|
||
// Assume ValueX1000 is discount ratio. 200 = 20% off.
|
||
discount = order.ActualAmount * p.ValueX1000 / 1000
|
||
} else if p.DiscountType == "fixed" {
|
||
discount = p.ValueX1000 // In cents
|
||
}
|
||
|
||
if p.MaxDiscountX1000 > 0 && discount > p.MaxDiscountX1000 {
|
||
discount = p.MaxDiscountX1000
|
||
}
|
||
|
||
if discount > order.ActualAmount {
|
||
discount = order.ActualAmount
|
||
}
|
||
|
||
if discount > 0 {
|
||
order.ActualAmount -= discount
|
||
// Append to remark or separate logging?
|
||
if order.Remark == "" {
|
||
order.Remark = fmt.Sprintf("title_discount:%d:%d", ef.ID, discount)
|
||
} else {
|
||
order.Remark += fmt.Sprintf("|title_discount:%d:%d", ef.ID, discount)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
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})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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
|
||
}
|
||
// Daily Seed logic removed to ensure strict adherence to CommitmentSeedMaster
|
||
|
||
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 {
|
||
var proof map[string]any
|
||
rid, proof, 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)
|
||
log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, issueID, userID, proof)
|
||
rsp.Result = map[string]any{"reward_id": rid, "reward_name": rw.Name}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
sel := strat.NewDefault(h.readDB, h.writeDB)
|
||
var proof map[string]any
|
||
rid, proof, 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)
|
||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, issueID, userID, proof)
|
||
icID := parseItemCardIDFromRemark(ord.Remark)
|
||
if icID > 0 {
|
||
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 {
|
||
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 {
|
||
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)
|
||
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)
|
||
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: orderID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
|
||
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
|
||
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
|
||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: orderID, ProductID: better.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
|
||
// 创建升级后的抽奖日志并保存凭据
|
||
drawLog2 := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1}
|
||
if err := h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog2); err != nil {
|
||
} else {
|
||
if err := strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog2.ID, issueID, userID, proof); err != nil {
|
||
} else {
|
||
}
|
||
}
|
||
} else {
|
||
}
|
||
} else {
|
||
}
|
||
_, _ = 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 {
|
||
}
|
||
} else {
|
||
}
|
||
} else {
|
||
}
|
||
} else {
|
||
}
|
||
} else {
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|