新增摸大红业务并修复后台链接按钮白字
支持分类筛选、搜索、下单支付建群与固定二维码;合并迁移种子数据;后台表格编辑/详情链接恢复主题色可见。
This commit is contained in:
@@ -45,16 +45,31 @@ func isOrderPaymentTerminalStatus(status string) bool {
|
||||
}
|
||||
func (r *Repository) confirmPaid(ctx context.Context, payment *model.PaymentOrder, status string, paidAt time.Time, raw map[string]string, source string) error {
|
||||
var newConvID uint64
|
||||
var notifyFn func(uint64)
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if payment.OrderID != 0 {
|
||||
if r.orderRepo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
switch payment.BizType {
|
||||
case "mohong_pay":
|
||||
if r.mohongRepo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
convID, err := r.mohongRepo.ConfirmPaidFromChannelTx(tx, payment.OrderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConvID = convID
|
||||
notifyFn = r.mohongRepo.NotifyNewConversation
|
||||
default:
|
||||
if r.orderRepo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
convID, err := r.orderRepo.ConfirmPaidFromChannelTx(tx, payment.OrderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConvID = convID
|
||||
notifyFn = r.orderRepo.NotifyNewConversation
|
||||
}
|
||||
convID, err := r.orderRepo.ConfirmPaidFromChannelTx(tx, payment.OrderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConvID = convID
|
||||
}
|
||||
|
||||
var latest model.PaymentOrder
|
||||
@@ -75,8 +90,8 @@ func (r *Repository) confirmPaid(ctx context.Context, payment *model.PaymentOrde
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newConvID > 0 && r.orderRepo != nil {
|
||||
r.orderRepo.NotifyNewConversation(newConvID)
|
||||
if newConvID > 0 && notifyFn != nil {
|
||||
notifyFn(newConvID)
|
||||
}
|
||||
if latest, err := r.findPaymentByID(ctx, payment.ID); err == nil {
|
||||
if runtimeConfig, err := r.runtimeConfigForPayment(ctx, latest); err == nil {
|
||||
|
||||
@@ -91,6 +91,26 @@ func (h *Handler) Start(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) StartMohong(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
orderID, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req StartPaymentRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
item, err := h.service.StartMohong(c.Request.Context(), userID, orderID, req, c.ClientIP())
|
||||
if err != nil {
|
||||
writePaymentError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) Query(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
@@ -109,6 +129,24 @@ func (h *Handler) Query(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) QueryMohong(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
orderID, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item, err := h.service.QueryMohong(c.Request.Context(), userID, orderID)
|
||||
if err != nil {
|
||||
writePaymentError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) QueryRefundStatus(c *gin.Context) {
|
||||
orderID, ok := parseID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
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
|
||||
}
|
||||
|
||||
r.log().Info("mohong 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("mohong 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)
|
||||
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)
|
||||
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
|
||||
}
|
||||
@@ -13,10 +13,17 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MohongOrderConfirmer 摸大红订单支付确认接口,避免 payment 与 mohong 循环依赖。
|
||||
type MohongOrderConfirmer interface {
|
||||
ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint64, error)
|
||||
NotifyNewConversation(conversationID uint64)
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
configRepo *paymentconfig.Repository
|
||||
orderRepo *order.Repository
|
||||
mohongRepo MohongOrderConfirmer
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -82,6 +89,13 @@ func WithLogger(logger *zap.Logger) RepositoryOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithMohongRepo 注入摸大红订单确认器。
|
||||
func WithMohongRepo(repo MohongOrderConfirmer) RepositoryOption {
|
||||
return func(r *Repository) {
|
||||
r.mohongRepo = repo
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) log() *zap.Logger {
|
||||
if r == nil || r.logger == nil {
|
||||
return zap.NewNop()
|
||||
@@ -237,7 +251,7 @@ func (r *Repository) recordConfigUsage(ctx context.Context, runtimeConfig *runti
|
||||
if r.configRepo == nil || runtimeConfig == nil || payment == nil || runtimeConfig.ID == 0 {
|
||||
return
|
||||
}
|
||||
if payment.BizType != "order_pay" || payment.Status != "paid" {
|
||||
if (payment.BizType != "order_pay" && payment.BizType != "mohong_pay") || payment.Status != "paid" {
|
||||
return
|
||||
}
|
||||
if err := r.configRepo.RecordUsage(ctx, runtimeConfig.ID, payment.ID, runtimeConfig.Provider, runtimeConfig.MerchantID, payment.AmountCent, payment.BizType); err != nil {
|
||||
|
||||
@@ -32,6 +32,16 @@ func (s *Service) Start(ctx context.Context, userID uint64, orderID uint64, req
|
||||
return s.repo.Start(ctx, userID, orderID, req, clientIP)
|
||||
}
|
||||
|
||||
func (s *Service) StartMohong(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if userID == 0 || orderID == 0 {
|
||||
return nil, ErrPaymentCannotStart
|
||||
}
|
||||
return s.repo.StartMohong(ctx, userID, orderID, req, clientIP)
|
||||
}
|
||||
|
||||
func (s *Service) Query(ctx context.Context, userID uint64, orderID uint64) (*PaymentDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
@@ -42,6 +52,16 @@ func (s *Service) Query(ctx context.Context, userID uint64, orderID uint64) (*Pa
|
||||
return s.repo.Query(ctx, userID, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) QueryMohong(ctx context.Context, userID uint64, orderID uint64) (*PaymentDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if userID == 0 || orderID == 0 {
|
||||
return nil, ErrPaymentNotFound
|
||||
}
|
||||
return s.repo.QueryMohong(ctx, userID, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) HandleLeshuaNotify(ctx context.Context, params map[string]string, rawPayload string, contentType string) (*NotifyResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
|
||||
Reference in New Issue
Block a user