Files
hfb_sys/backend/internal/modules/payment/payment_start.go
T

264 lines
8.8 KiB
Go

package payment
import (
"context"
"encoding/json"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/timeutil"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// Start 发起订单支付,并按请求支付方式选择对应默认渠道配置。
func (r *Repository) Start(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) {
req.PayWay = normalizePayWay(req.PayWay)
defaultConfig, err := r.defaultRuntimeConfig(ctx, req.PayWay)
if err != nil {
return nil, ErrPaymentUnavailable
}
payment, orderRow, err := r.preparePayment(ctx, userID, orderID, req, *defaultConfig)
if err != nil {
return nil, err
}
runtimeConfig, err := r.runtimeConfigForPayment(ctx, payment)
if err != nil {
return nil, ErrPaymentUnavailable
}
if payment.Status == "paid" {
r.recordConfigUsage(ctx, runtimeConfig, payment)
dto := toDTO(*payment)
return &dto, nil
}
if runtimeConfig.isMockMode() {
if err := r.confirmPaid(ctx, payment, "2", time.Now(), map[string]string{
"mock": "true",
"third_order_id": payment.ThirdOrderID,
"leshua_order_id": payment.ProviderOrderID,
"status": "2",
}, channelSourceMock); err != nil {
return nil, err
}
latest, err := r.findPaymentByID(ctx, payment.ID)
if err != nil {
return nil, err
}
r.recordConfigUsage(ctx, runtimeConfig, latest)
dto := toDTO(*latest)
return &dto, nil
}
if payment.Status == "paying" && (payment.TDCode != "" || payment.JSPayURL != "" || payment.JSPayInfo != "") {
r.recordConfigUsage(ctx, runtimeConfig, payment)
dto := toDTO(*payment)
return &dto, nil
}
if runtimeConfig.Channel == nil {
_ = r.markPaymentFailed(ctx, payment.ID, nil, "payment channel unavailable")
return nil, ErrPaymentUnavailable
}
r.log().Info("payment start", paymentLogFields(ctx, appendFields(
paymentOrderFields(payment),
runtimeConfigFields(runtimeConfig),
)...,
)...)
resp, err := runtimeConfig.Channel.CreatePayment(ctx, channelCreatePaymentRequest{
ThirdOrderID: payment.ThirdOrderID,
AmountCent: payment.AmountCent,
PayWay: payment.PayWay,
JSPayFlag: payment.JSPayFlag,
NotifyURL: runtimeConfig.NotifyURL,
JumpURL: runtimeConfig.JumpURL,
ClientIP: clientIP,
Body: "租号订单 " + orderRow.OrderNo,
Attach: orderRow.OrderNo,
})
if err != nil {
_ = r.markPaymentFailed(ctx, payment.ID, nil, err.Error())
r.log().Warn("payment request failed", paymentLogFields(ctx, appendFields(
paymentOrderFields(payment),
runtimeConfigFields(runtimeConfig),
[]zap.Field{zap.Error(err)},
)...,
)...)
return nil, err
}
if !resp.OK {
_ = r.markPaymentFailed(ctx, payment.ID, resp.Raw, resp.ErrorMessage)
r.log().Warn("payment rejected", paymentLogFields(ctx, appendFields(
paymentOrderFields(payment),
runtimeConfigFields(runtimeConfig),
[]zap.Field{
zap.String("channel_code", firstNonEmpty(resp.Raw["code"], resp.Raw["resp_code"], resp.Raw["result_code"])),
zap.String("channel_message", resp.ErrorMessage),
zap.Strings("raw_response_keys", stringMapKeys(resp.Raw)),
},
)...,
)...)
return nil, ErrPaymentUnavailable
}
if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{
"status": "paying",
"provider_order_id": resp.ProviderOrderID,
"pay_way": firstNonEmpty(resp.PayWay, payment.PayWay),
"td_code": resp.TDCode,
"jspay_url": resp.JSPayURL,
"jspay_info": resp.JSPayInfo,
"raw_request": jsonMap(resp.RawRequest),
"raw_response": jsonMap(withRawSource(resp.Raw, channelSourceCreate)),
}).Error; err != nil {
return nil, err
}
latest, err := r.findPaymentByID(ctx, payment.ID)
if err != nil {
return nil, err
}
r.recordConfigUsage(ctx, runtimeConfig, latest)
r.log().Info("payment result", paymentLogFields(ctx, appendFields(
paymentOrderFields(latest),
runtimeConfigFields(runtimeConfig),
)...,
)...)
dto := toDTO(*latest)
return &dto, nil
}
// preparePayment 创建或复用订单支付单,并写入本次命中的支付配置 ID。
func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (*model.PaymentOrder, *model.RentalOrder, error) {
var paymentID uint64
var orderRow model.RentalOrder
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var row model.RentalOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ? AND renter_id = ?", orderID, userID).
First(&row).Error; err != nil {
return err
}
if row.Status != "pending_payment" {
return ErrPaymentCannotStart
}
amountCent := row.RentAmountCent + row.DepositAmountCent
if amountCent <= 0 {
return ErrPaymentCannotStart
}
var existing model.PaymentOrder
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id = ? AND biz_type = ?", row.ID, "order_pay").
Order("id DESC").
First(&existing).Error
if err == nil {
if canReuseOrderPayment(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
orderRow = row
return nil
}
} else if err != gorm.ErrRecordNotFound {
return err
}
payment, err := newOrderPayment(row, amountCent, req, runtimeConfig)
if err != nil {
return err
}
if err := tx.Create(&payment).Error; err != nil {
return err
}
paymentID = payment.ID
orderRow = row
return nil
})
if err != nil {
return nil, nil, err
}
payment, err := r.findPaymentByID(ctx, paymentID)
if err != nil {
return nil, nil, err
}
return payment, &orderRow, nil
}
// canReuseOrderPayment 判断旧支付单是否还能复用,支付方式或配置不同则必须重新下单。
func canReuseOrderPayment(payment model.PaymentOrder, runtimeConfig runtimePaymentConfig) bool {
if payment.Status == "paid" {
return true
}
if payment.Status != "created" && payment.Status != "paying" {
return false
}
if payment.Provider != "" && runtimeConfig.Provider != "" && payment.Provider != runtimeConfig.Provider {
return false
}
if payment.MerchantID != "" && runtimeConfig.MerchantID != "" && payment.MerchantID != runtimeConfig.MerchantID {
return false
}
if payment.PaymentConfigID != 0 && runtimeConfig.ID != 0 && payment.PaymentConfigID != runtimeConfig.ID {
return false
}
if payment.PayWay != "" && runtimeConfig.PayWay != "" && normalizePayWay(payment.PayWay) != normalizePayWay(runtimeConfig.PayWay) {
return false
}
if payment.Status == "paying" && paymentCashierExpired(payment) {
return false
}
return true
}
// paymentCashierExpired 判断渠道收银台是否已过期,过期后不再复用支付单。
func paymentCashierExpired(payment model.PaymentOrder) bool {
if len(payment.RawRequest) == 0 {
return false
}
var raw map[string]string
if err := json.Unmarshal(payment.RawRequest, &raw); err != nil {
return false
}
deadline := parseChannelTime(raw["order_efficient_time"])
if deadline == nil {
return false
}
return !timeutil.ShanghaiNow().Before(*deadline)
}
// newOrderPayment 创建新的订单支付单,并固化支付方式和支付配置 ID。
func newOrderPayment(row model.RentalOrder, amountCent int64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (model.PaymentOrder, error) {
paymentNo, err := newPaymentNo()
if err != nil {
return model.PaymentOrder{}, err
}
payment := model.PaymentOrder{
PaymentNo: paymentNo,
OrderID: row.ID,
OrderNo: row.OrderNo,
UserID: row.RenterID,
PaymentConfigID: runtimeConfig.ID,
Provider: runtimeConfig.Provider,
MerchantID: runtimeConfig.MerchantID,
ThirdOrderID: paymentNo,
ProviderOrderID: "",
PayWay: firstNonEmpty(req.PayWay, runtimeConfig.PayWay, "ZFBZF"),
JSPayFlag: firstNonEmpty(req.JSPayFlag, runtimeConfig.JSPayFlag, "2"),
AmountCent: amountCent,
BizType: "order_pay",
Status: "created",
}
if runtimeConfig.isMockMode() {
payment.ProviderOrderID = "MOCK" + paymentNo
payment.TDCode = "mock://payment/pay/" + paymentNo
}
return payment, nil
}