feat(backend): license_purchases 增 amount_minor/currency/pay_url/renewed_to 列 + 存量回填(pay v2 金额口径)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,11 @@ type LicensePurchase struct {
|
||||
ShopID uint64 `gorm:"not null;index" json:"shop_id"`
|
||||
UserID uint64 `gorm:"not null" json:"user_id"`
|
||||
ProductBizCode string `gorm:"size:64;not null" json:"product_biz_code"`
|
||||
Amount string `gorm:"size:16" json:"amount"`
|
||||
AmountMinor int64 `gorm:"not null;default:0" json:"amount_minor"` // v2 口径:最小单位(CNY=分)
|
||||
Currency string `gorm:"size:8;not null;default:''" json:"currency"`
|
||||
PayURL string `gorm:"size:512" json:"pay_url,omitempty"` // v2 session payload.url,订单管理「继续支付」复用
|
||||
RenewedTo *time.Time `json:"renewed_to,omitempty"` // settle 续期后的授权到期日(订单流水「授权续期至」展示)
|
||||
Amount string `gorm:"size:16" json:"amount"` // Deprecated: v1 元字符串,只读兜底输出,观察一版后 DROP
|
||||
OutTradeNo string `gorm:"size:64;uniqueIndex:uk_out_trade_no" json:"out_trade_no"`
|
||||
Status string `gorm:"type:enum('pending','paid','failed');default:'pending';index" json:"status"`
|
||||
TradeNo string `gorm:"size:64" json:"trade_no,omitempty"` // 渠道交易号(支付宝/微信)
|
||||
|
||||
@@ -368,6 +368,21 @@ func entitle(tx *gorm.DB, shopID uint64, plan payPlan) error {
|
||||
}).Error
|
||||
}
|
||||
|
||||
// BackfillPurchaseAmountMinor 启动回填:v1 存量购买单 amount("2999.00") → amount_minor(299900)+CNY。
|
||||
// 幂等:只处理 amount_minor=0 且 amount 非空的行(参照 backfillPinyin 先例)。
|
||||
func BackfillPurchaseAmountMinor(db *gorm.DB) {
|
||||
var rows []model.LicensePurchase
|
||||
if err := db.Where("amount_minor = 0 AND amount <> ''").Find(&rows).Error; err != nil {
|
||||
return
|
||||
}
|
||||
for _, p := range rows {
|
||||
if c, err := toCents(p.Amount); err == nil && c > 0 {
|
||||
db.Model(&model.LicensePurchase{}).Where("id = ?", p.ID).
|
||||
Updates(map[string]any{"amount_minor": c, "currency": "CNY"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// amountEqual 金额按分归一比较("2999.00" == "2999" == "2999.0")。
|
||||
func amountEqual(a, b string) bool {
|
||||
ca, ea := toCents(a)
|
||||
|
||||
@@ -370,3 +370,30 @@ func TestStatus_ScopedToShop(t *testing.T) {
|
||||
_, err = svc.Status(other.ID, "yanmei-otn-7")
|
||||
assert.ErrorIs(t, err, ErrPurchaseNotFound, "跨店不可见")
|
||||
}
|
||||
|
||||
// ---------- v2 金额回填 ----------
|
||||
|
||||
func TestBackfillPurchaseAmountMinor(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PAY012")
|
||||
|
||||
p1 := createPendingPurchase(t, db, shop.ID, "annual_standard", "2999.00", "yanmei-backfill-1")
|
||||
p2 := createPendingPurchase(t, db, shop.ID, PromoBizCode, "1.00", "yanmei-backfill-2")
|
||||
// 已有 amount_minor>0 的行不应被覆盖
|
||||
p3 := createPendingPurchase(t, db, shop.ID, "monthly_pro", "599.00", "yanmei-backfill-3")
|
||||
require.NoError(t, db.Model(p3).Updates(map[string]any{"amount_minor": 123, "currency": "USD"}).Error)
|
||||
|
||||
BackfillPurchaseAmountMinor(db)
|
||||
|
||||
var got1, got2, got3 model.LicensePurchase
|
||||
require.NoError(t, db.First(&got1, p1.ID).Error)
|
||||
require.NoError(t, db.First(&got2, p2.ID).Error)
|
||||
require.NoError(t, db.First(&got3, p3.ID).Error)
|
||||
|
||||
assert.Equal(t, int64(299900), got1.AmountMinor)
|
||||
assert.Equal(t, "CNY", got1.Currency)
|
||||
assert.Equal(t, int64(100), got2.AmountMinor)
|
||||
assert.Equal(t, "CNY", got2.Currency)
|
||||
assert.Equal(t, int64(123), got3.AmountMinor, "已有 amount_minor>0 的行不被覆盖")
|
||||
assert.Equal(t, "USD", got3.Currency, "已有 amount_minor>0 的行不被覆盖")
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ func main() {
|
||||
// 幂等(新列为 0 才拷),旧列保留不再读写、观察一版后手动 DROP
|
||||
service.BackfillPricingColumns(db)
|
||||
|
||||
// pay v2 金额口径迁移:存量购买单 amount(元字符串) → amount_minor(分)+currency,
|
||||
// 幂等(amount_minor=0 才回填),旧列保留不再读写、观察一版后手动 DROP
|
||||
service.BackfillPurchaseAmountMinor(db)
|
||||
|
||||
// 启动会话/失败登录保留期清理任务(后台 goroutine)
|
||||
service.StartSessionCleanup(db, config.C.Session.RetentionDays)
|
||||
|
||||
|
||||
@@ -167,7 +167,11 @@ CREATE TABLE IF NOT EXISTS `license_purchases` (
|
||||
`shop_id` BIGINT UNSIGNED NOT NULL,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '下单管理员',
|
||||
`product_biz_code` VARCHAR(64) NOT NULL COMMENT '套餐稳定码(monthly_standard/annual_standard/monthly_pro/annual_pro)',
|
||||
`amount` VARCHAR(16) DEFAULT NULL COMMENT 'pay 下单回传金额,如 2999.00',
|
||||
`amount_minor` BIGINT NOT NULL DEFAULT 0 COMMENT 'v2 口径:最小单位金额(CNY=分)',
|
||||
`currency` VARCHAR(8) NOT NULL DEFAULT '' COMMENT 'v2 口径:币种,如 CNY',
|
||||
`pay_url` VARCHAR(512) DEFAULT NULL COMMENT 'v2 下单 session payload.url,订单管理「继续支付」复用',
|
||||
`renewed_to` DATETIME DEFAULT NULL COMMENT 'settle 续期后的授权到期日(订单流水「授权续期至」展示)',
|
||||
`amount` VARCHAR(16) DEFAULT NULL COMMENT 'Deprecated: v1 元字符串,如 2999.00,观察一版后 DROP',
|
||||
`out_trade_no` VARCHAR(64) DEFAULT NULL COMMENT 'pay 订单号',
|
||||
`status` ENUM('pending','paid','failed') NOT NULL DEFAULT 'pending',
|
||||
`trade_no` VARCHAR(64) DEFAULT NULL COMMENT '渠道交易号(支付宝/微信)',
|
||||
|
||||
@@ -168,6 +168,10 @@ func SetupTestDB() *gorm.DB {
|
||||
shop_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
product_biz_code TEXT NOT NULL,
|
||||
amount_minor INTEGER NOT NULL DEFAULT 0,
|
||||
currency TEXT NOT NULL DEFAULT '',
|
||||
pay_url TEXT,
|
||||
renewed_to DATETIME,
|
||||
amount TEXT,
|
||||
out_trade_no TEXT UNIQUE,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
|
||||
Reference in New Issue
Block a user