177 lines
7.8 KiB
Go
177 lines
7.8 KiB
Go
package model
|
|
|
|
import "time"
|
|
|
|
// ---- 状态枚举 ----
|
|
|
|
type OrderStatusV2 string
|
|
|
|
// 注:v1 model.OrderStatus 已占用 OrderPending/OrderPaid/OrderRefunded 三个标识符,
|
|
// 为免包内重名冲突,v2 状态枚举统一加 V2 后缀(与 AttemptStatus/RefundStatus 的独立命名空间不冲突,故那两组不加后缀)。
|
|
const (
|
|
OrderCreatedV2 OrderStatusV2 = "created"
|
|
OrderPendingV2 OrderStatusV2 = "pending"
|
|
OrderPaidV2 OrderStatusV2 = "paid"
|
|
OrderCanceledV2 OrderStatusV2 = "canceled"
|
|
OrderExpiredV2 OrderStatusV2 = "expired"
|
|
OrderRefundingV2 OrderStatusV2 = "refunding"
|
|
OrderPartRefundedV2 OrderStatusV2 = "partially_refunded"
|
|
OrderRefundedV2 OrderStatusV2 = "refunded"
|
|
)
|
|
|
|
type AttemptStatus string
|
|
|
|
const (
|
|
AttemptCreated AttemptStatus = "created"
|
|
AttemptPending AttemptStatus = "pending"
|
|
AttemptPaid AttemptStatus = "paid"
|
|
AttemptFailed AttemptStatus = "failed"
|
|
AttemptExpired AttemptStatus = "expired"
|
|
)
|
|
|
|
type RefundStatus string
|
|
|
|
const (
|
|
RefundRequested RefundStatus = "requested"
|
|
RefundProcessing RefundStatus = "processing"
|
|
RefundSucceeded RefundStatus = "succeeded"
|
|
RefundFailed RefundStatus = "failed"
|
|
RefundManualPending RefundStatus = "manual_pending" // crypto 自托管:待运营人工 sweep 退款
|
|
)
|
|
|
|
// Settled 报告订单是否「已真正收到过钱」(paid 及其后的退款态)。webhook 投递门禁用:
|
|
// 只有已结算订单的 outbox 才放行——payment 事件挡住崩溃窗口里的未付单,refund 事件
|
|
// 则因订单已 paid 过而正常放行(不会因订单转入退款态被卡)。
|
|
func (s OrderStatusV2) Settled() bool {
|
|
switch s {
|
|
case OrderPaidV2, OrderRefundingV2, OrderPartRefundedV2, OrderRefundedV2:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ---- 业务订单(购买账本)----
|
|
|
|
type OrderV2 struct {
|
|
Base
|
|
OutTradeNo string `gorm:"uniqueIndex;size:64;not null"`
|
|
MerchantID uint64 `gorm:"index"`
|
|
BizSystem string `gorm:"index;size:32"` // 接入方(pangolin/jiu),空=独立收款
|
|
BizRef string `gorm:"size:128"` // 接入方业务单号,回调原样带回
|
|
ProductID uint64 `gorm:"index"`
|
|
BizCode string `gorm:"index;size:64"` // 套餐码副本(下单时从 product 复制),回调带回业务方按码映射权益(设计 §5)
|
|
Subject string `gorm:"size:128"`
|
|
// 金额:int64 最小单位 + 币种(presentment/charge/settlement 三态在 charge 落地此处)
|
|
AmountMinor int64 `gorm:"not null"`
|
|
Currency string `gorm:"size:16;not null"`
|
|
Status OrderStatusV2 `gorm:"index;size:24;not null"`
|
|
// 折扣(业务授权的折后价,记账留痕)
|
|
BaseAmountMinor int64 `gorm:"default:0"`
|
|
DiscountMinor int64 `gorm:"default:0"`
|
|
CampaignID string `gorm:"size:64"`
|
|
DiscountReason string `gorm:"size:128"`
|
|
PaidAt *time.Time
|
|
ExpiresAt *time.Time // 整体购买窗口
|
|
// Disputed 打标位(P8 Task6):渠道拒付(charge.dispute.created)命中该单时置 true,
|
|
// 仅留痕、不改状态机(§6 决策:不自动回收权益,业务方按 chargeback.received 自行冲正)。
|
|
Disputed bool `gorm:"default:false"`
|
|
}
|
|
|
|
// ---- 支付尝试(收款账本)----
|
|
|
|
type Attempt struct {
|
|
Base
|
|
OutTradeNo string `gorm:"index;size:64;not null"`
|
|
Channel string `gorm:"index;size:32;not null;uniqueIndex:uq_attempt_provider"` // alipay/wechat/crypto/stripe/...
|
|
AccountID string `gorm:"index;size:64"` // 选中的收款账户
|
|
Provider string `gorm:"size:32"`
|
|
ProviderRef string `gorm:"size:128;uniqueIndex:uq_attempt_provider"` // 渠道单号
|
|
RenderType string `gorm:"size:24"` // redirect/qr/crypto_address/...
|
|
AmountMinor int64 `gorm:"not null"`
|
|
Currency string `gorm:"size:16;not null"`
|
|
Status AttemptStatus `gorm:"index;size:16;not null"`
|
|
ExpiresAt *time.Time // 本次尝试超时
|
|
PaidAt *time.Time
|
|
}
|
|
|
|
// ---- 收款账户(同一渠道多账户)----
|
|
|
|
type Account struct {
|
|
Base
|
|
AccountID string `gorm:"uniqueIndex;size:64;not null"` // 逻辑账户标识(配置里声明)
|
|
Channel string `gorm:"index;size:32;not null"`
|
|
Weight int `gorm:"default:1"`
|
|
Enabled bool `gorm:"default:true"`
|
|
DailyLimit int64 `gorm:"default:0"` // 0=不限
|
|
Region string `gorm:"size:16"` // cn/global/...
|
|
Subject string `gorm:"size:128"` // 收款主体
|
|
// 凭证不落库明文:仅存 env 变量名引用,真值运行时从 env 取(见 Task 4)
|
|
CredentialEnvPrefix string `gorm:"size:64"`
|
|
}
|
|
|
|
// ---- 退款子实体 ----
|
|
|
|
type Refund struct {
|
|
Base
|
|
RefundID string `gorm:"uniqueIndex;size:64;not null"`
|
|
OutTradeNo string `gorm:"index;size:64;not null"`
|
|
AttemptProviderRef string `gorm:"size:128"` // 针对哪笔支付,原路退
|
|
AmountMinor int64 `gorm:"not null"`
|
|
Currency string `gorm:"size:16;not null"`
|
|
Reason string `gorm:"size:256"`
|
|
Status RefundStatus `gorm:"index;size:16;not null"`
|
|
ProviderRefundRef string `gorm:"size:128"`
|
|
InitiatedBy string `gorm:"size:16"` // business/platform
|
|
CompletedAt *time.Time
|
|
}
|
|
|
|
// ---- 订阅(recurring)----
|
|
|
|
type SubStatus string
|
|
|
|
const (
|
|
SubActive SubStatus = "active" // 已激活,正常续费
|
|
SubPastDue SubStatus = "past_due" // 某期扣款失败,待恢复(invoice.payment_failed)
|
|
SubCanceled SubStatus = "canceled" // 已取消(主动/网关删除),终态
|
|
)
|
|
|
|
// Subscription 是"同一 entitlement 的跨期账本"(设计 §5.1)。单笔层(OrderV2/Attempt)
|
|
// 与订阅层解耦:每期扣款仍落一张 renewal OrderV2,Subscription 只维护状态机 + 续费锚点。
|
|
type Subscription struct {
|
|
Base
|
|
SubID string `gorm:"uniqueIndex;size:64;not null"` // pay 生成的逻辑订阅号
|
|
OutTradeNo string `gorm:"index;size:64;not null"` // 首购 order(诞生订阅那笔)
|
|
MerchantID uint64 `gorm:"index"`
|
|
BizSystem string `gorm:"index;size:32"`
|
|
BizRef string `gorm:"size:128"`
|
|
BizCode string `gorm:"index;size:64"` // 套餐码副本,续费事件带回
|
|
Channel string `gorm:"index;size:32;not null;uniqueIndex:uq_sub_provider"`
|
|
ProviderSubRef string `gorm:"size:128;not null;uniqueIndex:uq_sub_provider"` // 渠道订阅号(stripe sub id)
|
|
RecurringKind string `gorm:"size:24"` // gateway_scheduled/token_offsession/...
|
|
AmountMinor int64 `gorm:"not null"`
|
|
Currency string `gorm:"size:16;not null"`
|
|
Status SubStatus `gorm:"index;size:16;not null"`
|
|
CurrentPeriodEnd *time.Time
|
|
CanceledAt *time.Time
|
|
}
|
|
|
|
// ---- 拒付(chargeback,P8 Task6)----
|
|
|
|
// Chargeback 记录渠道拒付(设计 §6「钱到账不可逆」的例外形态:卡组织裁定退单)。只有
|
|
// Stripe(卡)有此语义;alipay/微信本轮无拒付流,crypto 收款不可逆、永无 chargeback——
|
|
// 三者均不实现 Capabilities 里任何 chargeback 相关声明(P8 未新增该 capability 位,
|
|
// 靠 EventKind 是否产出 EventChargeback 天然区分)。
|
|
// 每次拒付(含重投)按 DisputeRef 幂等落一行留痕;是否成功定位/转发给业务方是另一回事
|
|
// (见 gateway.recordChargeback),Chargeback 表本身对"能否定位业务单"保持中立、全记录。
|
|
type Chargeback struct {
|
|
Base
|
|
DisputeRef string `gorm:"uniqueIndex;size:128;not null"` // 渠道拒付号(stripe dp_...)
|
|
OutTradeNo string `gorm:"index;size:64"` // 解析出的原单号(订阅拒付可能为空)
|
|
Channel string `gorm:"index;size:32;not null"`
|
|
ProviderPaymentRef string `gorm:"size:128"` // 关联 PaymentIntent id
|
|
AmountMinor int64 `gorm:"not null"`
|
|
Currency string `gorm:"size:16;not null"`
|
|
Reason string `gorm:"size:64"`
|
|
Status string `gorm:"size:24"` // 渠道拒付状态快照(needs_response/...)
|
|
}
|