feat: 完善商品筛选、发布配置与优质号主流程

This commit is contained in:
yml2213
2026-11-05 19:00:37 +08:00
parent 88beae4c96
commit f01329e049
19 changed files with 1256 additions and 180 deletions
@@ -52,7 +52,7 @@ func (r *Repository) prepareQualityOwnerPayment(ctx context.Context, userID uint
Order("id DESC").
First(&existing).Error
if err == nil {
if canReuseOrderPayment(existing, *runtimeConfig) {
if canReuseQualityOwnerPayment(existing, *runtimeConfig) {
existing.PayWay = firstNonEmpty(req.PayWay, existing.PayWay, runtimeConfig.PayWay, "ZFBZF")
existing.JSPayFlag = firstNonEmpty(req.JSPayFlag, existing.JSPayFlag, runtimeConfig.JSPayFlag, "2")
existing.AmountCent = amountCent
@@ -87,6 +87,15 @@ func (r *Repository) prepareQualityOwnerPayment(ctx context.Context, userID uint
return r.findPaymentByID(ctx, paymentID)
}
// canReuseQualityOwnerPayment 仅复用尚未完成的有效支付单。
// 已支付单属于上一轮开通周期,取消后必须重新创建支付单,避免“旧单已支付”被误当成新一轮开通。
func canReuseQualityOwnerPayment(payment model.PaymentOrder, runtimeConfig runtimePaymentConfig) bool {
if payment.Status == "paid" {
return false
}
return canReuseOrderPayment(payment, runtimeConfig)
}
func newQualityOwnerPayment(userID uint64, amountCent int64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (model.PaymentOrder, error) {
paymentNo, err := newPaymentNo()
if err != nil {
@@ -1,11 +1,13 @@
package payment
import (
"errors"
"testing"
"time"
"hfb_sys/backend/internal/database"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/paymentconfig"
"hfb_sys/backend/internal/modules/qualityowner"
)
@@ -95,3 +97,168 @@ func TestNewQualityOwnerPaymentUsesBizIDAndMockCashier(t *testing.T) {
t.Fatalf("mock 支付单未生成收银台: %+v", payment)
}
}
func TestCanReuseQualityOwnerPaymentRejectsPaidActivation(t *testing.T) {
config := runtimePaymentConfig{
Provider: "leshua",
MerchantID: "merchant-1",
PayWay: "ZFBZF",
}
paid := model.PaymentOrder{
Status: "paid",
Provider: "leshua",
MerchantID: "merchant-1",
PayWay: "ZFBZF",
}
if canReuseQualityOwnerPayment(paid, config) {
t.Fatal("已完成的上轮保证金支付单不应被复用")
}
paying := paid
paying.Status = "paying"
if !canReuseQualityOwnerPayment(paying, config) {
t.Fatal("同一配置下尚未完成的支付单应继续复用")
}
}
func TestQualityOwnerCanReactivateAfterCancelAndOldPaymentCannotReactivate(t *testing.T) {
db := setupPaymentTestDB(t)
user := model.User{
Phone: "13900009402",
RealnameStatus: "verified",
Status: "active",
}
if err := db.Create(&user).Error; err != nil {
t.Fatalf("创建用户失败: %v", err)
}
if err := db.Create(&model.WalletAccount{
UserID: user.ID,
Status: "active",
}).Error; err != nil {
t.Fatalf("创建钱包失败: %v", err)
}
config := model.PaymentMerchantConfig{
Name: "优质号主模拟支付",
Provider: "mock",
MerchantID: "QUALITY_OWNER_MOCK",
PayWay: "ZFBZF",
IsDefault: true,
Status: "active",
Environment: "sandbox",
}
if err := db.Create(&config).Error; err != nil {
t.Fatalf("创建支付配置失败: %v", err)
}
qualityOwnerRepo := qualityowner.NewRepository(db)
paymentRepo := NewRepository(
db,
paymentconfig.NewRepository(db, &paymentconfig.MockEncryptor{}),
nil,
WithQualityOwnerRepo(qualityOwnerRepo),
)
first, err := paymentRepo.StartQualityOwner(t.Context(), user.ID, StartPaymentRequest{PayWay: "ZFBZF"}, "")
if err != nil {
t.Fatalf("首次在线开通失败: %v", err)
}
if !first.Paid {
t.Fatalf("首次支付状态 = %q, want paid", first.Status)
}
status, err := qualityOwnerRepo.Status(t.Context(), user.ID)
if err != nil {
t.Fatalf("读取首次开通状态失败: %v", err)
}
if !status.Active {
t.Fatal("首次支付后应开通优质号主")
}
status, err = qualityOwnerRepo.Cancel(t.Context(), user.ID, qualityowner.AuditMeta{})
if err != nil {
t.Fatalf("取消优质号主失败: %v", err)
}
if status.Active {
t.Fatal("取消后不应保持优质号主身份")
}
var firstPayment model.PaymentOrder
if err := db.First(&firstPayment, first.ID).Error; err != nil {
t.Fatalf("读取首次支付单失败: %v", err)
}
if err := paymentRepo.confirmPaid(
t.Context(),
&firstPayment,
"2",
time.Now(),
map[string]string{"status": "2", "third_order_id": firstPayment.ThirdOrderID},
channelSourceMock,
); err != nil {
t.Fatalf("重放旧支付回调失败: %v", err)
}
status, err = qualityOwnerRepo.Status(t.Context(), user.ID)
if err != nil {
t.Fatalf("读取旧回调后状态失败: %v", err)
}
if status.Active {
t.Fatal("取消后重放旧支付回调不应重新开通优质号主")
}
second, err := paymentRepo.StartQualityOwner(t.Context(), user.ID, StartPaymentRequest{PayWay: "ZFBZF"}, "")
if err != nil {
t.Fatalf("取消后再次在线开通失败: %v", err)
}
if !second.Paid {
t.Fatalf("第二次支付状态 = %q, want paid", second.Status)
}
if second.ID == first.ID || second.PaymentNo == first.PaymentNo {
t.Fatalf("第二次开通复用了旧支付单: first=%+v second=%+v", first, second)
}
status, err = qualityOwnerRepo.Status(t.Context(), user.ID)
if err != nil {
t.Fatalf("读取第二次开通状态失败: %v", err)
}
if !status.Active || status.DepositSource != qualityowner.DepositSourcePaid {
t.Fatalf("第二次在线支付后状态不正确: %+v", status)
}
var paymentCount int64
if err := db.Model(&model.PaymentOrder{}).
Where("user_id = ? AND biz_type = ?", user.ID, qualityOwnerPayBizType).
Count(&paymentCount).Error; err != nil {
t.Fatalf("统计保证金支付单失败: %v", err)
}
if paymentCount != 2 {
t.Fatalf("保证金支付单数量 = %d, want 2", paymentCount)
}
var duplicateFrozenCount int64
if err := db.Model(&model.WalletLedger{}).
Where(
"user_id = ? AND biz_type = ? AND biz_no = ? AND direction = ? AND balance_type = ?",
user.ID,
"quality_owner_deposit",
first.PaymentNo,
"in",
"frozen",
).
Count(&duplicateFrozenCount).Error; err != nil {
t.Fatalf("统计首次保证金冻结流水失败: %v", err)
}
if duplicateFrozenCount != 1 {
t.Fatalf("首次支付冻结流水数量 = %d, want 1", duplicateFrozenCount)
}
}
func TestConfirmQualityOwnerPaymentRejectsEmptyPaymentNo(t *testing.T) {
db := setupPaymentTestDB(t)
repo := NewRepository(db, nil, nil, WithQualityOwnerRepo(qualityowner.NewRepository(db)))
payment := model.PaymentOrder{
BizType: qualityOwnerPayBizType,
UserID: 1,
AmountCent: 20_000,
}
err := repo.confirmPaid(t.Context(), &payment, "2", time.Now(), nil, channelSourceMock)
if !errors.Is(err, qualityowner.ErrInvalidUser) {
t.Fatalf("空支付单号 error = %v, want quality owner invalid user", err)
}
}