07409eb602
Closes HIF-4 Stage 2 交付:人工奖领奖工单完整闭环。crypto / physical / manual_other 三类奖品从抽中到 mark-paid 的全流程可用。 - 迁移 02159_lottery_claim:UNIQUE(draw_id) + 3 支持索引,状态机 pending_claim→reviewing→paying→paid,rejected 可复活,超时 expired - 3 个 PrizeHandler:Dispatch→ErrDispatchNotSupported 兜底、ClaimSchema 各自形态、ValidateClaim 表驱动 - BuildCryptoClaimSchema:抽中时按奖品 config.networks 注入 enum,前端下拉直接可用 - Draw service dispatchOrEnqueueClaim:人工奖同 tx 插 pending_claim(回滚双清),nonce 重放回读 ExpiresAt + ClaimFormSchema - POST /claim 实装:ownership 校验 → prize 类型校验 → handler.ValidateClaim → crypto network 白名单二次校验 → tx CAS status IN (pending_claim, rejected) AND expires_at > now - Admin CRUD 5 接口:list(IN 批拉 snap + user,无 N+1)、summary(GROUP BY 一次拿计数 + overdue 单查)、approve/reject/mark-paid 全走 CAS + audit - Scheduler @every 1h 扫过期,级联 lottery_draw.dispatch_state → expired - 新增错误码 100005-100011(already_submitted / invalid_claim_data / draw_not_found / not_your_draw / claim_expired / claim_state_invalid) - Rebase 后 Stage 2 测试主动 reuse PR E 的 unmetReasonsNotEmpty + evaluatedAtNotZero matcher,人工奖分支若绕过守卫会立即挂 - Stage 1 全部 4 处 guardrail 后端 rebase 时自检过:UnmetReasons、EvaluatedAt、GrantLedger.Payload、AdminMetaMiddleware 全保留 CI 全绿;28 files, +2442/-126;覆盖率 handler 78.9% / model.lottery 74.3% / draw 68.7% / queue/lottery 76.9%
500 lines
16 KiB
Go
500 lines
16 KiB
Go
// Package lottery implements the user-side lottery HTTP endpoints:
|
||
//
|
||
// GET /api/v1/lottery/config
|
||
// POST /api/v1/lottery/draw
|
||
// GET /api/v1/lottery/records
|
||
// POST /api/v1/lottery/claim (Stage 2: submit manual claim data)
|
||
package lottery
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/perfect-panel/server/internal/logic/lottery/draw"
|
||
lotteryhandler "github.com/perfect-panel/server/internal/logic/lottery/handler"
|
||
modelLottery "github.com/perfect-panel/server/internal/model/lottery"
|
||
userModel "github.com/perfect-panel/server/internal/model/user"
|
||
"github.com/perfect-panel/server/internal/svc"
|
||
"github.com/perfect-panel/server/internal/types"
|
||
"github.com/perfect-panel/server/pkg/constant"
|
||
"github.com/perfect-panel/server/pkg/logger"
|
||
"github.com/perfect-panel/server/pkg/xerr"
|
||
"github.com/pkg/errors"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// currentUserId 取 middleware.AuthMiddleware 注入的 user 上下文。
|
||
// 匿名 / 未登录返回 0;handler 侧应当由 AuthMiddleware 已经拦截。
|
||
func currentUserId(ctx context.Context) int64 {
|
||
u, ok := ctx.Value(constant.CtxKeyUser).(*userModel.User)
|
||
if !ok || u == nil {
|
||
return 0
|
||
}
|
||
return u.Id
|
||
}
|
||
|
||
// ---- GET /config ------------------------------------------------------------
|
||
|
||
// QueryLotteryConfigLogic 组装活动 + 奖品 + 用户门槛/次数状态。
|
||
type QueryLotteryConfigLogic struct {
|
||
logger.Logger
|
||
ctx context.Context
|
||
svcCtx *svc.ServiceContext
|
||
}
|
||
|
||
func NewQueryLotteryConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryLotteryConfigLogic {
|
||
return &QueryLotteryConfigLogic{
|
||
Logger: logger.WithContext(ctx),
|
||
ctx: ctx,
|
||
svcCtx: svcCtx,
|
||
}
|
||
}
|
||
|
||
func (l *QueryLotteryConfigLogic) QueryLotteryConfig(req *types.GetLotteryConfigRequest) (*types.GetLotteryConfigResponse, error) {
|
||
userId := currentUserId(l.ctx)
|
||
if userId == 0 {
|
||
return nil, xerr.NewErrCode(xerr.ErrorTokenInvalid)
|
||
}
|
||
activity, err := l.loadActivity(req.ActivityId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
prizes, err := l.loadPrizes(req.ActivityId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
remaining, _ := l.svcCtx.LotteryChance.Query(l.ctx, userId, req.ActivityId)
|
||
|
||
resp := &types.GetLotteryConfigResponse{
|
||
Activity: types.LotteryActivityConfig{
|
||
Id: activity.Id,
|
||
Title: activity.Title,
|
||
Description: activity.Description,
|
||
StartAt: activity.StartAt.Unix(),
|
||
EndAt: activity.EndAt.Unix(),
|
||
Status: activity.Status,
|
||
GridSize: activity.GridSize,
|
||
},
|
||
User: types.LotteryUserStatus{
|
||
ChancesRemaining: remaining,
|
||
// Eligible / UnmetReasons 需要 RuleContextBuilder;PR C 里 draw 路径
|
||
// 用真实构造器,config 路径为节省 DB 查询暂只返回次数,前端拿到
|
||
// 未通过时的具体 reason 是在 POST /draw 返回码 100001 里附带的。
|
||
Eligible: true,
|
||
},
|
||
}
|
||
resp.Activity.Prizes = make([]types.LotteryPrizeConfig, 0, len(prizes))
|
||
for _, p := range prizes {
|
||
soldOut := p.RemainingStock.Valid && p.RemainingStock.Int64 <= 0
|
||
resp.Activity.Prizes = append(resp.Activity.Prizes, types.LotteryPrizeConfig{
|
||
Slot: p.Slot,
|
||
Id: p.Id,
|
||
Type: p.Type,
|
||
Name: p.Name,
|
||
IconUrl: p.IconURL,
|
||
Config: json.RawMessage(defaultIfEmpty(p.Config)),
|
||
SoldOut: soldOut,
|
||
})
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
func (l *QueryLotteryConfigLogic) loadActivity(id int64) (*modelLottery.Activity, error) {
|
||
var activity modelLottery.Activity
|
||
err := l.svcCtx.DB.WithContext(l.ctx).Where("id = ?", id).First(&activity).Error
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, xerr.NewErrCode(xerr.LotteryActivityEnded)
|
||
}
|
||
return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
if activity.Status == modelLottery.ActivityStatusEnded {
|
||
return nil, xerr.NewErrCode(xerr.LotteryActivityEnded)
|
||
}
|
||
return &activity, nil
|
||
}
|
||
|
||
func (l *QueryLotteryConfigLogic) loadPrizes(activityId int64) ([]modelLottery.Prize, error) {
|
||
var prizes []modelLottery.Prize
|
||
err := l.svcCtx.DB.WithContext(l.ctx).
|
||
Where("activity_id = ?", activityId).
|
||
Order("slot ASC").
|
||
Find(&prizes).Error
|
||
if err != nil {
|
||
return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
return prizes, nil
|
||
}
|
||
|
||
func defaultIfEmpty(s string) string {
|
||
if strings.TrimSpace(s) == "" {
|
||
return "{}"
|
||
}
|
||
return s
|
||
}
|
||
|
||
// ---- POST /draw -------------------------------------------------------------
|
||
|
||
// DrawLotteryLogic 是 POST /draw 的入口,委托给 draw.Service。
|
||
type DrawLotteryLogic struct {
|
||
logger.Logger
|
||
ctx context.Context
|
||
svcCtx *svc.ServiceContext
|
||
}
|
||
|
||
func NewDrawLotteryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DrawLotteryLogic {
|
||
return &DrawLotteryLogic{
|
||
Logger: logger.WithContext(ctx),
|
||
ctx: ctx,
|
||
svcCtx: svcCtx,
|
||
}
|
||
}
|
||
|
||
func (l *DrawLotteryLogic) DrawLottery(req *types.DrawLotteryRequest) (*types.DrawLotteryResponse, error) {
|
||
userId := currentUserId(l.ctx)
|
||
if userId == 0 {
|
||
return nil, xerr.NewErrCode(xerr.ErrorTokenInvalid)
|
||
}
|
||
if l.svcCtx.LotteryDrawService == nil {
|
||
return nil, xerr.NewErrCode(xerr.LotteryActivityEnded)
|
||
}
|
||
result, err := l.svcCtx.LotteryDrawService.Draw(l.ctx, draw.Request{
|
||
UserId: userId,
|
||
ActivityId: req.ActivityId,
|
||
ClientNonce: req.ClientNonce,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &types.DrawLotteryResponse{
|
||
DrawId: result.DrawId,
|
||
IsWin: result.IsWin,
|
||
ChancesRemaining: result.ChancesRemaining,
|
||
Claim: types.LotteryClaimStatus{
|
||
Required: result.Claim.Required,
|
||
AutoClaimed: result.Claim.AutoClaimed,
|
||
Message: result.Claim.Message,
|
||
ExpiresAt: result.Claim.ExpiresAt,
|
||
ClaimFormSchema: result.Claim.ClaimFormSchema,
|
||
},
|
||
}
|
||
if result.Prize != nil {
|
||
resp.Prize = &types.DrawnPrize{
|
||
Slot: result.Prize.Slot,
|
||
Id: result.Prize.Id,
|
||
Type: result.Prize.Type,
|
||
Name: result.Prize.Name,
|
||
Config: result.Prize.Config,
|
||
}
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// ---- GET /records ----------------------------------------------------------
|
||
|
||
// QueryLotteryRecordsLogic 分页列出当前用户的中奖流水。
|
||
type QueryLotteryRecordsLogic struct {
|
||
logger.Logger
|
||
ctx context.Context
|
||
svcCtx *svc.ServiceContext
|
||
}
|
||
|
||
func NewQueryLotteryRecordsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryLotteryRecordsLogic {
|
||
return &QueryLotteryRecordsLogic{
|
||
Logger: logger.WithContext(ctx),
|
||
ctx: ctx,
|
||
svcCtx: svcCtx,
|
||
}
|
||
}
|
||
|
||
func (l *QueryLotteryRecordsLogic) QueryLotteryRecords(req *types.GetLotteryRecordsRequest) (*types.GetLotteryRecordsResponse, error) {
|
||
userId := currentUserId(l.ctx)
|
||
if userId == 0 {
|
||
return nil, xerr.NewErrCode(xerr.ErrorTokenInvalid)
|
||
}
|
||
page, size := req.Page, req.Size
|
||
if page <= 0 {
|
||
page = 1
|
||
}
|
||
if size <= 0 || size > 200 {
|
||
size = 20
|
||
}
|
||
db := l.svcCtx.DB.WithContext(l.ctx).
|
||
Model(&modelLottery.Draw{}).
|
||
Where("user_id = ?", userId)
|
||
if req.ActivityId > 0 {
|
||
db = db.Where("activity_id = ?", req.ActivityId)
|
||
}
|
||
if state := recordStatusFilter(req.Status); state != "" {
|
||
switch state {
|
||
case "unclaimed":
|
||
db = db.Where("dispatch_state = ?", modelLottery.DispatchStatePendingClaim)
|
||
case "paid":
|
||
db = db.Where("dispatch_state = ?", modelLottery.DispatchStatePaid)
|
||
case "expired":
|
||
db = db.Where("dispatch_state = ?", modelLottery.DispatchStateExpired)
|
||
}
|
||
}
|
||
var total int64
|
||
if err := db.Count(&total).Error; err != nil {
|
||
return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
var draws []modelLottery.Draw
|
||
if err := db.Order("drawn_at DESC").Limit(size).Offset((page - 1) * size).Find(&draws).Error; err != nil {
|
||
return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
snapshots, err := l.loadPrizeSnapshots(draws)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
claims, err := l.loadClaims(draws)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &types.GetLotteryRecordsResponse{Total: total, List: make([]types.LotteryRecord, 0, len(draws))}
|
||
for _, d := range draws {
|
||
record := types.LotteryRecord{
|
||
DrawId: d.Id,
|
||
ActivityId: d.ActivityId,
|
||
IsWin: d.IsWin,
|
||
DispatchState: d.DispatchState,
|
||
DrawnAt: d.DrawnAt.Unix(),
|
||
}
|
||
snap, hasSnap := snapshots[d.Id]
|
||
if hasSnap {
|
||
record.Prize = &types.DrawnPrize{
|
||
Slot: snap.Slot,
|
||
Id: snap.PrizeId,
|
||
Type: snap.Type,
|
||
Name: snap.Name,
|
||
Config: json.RawMessage(defaultIfEmpty(snap.Config)),
|
||
}
|
||
}
|
||
if claim, ok := claims[d.Id]; ok {
|
||
record.Claim = l.buildRecordClaim(claim, snap, hasSnap)
|
||
}
|
||
resp.List = append(resp.List, record)
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// loadClaims 批量拉这一页里所有 draw 关联的 lottery_claim。人工奖 draw 一定有一行,
|
||
// 自动奖 draw / 谢谢参与不会有;缺失的 draw_id 直接不在 map 里,调用侧只做存在性判断。
|
||
func (l *QueryLotteryRecordsLogic) loadClaims(draws []modelLottery.Draw) (map[int64]modelLottery.Claim, error) {
|
||
if len(draws) == 0 {
|
||
return nil, nil
|
||
}
|
||
ids := make([]int64, 0, len(draws))
|
||
for _, d := range draws {
|
||
ids = append(ids, d.Id)
|
||
}
|
||
var rows []modelLottery.Claim
|
||
if err := l.svcCtx.DB.WithContext(l.ctx).Where("draw_id IN ?", ids).Find(&rows).Error; err != nil {
|
||
return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
out := make(map[int64]modelLottery.Claim, len(rows))
|
||
for _, c := range rows {
|
||
out[c.DrawId] = c
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// buildRecordClaim 把 lottery_claim 组装成 GET /records 里的 Claim 字段。
|
||
// 状态允许再提交(pending_claim / rejected)时附带 ClaimFormSchema,
|
||
// 否则不再下发(避免前端误以为还能再填)。
|
||
func (l *QueryLotteryRecordsLogic) buildRecordClaim(claim modelLottery.Claim, snap modelLottery.PrizeSnapshot, hasSnap bool) *types.LotteryRecordClaim {
|
||
view := &types.LotteryRecordClaim{
|
||
Status: claim.Status,
|
||
ExpiresAt: claim.ExpiresAt.Unix(),
|
||
TxHash: claim.TxHash,
|
||
DeliveryRef: claim.DeliveryRef,
|
||
RejectReason: claim.RejectReason,
|
||
}
|
||
if claim.ClaimData != "" {
|
||
view.ClaimData = json.RawMessage(claim.ClaimData)
|
||
}
|
||
if claim.SubmittedAt != nil {
|
||
view.SubmittedAt = claim.SubmittedAt.Unix()
|
||
}
|
||
if claim.PaidAt != nil {
|
||
view.PaidAt = claim.PaidAt.Unix()
|
||
}
|
||
// 只在允许再提交状态下下发 schema。
|
||
if modelLottery.IsClaimStatusResubmittable(claim.Status) && l.svcCtx.LotteryRegistry != nil {
|
||
if h, ok := l.svcCtx.LotteryRegistry.Get(claim.PrizeType); ok {
|
||
if claim.PrizeType == modelLottery.PrizeTypeCrypto && hasSnap {
|
||
view.ClaimFormSchema = lotteryhandler.BuildCryptoClaimSchema(snap.Config)
|
||
} else {
|
||
view.ClaimFormSchema = h.ClaimSchema()
|
||
}
|
||
}
|
||
}
|
||
return view
|
||
}
|
||
|
||
func (l *QueryLotteryRecordsLogic) loadPrizeSnapshots(draws []modelLottery.Draw) (map[int64]modelLottery.PrizeSnapshot, error) {
|
||
if len(draws) == 0 {
|
||
return nil, nil
|
||
}
|
||
ids := make([]int64, 0, len(draws))
|
||
for _, d := range draws {
|
||
ids = append(ids, d.Id)
|
||
}
|
||
var snaps []modelLottery.PrizeSnapshot
|
||
if err := l.svcCtx.DB.WithContext(l.ctx).Where("draw_id IN ?", ids).Find(&snaps).Error; err != nil {
|
||
return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
out := make(map[int64]modelLottery.PrizeSnapshot, len(snaps))
|
||
for _, s := range snaps {
|
||
out[s.DrawId] = s
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func recordStatusFilter(s string) string {
|
||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||
case "", "all":
|
||
return ""
|
||
case "unclaimed", "paid", "expired":
|
||
return strings.ToLower(s)
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// ---- POST /claim ----------------------------------------------------------
|
||
|
||
// ClaimLotteryPrizeLogic 是 Stage 2 人工奖领奖入口。
|
||
//
|
||
// 调用契约(错误码见 pkg/xerr):
|
||
//
|
||
// 4007 draw_not_found — 传入的 draw_id 不存在
|
||
// 4008 not_your_draw — draw 属于其他用户
|
||
// 4010 not_claimable — 该 draw 未中奖 / 自动奖 / 找不到 pending_claim
|
||
// 4009 claim_expired — pending_claim.expires_at 已过期
|
||
// 4005 already_submitted — 当前状态 (reviewing/paying/paid/expired) 禁止再提交
|
||
// 4006 invalid_claim_data — handler.ValidateClaim 校验失败
|
||
type ClaimLotteryPrizeLogic struct {
|
||
logger.Logger
|
||
ctx context.Context
|
||
svcCtx *svc.ServiceContext
|
||
}
|
||
|
||
func NewClaimLotteryPrizeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ClaimLotteryPrizeLogic {
|
||
return &ClaimLotteryPrizeLogic{
|
||
Logger: logger.WithContext(ctx),
|
||
ctx: ctx,
|
||
svcCtx: svcCtx,
|
||
}
|
||
}
|
||
|
||
// ClaimLotteryPrize 提交领奖表单:pending_claim → reviewing,或 rejected → reviewing。
|
||
func (l *ClaimLotteryPrizeLogic) ClaimLotteryPrize(req *types.ClaimLotteryPrizeRequest) (*types.ClaimLotteryPrizeResponse, error) {
|
||
userId := currentUserId(l.ctx)
|
||
if userId == 0 {
|
||
return nil, xerr.NewErrCode(xerr.ErrorTokenInvalid)
|
||
}
|
||
if req.DrawId <= 0 {
|
||
return nil, xerr.NewErrCode(xerr.InvalidParams)
|
||
}
|
||
claimData := selectClaimData(req)
|
||
|
||
// 1. 定位 draw + 归属校验(提前失败,避免暴露内部资源)
|
||
var draw modelLottery.Draw
|
||
if err := l.svcCtx.DB.WithContext(l.ctx).Where("id = ?", req.DrawId).First(&draw).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, xerr.NewErrCode(xerr.LotteryDrawNotFound)
|
||
}
|
||
return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
if draw.UserId != userId {
|
||
return nil, xerr.NewErrCode(xerr.LotteryNotYourDraw)
|
||
}
|
||
if !draw.IsWin || draw.DispatchState != modelLottery.DispatchStatePendingClaim {
|
||
return nil, xerr.NewErrCode(xerr.LotteryNotClaimable)
|
||
}
|
||
|
||
// 2. 抽奖时刻快照(用于 crypto network 二次校验)
|
||
var snap modelLottery.PrizeSnapshot
|
||
if err := l.svcCtx.DB.WithContext(l.ctx).Where("draw_id = ?", draw.Id).First(&snap).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, xerr.NewErrCode(xerr.LotteryNotClaimable)
|
||
}
|
||
return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
if !modelLottery.IsPrizeTypeManualClaim(snap.Type) {
|
||
return nil, xerr.NewErrCode(xerr.LotteryNotClaimable)
|
||
}
|
||
handler, ok := l.svcCtx.LotteryRegistry.Get(snap.Type)
|
||
if !ok {
|
||
return nil, xerr.NewErrCode(xerr.LotteryInternalError)
|
||
}
|
||
|
||
// 3. handler 校验 body
|
||
if err := handler.ValidateClaim(claimData); err != nil {
|
||
return nil, xerr.NewErrCodeMsg(xerr.LotteryInvalidClaimData, err.Error())
|
||
}
|
||
if snap.Type == modelLottery.PrizeTypeCrypto {
|
||
if err := lotteryhandler.ValidateCryptoNetwork(claimData, snap.Config); err != nil {
|
||
return nil, xerr.NewErrCodeMsg(xerr.LotteryInvalidClaimData, err.Error())
|
||
}
|
||
}
|
||
|
||
// 4. 事务内更新 claim(乐观锁:status IN (pending_claim, rejected) AND expires_at > now)
|
||
now := time.Now()
|
||
var response *types.ClaimLotteryPrizeResponse
|
||
err := l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
|
||
var claim modelLottery.Claim
|
||
if err := tx.Where("draw_id = ?", draw.Id).First(&claim).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return xerr.NewErrCode(xerr.LotteryNotClaimable)
|
||
}
|
||
return errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||
}
|
||
if claim.ExpiresAt.Before(now) {
|
||
return xerr.NewErrCode(xerr.LotteryClaimExpired)
|
||
}
|
||
if !modelLottery.IsClaimStatusResubmittable(claim.Status) {
|
||
return xerr.NewErrCode(xerr.LotteryAlreadySubmitted)
|
||
}
|
||
|
||
// CAS 更新:命中 status 白名单 + expires_at 未过期时才走。RowsAffected==0
|
||
// 视为并发拦截(另一个请求已经把状态推进了),报 4005 即可。
|
||
res := tx.Model(&modelLottery.Claim{}).
|
||
Where("id = ? AND status IN ? AND expires_at > ?",
|
||
claim.Id,
|
||
[]string{modelLottery.ClaimStatusPendingClaim, modelLottery.ClaimStatusRejected},
|
||
now).
|
||
Updates(map[string]any{
|
||
"claim_data": string(claimData),
|
||
"status": modelLottery.ClaimStatusReviewing,
|
||
"submitted_at": now,
|
||
})
|
||
if res.Error != nil {
|
||
return errors.Wrap(xerr.NewErrCode(xerr.DatabaseUpdateError), res.Error.Error())
|
||
}
|
||
if res.RowsAffected == 0 {
|
||
return xerr.NewErrCode(xerr.LotteryAlreadySubmitted)
|
||
}
|
||
response = &types.ClaimLotteryPrizeResponse{
|
||
Status: modelLottery.ClaimStatusReviewing,
|
||
SubmittedAt: now.Unix(),
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return response, nil
|
||
}
|
||
|
||
// selectClaimData 兼容 ClaimData(首选)与 Input(历史字段名)。
|
||
func selectClaimData(req *types.ClaimLotteryPrizeRequest) []byte {
|
||
if len(req.ClaimData) > 0 {
|
||
return req.ClaimData
|
||
}
|
||
return req.Input
|
||
}
|