package payment import ( "context" "time" "hfb_sys/backend/internal/model" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" ) const bizTypeMohongPay = "mohong_pay" // StartMohong 发起撞车订单支付。 func (r *Repository) StartMohong(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.prepareMohongPayment(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) 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 } func (r *Repository) prepareMohongPayment(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (*model.PaymentOrder, *model.MohongOrder, error) { var paymentID uint64 var orderRow model.MohongOrder err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var row model.MohongOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("id = ? AND user_id = ?", orderID, userID). First(&row).Error; err != nil { return err } if row.Status != model.MohongOrderStatusPendingPayment { return ErrPaymentCannotStart } if row.AmountCent <= 0 { return ErrPaymentCannotStart } var existing model.PaymentOrder err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("order_id = ? AND biz_type = ?", row.ID, bizTypeMohongPay). 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 = row.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 := newMohongPayment(row, 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 } func newMohongPayment(row model.MohongOrder, 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.UserID, 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: row.AmountCent, BizType: bizTypeMohongPay, Status: "created", } if runtimeConfig.isMockMode() { payment.ProviderOrderID = "MOCK" + paymentNo payment.TDCode = "mock://payment/pay/" + paymentNo } return payment, nil } // QueryMohong 查询撞车订单支付状态。 func (r *Repository) QueryMohong(ctx context.Context, userID uint64, orderID uint64) (*PaymentDTO, error) { var payment model.PaymentOrder if err := r.db.WithContext(ctx). Where("order_id = ? AND user_id = ? AND biz_type = ?", orderID, userID, bizTypeMohongPay). 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 }