Files
hi-server/internal/types/lottery.go
T
shanshanzhong147 2c1ee78bc4 修复(#4): 奖品更新支持修改 type(原来 type 被静默忽略)
UpdateLotteryPrize 之前可改字段漏了 type,导致 PUT 带 type 不生效、
只能删了重建。补上 UpdateAdminLotteryPrizeRequest.Type + fields["type"]。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 01:03:42 -07:00

445 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Types for lottery Stage 1 user & admin APIs. Kept in a separate file so
// future `goctl` regenerations of types.go do not clobber them (same pattern
// as internal/types/subscribe.go).
//
// Field names mirror the shape declared in HIF-3 (Stage 1 spec) and the
// architect's PR C brief. Do NOT rename without updating the .api files and
// notifying frontend.
package types
import "encoding/json"
// ---- User API ---------------------------------------------------------------
// GetLotteryConfigRequest is the query for GET /api/v1/lottery/config.
type GetLotteryConfigRequest struct {
ActivityId int64 `form:"activity_id" validate:"required"`
}
// LotteryActivityConfig is the activity snapshot returned to the user.
type LotteryActivityConfig struct {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
StartAt int64 `json:"start_at"`
EndAt int64 `json:"end_at"`
Status string `json:"status"`
GridSize int `json:"grid_size"`
Prizes []LotteryPrizeConfig `json:"prizes"`
}
// LotteryPrizeConfig is the slot-facing view of a prize (no weight / stock
// exposed — those are admin-only).
type LotteryPrizeConfig struct {
Slot int `json:"slot"`
Id int64 `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
IconUrl string `json:"icon_url"`
Config json.RawMessage `json:"config"`
SoldOut bool `json:"sold_out"`
}
// LotteryUserStatus reports whether the user can currently draw.
type LotteryUserStatus struct {
Eligible bool `json:"eligible"`
ChancesRemaining int64 `json:"chances_remaining"`
UnmetReasons []LotteryUnmetReason `json:"unmet_reasons"`
}
// LotteryUnmetReason is a single unmet-rule explanation for frontend display.
type LotteryUnmetReason struct {
Rule string `json:"rule"`
Hint string `json:"hint"`
Current int64 `json:"current,omitempty"`
Required int64 `json:"required,omitempty"`
}
// GetLotteryConfigResponse is the envelope for GET /config.
type GetLotteryConfigResponse struct {
Activity LotteryActivityConfig `json:"activity"`
User LotteryUserStatus `json:"user"`
}
// DrawLotteryRequest is the body for POST /api/v1/lottery/draw.
type DrawLotteryRequest struct {
ActivityId int64 `json:"activity_id" validate:"required"`
ClientNonce string `json:"client_nonce" validate:"required,max=64"`
}
// DrawnPrize is a slim prize view returned on draw success.
type DrawnPrize struct {
Slot int `json:"slot"`
Id int64 `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Config json.RawMessage `json:"config"`
}
// LotteryClaimStatus tells the frontend whether more action is needed.
type LotteryClaimStatus struct {
Required bool `json:"required"`
AutoClaimed bool `json:"auto_claimed"`
Message string `json:"message,omitempty"`
// ExpiresAt 是领奖窗口截止时间(Unix 秒;0 表示不适用,例如自动奖)。
ExpiresAt int64 `json:"expires_at,omitempty"`
// ClaimFormSchema 是人工奖的领奖表单 JSON Schema(前端据此动态渲染)。
// nil 表示不适用(自动奖 / 谢谢参与)。
ClaimFormSchema json.RawMessage `json:"claim_form_schema,omitempty"`
}
// DrawLotteryResponse is what /draw returns.
type DrawLotteryResponse struct {
DrawId int64 `json:"draw_id"`
IsWin bool `json:"is_win"`
Prize *DrawnPrize `json:"prize"`
Claim LotteryClaimStatus `json:"claim"`
ChancesRemaining int64 `json:"chances_remaining"`
}
// GetLotteryRecordsRequest paginates over the user's draws.
// Page/Size 默认 by logic 层(Page<=0 → 1Size<=0||>200 → 20);tag 里不设
// default 以避免 staticcheck 与 Gin binding 的语义冲突。
type GetLotteryRecordsRequest struct {
ActivityId int64 `form:"activity_id"`
Status string `form:"status"`
Page int `form:"page"`
Size int `form:"size"`
}
// LotteryRecord is one row in the records list.
type LotteryRecord struct {
DrawId int64 `json:"draw_id"`
ActivityId int64 `json:"activity_id"`
IsWin bool `json:"is_win"`
Prize *DrawnPrize `json:"prize"`
DispatchState string `json:"dispatch_state"`
DrawnAt int64 `json:"drawn_at"`
// Claim 是人工奖的工单详情;自动奖 / 未中奖时为 nil。Stage 2 新增。
Claim *LotteryRecordClaim `json:"claim,omitempty"`
}
// LotteryRecordClaim 是 GET /records 里"人工奖工单"的用户视图。
// 状态:pending_claim / reviewing / paying / paid / rejected / expired。
// - 用户在 pending_claim 或 rejected 时可再次提交(前端读 ClaimFormSchema 渲染)。
// - paying / paid / expired 时前端只展示状态与打款/发货结果。
type LotteryRecordClaim struct {
Status string `json:"status"`
ClaimData json.RawMessage `json:"claim_data,omitempty"`
SubmittedAt int64 `json:"submitted_at,omitempty"`
ExpiresAt int64 `json:"expires_at"`
TxHash string `json:"tx_hash,omitempty"`
DeliveryRef string `json:"delivery_ref,omitempty"`
RejectReason string `json:"reject_reason,omitempty"`
PaidAt int64 `json:"paid_at,omitempty"`
// ClaimFormSchema 仅当 status 允许再提交(pending_claim / rejected)时下发。
ClaimFormSchema json.RawMessage `json:"claim_form_schema,omitempty"`
}
// GetLotteryRecordsResponse is the paginated payload.
type GetLotteryRecordsResponse struct {
Total int64 `json:"total"`
List []LotteryRecord `json:"list"`
}
// ClaimLotteryPrizeRequest is Stage 2: submit claim data for a manual prize.
type ClaimLotteryPrizeRequest struct {
DrawId int64 `json:"draw_id" validate:"required"`
ClaimData json.RawMessage `json:"claim_data"`
// Input 是 Stage 1 骨架里预留的旧字段名,为了不破坏前端契约保留。
// Deprecated: Prefer ClaimData for new callers.
Input json.RawMessage `json:"input,omitempty"`
}
// ClaimLotteryPrizeResponse mirrors Stage 2 shape.
type ClaimLotteryPrizeResponse struct {
Status string `json:"status"`
SubmittedAt int64 `json:"submitted_at"`
}
// ---- Admin API --------------------------------------------------------------
// AdminLotteryActivity is the full admin view of an activity.
type AdminLotteryActivity struct {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
StartAt int64 `json:"start_at"`
EndAt int64 `json:"end_at"`
Status string `json:"status"`
GridSize int `json:"grid_size"`
Eligibility json.RawMessage `json:"eligibility"`
ChanceSources json.RawMessage `json:"chance_sources"`
UnmetAction string `json:"unmet_action"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// CreateAdminLotteryActivityRequest creates a new activity in "draft" status.
type CreateAdminLotteryActivityRequest struct {
Title string `json:"title" validate:"required,max=128"`
Description string `json:"description"`
StartAt int64 `json:"start_at" validate:"required"`
EndAt int64 `json:"end_at" validate:"required"`
// GridSize0 由 logic 层兜底为 9(不能在 json tag 里写 default=… ——
// staticcheck SA5008 会拒;encoding/json 也不认这个选项)。
GridSize int `json:"grid_size"`
Eligibility json.RawMessage `json:"eligibility"`
ChanceSources json.RawMessage `json:"chance_sources"`
// UnmetAction:空字符串由 logic 层兜底为 "block"。
UnmetAction string `json:"unmet_action"`
}
// UpdateAdminLotteryActivityRequest updates mutable fields.
type UpdateAdminLotteryActivityRequest struct {
Id int64 `json:"id" validate:"required"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
StartAt int64 `json:"start_at,omitempty"`
EndAt int64 `json:"end_at,omitempty"`
GridSize int `json:"grid_size,omitempty"`
UnmetAction string `json:"unmet_action,omitempty"`
}
// ListAdminLotteryActivitiesRequest paginates admin listings.
// Page/Size 默认 by logic 层。
type ListAdminLotteryActivitiesRequest struct {
Page int `form:"page"`
Size int `form:"size"`
Status string `form:"status,omitempty"`
Search string `form:"search,omitempty"`
}
// ListAdminLotteryActivitiesResponse pages.
type ListAdminLotteryActivitiesResponse struct {
Total int64 `json:"total"`
List []AdminLotteryActivity `json:"list"`
}
// AdminActivityIdRequest is used by GET /detail, publish, pause, delete.
type AdminActivityIdRequest struct {
Id int64 `form:"id" json:"id" validate:"required"`
}
// UpdateAdminLotteryRulesRequest overwrites eligibility / chance_sources /
// unmet_action in a single call. Validated against rule caps before persist.
type UpdateAdminLotteryRulesRequest struct {
Id int64 `json:"id" validate:"required"`
Eligibility json.RawMessage `json:"eligibility"`
ChanceSources json.RawMessage `json:"chance_sources"`
UnmetAction string `json:"unmet_action,omitempty"`
}
// AdminLotteryPrize is the admin view of a prize (includes weight + stock).
type AdminLotteryPrize struct {
Id int64 `json:"id"`
ActivityId int64 `json:"activity_id"`
Slot int `json:"slot"`
Type string `json:"type"`
Name string `json:"name"`
IconUrl string `json:"icon_url"`
Config json.RawMessage `json:"config"`
Weight int `json:"weight"`
TotalStock *int64 `json:"total_stock"`
RemainingStock *int64 `json:"remaining_stock"`
IsFallback bool `json:"is_fallback"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// CreateAdminLotteryPrizeRequest is nested under /activities/{id}/prizes.
type CreateAdminLotteryPrizeRequest struct {
ActivityId int64 `json:"activity_id" validate:"required"`
Slot int `json:"slot"`
Type string `json:"type" validate:"required,max=32"`
Name string `json:"name" validate:"required,max=128"`
IconUrl string `json:"icon_url"`
Config json.RawMessage `json:"config"`
Weight int `json:"weight"`
TotalStock *int64 `json:"total_stock,omitempty"`
IsFallback bool `json:"is_fallback"`
}
// UpdateAdminLotteryPrizeRequest updates mutable prize fields. Id comes from
// the URL path (PUT /prizes/:id), not the body.
type UpdateAdminLotteryPrizeRequest struct {
Id int64 `path:"id" json:"-" validate:"required"`
Slot *int `json:"slot,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
IconUrl string `json:"icon_url,omitempty"`
Config json.RawMessage `json:"config,omitempty"`
Weight *int `json:"weight,omitempty"`
TotalStock *int64 `json:"total_stock,omitempty"`
IsFallback *bool `json:"is_fallback,omitempty"`
}
// ListAdminLotteryPrizesRequest lists prizes for an activity.
type ListAdminLotteryPrizesRequest struct {
ActivityId int64 `form:"activity_id" validate:"required"`
}
// ListAdminLotteryPrizesResponse pages.
type ListAdminLotteryPrizesResponse struct {
List []AdminLotteryPrize `json:"list"`
}
// AdminPrizeIdRequest is used by DELETE /prizes/:id. Id comes from the URL path.
type AdminPrizeIdRequest struct {
Id int64 `path:"id" form:"id" json:"id" validate:"required"`
}
// GrantAdminLotteryChanceRequest gives a specified user N chances on an
// activity. sourceRef doubles as idempotency key.
type GrantAdminLotteryChanceRequest struct {
ActivityId int64 `json:"activity_id" validate:"required"`
UserId int64 `json:"user_id" validate:"required"`
Amount int `json:"amount" validate:"required,min=1"`
SourceRef string `json:"source_ref" validate:"required,max=128"`
}
// ---- Stage 2 Admin Claims --------------------------------------------------
// ListAdminLotteryClaimsRequest lists claims filtered by type/status/activity.
// Page/Size 默认 by logic 层(Page<=0 → 1Size<=0||>200 → 20)。
type ListAdminLotteryClaimsRequest struct {
Type string `form:"type,omitempty"`
Status string `form:"status,omitempty"`
ActivityId int64 `form:"activity_id,omitempty"`
UserId int64 `form:"user_id,omitempty"`
From int64 `form:"from,omitempty"` // Unix 秒
To int64 `form:"to,omitempty"` // Unix 秒
Page int `form:"page"`
Size int `form:"size"`
}
// AdminLotteryClaim is the admin-facing view of one claim row.
type AdminLotteryClaim struct {
Id int64 `json:"id"`
DrawId int64 `json:"draw_id"`
ActivityId int64 `json:"activity_id"`
User AdminLotteryClaimUser `json:"user"`
Prize AdminLotteryClaimPrize `json:"prize"`
Status string `json:"status"`
ClaimData json.RawMessage `json:"claim_data,omitempty"`
SubmittedAt int64 `json:"submitted_at,omitempty"`
ExpiresAt int64 `json:"expires_at"`
ReviewedBy int64 `json:"reviewed_by,omitempty"`
ReviewedAt int64 `json:"reviewed_at,omitempty"`
RejectReason string `json:"reject_reason,omitempty"`
TxHash string `json:"tx_hash,omitempty"`
DeliveryRef string `json:"delivery_ref,omitempty"`
PaidAt int64 `json:"paid_at,omitempty"`
CreatedAt int64 `json:"created_at"`
}
// AdminLotteryClaimUser 是 claim 列表里附带的用户简况。
type AdminLotteryClaimUser struct {
Id int64 `json:"id"`
Email string `json:"email,omitempty"`
}
// AdminLotteryClaimPrize 是 claim 列表里附带的奖品简况(含 config,供审核判断)。
type AdminLotteryClaimPrize struct {
Type string `json:"type"`
Name string `json:"name"`
Config json.RawMessage `json:"config"`
}
// ListAdminLotteryClaimsResponse pages.
type ListAdminLotteryClaimsResponse struct {
Total int64 `json:"total"`
Claims []AdminLotteryClaim `json:"claims"`
}
// AdminApproveClaimRequest 转 reviewing → paying。
type AdminApproveClaimRequest struct {
Id int64 `json:"id" validate:"required"`
}
// AdminRejectClaimRequest 转 reviewing/paying → rejectedreason 必填。
type AdminRejectClaimRequest struct {
Id int64 `json:"id" validate:"required"`
Reason string `json:"reason" validate:"required,max=512"`
}
// AdminMarkPaidClaimRequest 转 paying → paid。
// tx_hash / delivery_ref 至少填一个;crypto 必填 tx_hashphysical 必填
// delivery_refmanual_other 至少填一个(业务层做类型检查)。
// paid_at 可选,缺省时用服务端 now。
type AdminMarkPaidClaimRequest struct {
Id int64 `json:"id" validate:"required"`
TxHash string `json:"tx_hash,omitempty" validate:"max=128"`
DeliveryRef string `json:"delivery_ref,omitempty" validate:"max=128"`
PaidAt int64 `json:"paid_at,omitempty"` // Unix 秒
}
// AdminLotteryClaimsSummary 是 /claims/summary 的响应。overdue = 待用户填的
// pending_claim 中已过期的条数(不重叠 status=expired)。
type AdminLotteryClaimsSummary struct {
Crypto AdminLotteryClaimsStatusCount `json:"crypto"`
Physical AdminLotteryClaimsStatusCount `json:"physical"`
ManualOther AdminLotteryClaimsStatusCount `json:"manual_other"`
Overdue int64 `json:"overdue"`
}
// AdminLotteryClaimsStatusCount 是各类型的关键状态计数。
type AdminLotteryClaimsStatusCount struct {
Reviewing int64 `json:"reviewing"`
Paying int64 `json:"paying"`
}
// ---- Stage 3 后台抽奖记录(发放流水)--------------------------------------
// ListAdminLotteryDrawsRequest 列出抽奖记录,支持活动/用户/中奖/发放状态/奖品类型/时间窗过滤。
type ListAdminLotteryDrawsRequest struct {
ActivityId int64 `form:"activity_id,omitempty"`
UserId int64 `form:"user_id,omitempty"`
PrizeType string `form:"prize_type,omitempty"`
DispatchState string `form:"dispatch_state,omitempty"`
Win string `form:"win,omitempty"` // "1"=只看中奖, "0"=只看未中奖, ""=全部
From int64 `form:"from,omitempty"`
To int64 `form:"to,omitempty"`
Page int `form:"page"`
Size int `form:"size"`
}
// AdminLotteryDrawUser 抽奖记录里附带的用户简况。
type AdminLotteryDrawUser struct {
Id int64 `json:"id"`
Email string `json:"email,omitempty"`
}
// AdminLotteryDrawPrize 抽奖记录里的奖品快照(抽奖时刻冻结)。
type AdminLotteryDrawPrize struct {
Slot int `json:"slot"`
Type string `json:"type"`
Name string `json:"name"`
Config json.RawMessage `json:"config"`
}
// AdminLotteryDraw 是后台抽奖记录视图:谁、何时、中了什么、发放状态与结果。
type AdminLotteryDraw struct {
DrawId int64 `json:"draw_id"`
ActivityId int64 `json:"activity_id"`
User AdminLotteryDrawUser `json:"user"`
IsWin bool `json:"is_win"`
Prize *AdminLotteryDrawPrize `json:"prize,omitempty"`
DispatchState string `json:"dispatch_state"`
DispatchError string `json:"dispatch_error,omitempty"`
// GrantAmount / GrantMessage 来自 lottery_grant_ledger(自动奖发放结果):
// vpn_duration=天数,commission=分;message 如"已新建订阅并加 30 天"。
GrantAmount int64 `json:"grant_amount,omitempty"`
GrantMessage string `json:"grant_message,omitempty"`
DrawnAt int64 `json:"drawn_at"`
DispatchedAt int64 `json:"dispatched_at,omitempty"`
CreatedAt int64 `json:"created_at"`
}
// ListAdminLotteryDrawsResponse 分页。
type ListAdminLotteryDrawsResponse struct {
Total int64 `json:"total"`
List []AdminLotteryDraw `json:"list"`
}