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 }