Files
hi-server/internal/types/lottery.go
T
shanshanzhong147 58c346abec 重构(#4): 抽奖奖品改/删 id 走 URL path(RESTful)
- PUT /admin/lottery/prizes/:id、DELETE /admin/lottery/prizes/:id
- handler 从 c.Param("id") 取 id;types 用 path:"id"
- 同步 apis/admin/lottery.api 定义

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 19:45:23 -07:00

391 lines
16 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"`
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"`
}