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 } 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("支付下单请求失败", 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("支付下单被渠道拒绝", 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("支付下单完成", 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 }