feat(v2): Order/Attempt/Account/Refund 模型 + 状态枚举 + :memory: 测试约定
v2 OrderStatus 三个值(Pending/Paid/Refunded)与 v1 model.OrderStatus 撞名, 统一加 V2 后缀(OrderPendingV2 等)规避包内重名;AttemptStatus/RefundStatus 无此冲突,原样保留。
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenTestDB opens an in-memory SQLite DB with the v2 schema migrated.
|
||||||
|
// Shared by all package tests (pay has no prior DB test harness — this is it).
|
||||||
|
func OpenTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"),
|
||||||
|
&gorm.Config{Logger: logger.Default.LogMode(logger.Silent), TranslateError: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open test db: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&OrderV2{}, &Attempt{}, &Account{}, &Refund{}); err != nil {
|
||||||
|
t.Fatalf("migrate: %v", err)
|
||||||
|
}
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||||
|
return db
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- 业务订单(购买账本)----
|
||||||
|
|
||||||
|
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"`
|
||||||
|
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 // 整体购买窗口
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 支付尝试(收款账本)----
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package model_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wangjia/pay/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV2Migrate(t *testing.T) {
|
||||||
|
db := model.OpenTestDB(t)
|
||||||
|
|
||||||
|
o := model.OrderV2{
|
||||||
|
OutTradeNo: "PAY-1", MerchantID: 1, BizSystem: "pangolin", BizRef: "u-1",
|
||||||
|
ProductID: 3, Subject: "pro-year", AmountMinor: 29990000, Currency: "USDT",
|
||||||
|
Status: model.OrderPendingV2,
|
||||||
|
}
|
||||||
|
if err := db.Create(&o).Error; err != nil {
|
||||||
|
t.Fatalf("create order: %v", err)
|
||||||
|
}
|
||||||
|
a := model.Attempt{
|
||||||
|
OutTradeNo: "PAY-1", Channel: "crypto", AccountID: "usdt-a1", ProviderRef: "P-9",
|
||||||
|
RenderType: "crypto_address", AmountMinor: 29990000, Currency: "USDT",
|
||||||
|
Status: model.AttemptPending,
|
||||||
|
}
|
||||||
|
if err := db.Create(&a).Error; err != nil {
|
||||||
|
t.Fatalf("create attempt: %v", err)
|
||||||
|
}
|
||||||
|
var got model.OrderV2
|
||||||
|
if err := db.Where("out_trade_no = ?", "PAY-1").First(&got).Error; err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
if got.AmountMinor != 29990000 || got.Status != model.OrderPendingV2 {
|
||||||
|
t.Fatalf("got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,7 @@ func autoMigrate(db *gorm.DB) {
|
|||||||
&model.Order{},
|
&model.Order{},
|
||||||
&model.NotifyLog{},
|
&model.NotifyLog{},
|
||||||
&model.BizNotifyLog{},
|
&model.BizNotifyLog{},
|
||||||
|
&model.OrderV2{}, &model.Attempt{}, &model.Account{}, &model.Refund{}, // v2
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatalf("自动迁移失败: %v", err)
|
log.Fatalf("自动迁移失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user