168 lines
5.4 KiB
Go
168 lines
5.4 KiB
Go
package payment
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"hfb_sys/backend/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const qualityOwnerPayBizType = "quality_owner_pay"
|
|
|
|
// StartQualityOwner 创建优质号主保证金在线支付单,支付成功后自动开通资格并冻结保证金。
|
|
func (r *Repository) StartQualityOwner(ctx context.Context, userID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) {
|
|
if r.qualityOwnerRepo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
req.PayWay = normalizePayWay(req.PayWay)
|
|
payment, err := r.prepareQualityOwnerPayment(ctx, userID, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.startPreparedPayment(ctx, payment, "优质号主保证金", payment.OrderNo, clientIP)
|
|
}
|
|
|
|
func (r *Repository) prepareQualityOwnerPayment(ctx context.Context, userID uint64, req StartPaymentRequest) (*model.PaymentOrder, error) {
|
|
amountCent, active, err := r.qualityOwnerRepo.PaymentAmount(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if active || amountCent <= 0 {
|
|
return nil, ErrPaymentCannotStart
|
|
}
|
|
runtimeConfig, err := r.defaultRuntimeConfig(ctx, req.PayWay)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var paymentID uint64
|
|
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var user model.User
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Select("id").
|
|
First(&user, userID).Error; err != nil {
|
|
return err
|
|
}
|
|
var existing model.PaymentOrder
|
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("user_id = ? AND biz_type = ? AND biz_id = ?", userID, qualityOwnerPayBizType, userID).
|
|
Order("id DESC").
|
|
First(&existing).Error
|
|
if err == nil {
|
|
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
|
|
existing.PaymentConfigID = firstNonZero(existing.PaymentConfigID, runtimeConfig.ID)
|
|
existing.Provider = firstNonEmpty(existing.Provider, runtimeConfig.Provider)
|
|
existing.MerchantID = firstNonEmpty(existing.MerchantID, runtimeConfig.MerchantID)
|
|
if existing.Provider == "mock" && existing.ProviderOrderID == "" {
|
|
existing.ProviderOrderID = "MOCK" + existing.ThirdOrderID
|
|
}
|
|
if err := tx.Save(&existing).Error; err != nil {
|
|
return err
|
|
}
|
|
paymentID = existing.ID
|
|
return nil
|
|
}
|
|
} else if err != gorm.ErrRecordNotFound {
|
|
return err
|
|
}
|
|
payment, err := newQualityOwnerPayment(userID, amountCent, req, *runtimeConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Create(&payment).Error; err != nil {
|
|
return err
|
|
}
|
|
paymentID = payment.ID
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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 {
|
|
return model.PaymentOrder{}, err
|
|
}
|
|
orderNo := fmt.Sprintf("QO%d", userID)
|
|
payment := model.PaymentOrder{
|
|
PaymentNo: paymentNo,
|
|
OrderID: 0,
|
|
OrderNo: orderNo,
|
|
BizID: userID,
|
|
UserID: userID,
|
|
PaymentConfigID: runtimeConfig.ID,
|
|
Provider: runtimeConfig.Provider,
|
|
MerchantID: runtimeConfig.MerchantID,
|
|
ThirdOrderID: paymentNo,
|
|
PayWay: firstNonEmpty(req.PayWay, runtimeConfig.PayWay, "ZFBZF"),
|
|
JSPayFlag: firstNonEmpty(req.JSPayFlag, runtimeConfig.JSPayFlag, "2"),
|
|
AmountCent: amountCent,
|
|
BizType: qualityOwnerPayBizType,
|
|
Status: "created",
|
|
}
|
|
if runtimeConfig.isMockMode() {
|
|
payment.ProviderOrderID = "MOCK" + paymentNo
|
|
payment.TDCode = "mock://payment/pay/" + paymentNo
|
|
}
|
|
return payment, nil
|
|
}
|
|
|
|
func (r *Repository) QueryQualityOwner(ctx context.Context, userID uint64) (*PaymentDTO, error) {
|
|
var payment model.PaymentOrder
|
|
if err := r.db.WithContext(ctx).
|
|
Where("user_id = ? AND biz_type = ? AND biz_id = ?", userID, qualityOwnerPayBizType, userID).
|
|
Order("id DESC").
|
|
First(&payment).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, ErrPaymentNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
if isOrderPaymentTerminalStatus(payment.Status) {
|
|
dto := toDTO(payment)
|
|
return &dto, nil
|
|
}
|
|
runtimeConfig, err := r.runtimeConfigForPayment(ctx, &payment)
|
|
if err != nil {
|
|
return nil, ErrPaymentUnavailable
|
|
}
|
|
if runtimeConfig.isMockMode() {
|
|
dto := toDTO(payment)
|
|
return &dto, nil
|
|
}
|
|
if runtimeConfig.Channel == nil {
|
|
return nil, ErrPaymentUnavailable
|
|
}
|
|
resp, err := runtimeConfig.Channel.QueryPayment(ctx, payment.ThirdOrderID, payment.ProviderOrderID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := r.applyChannelStatus(ctx, &payment, resp.Status, resp.PayTime, resp.Raw, channelSourceQuery); err != nil {
|
|
return nil, err
|
|
}
|
|
latest, err := r.findPaymentByID(ctx, payment.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dto := toDTO(*latest)
|
|
return &dto, nil
|
|
}
|