新增撞车订单人工退款

This commit is contained in:
yml2213
2026-07-31 21:57:35 +08:00
parent 8c2431c4f2
commit 9511b580e4
13 changed files with 516 additions and 20 deletions
@@ -63,20 +63,20 @@ func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*Financ
db := r.db.WithContext(ctx)
var payment paymentSummaryRow
if err := db.Table("payment_orders AS po").
Select(`COALESCE(SUM(CASE WHEN po.biz_type IN ('order_pay') AND po.status = 'paid' THEN po.amount_cent ELSE 0 END), 0) AS total_flow_amount_cent,
Select(`COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'paid' THEN po.amount_cent ELSE 0 END), 0) AS total_flow_amount_cent,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' THEN po.amount_cent ELSE 0 END), 0) AS total_refund_amount_cent,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunding' THEN po.amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent,
COALESCE(SUM(CASE WHEN po.biz_type IN ('order_pay') AND po.status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' AND po.amount_cent >= COALESCE(orig.amount_cent, 0) THEN 1 ELSE 0 END), 0) AS full_refund_count,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' AND po.amount_cent < COALESCE(orig.amount_cent, 0) THEN 1 ELSE 0 END), 0) AS partial_refund_count,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`,
refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()).
payBizTypes(), refundBizTypes(), refundBizTypes(), payBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()).
Joins(`LEFT JOIN (
SELECT order_id, MAX(amount_cent) AS amount_cent
FROM payment_orders
WHERE biz_type = 'order_pay' AND status = 'paid'
WHERE biz_type IN ? AND status = 'paid'
GROUP BY order_id
) AS orig ON orig.order_id = po.order_id`).
) AS orig ON orig.order_id = po.order_id`, payBizTypes()).
Where("po.created_at >= ? AND po.created_at <= ?", query.StartDate, query.EndDate).
Scan(&payment).Error; err != nil {
return nil, err
@@ -168,20 +168,20 @@ func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]Fi
payments := make([]dailyPaymentRow, 0)
if err := db.Table("payment_orders AS po").
Select(`DATE(po.created_at) AS date,
COALESCE(SUM(CASE WHEN po.biz_type IN ('order_pay') AND po.status = 'paid' THEN po.amount_cent ELSE 0 END), 0) AS total_flow_amount_cent,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'paid' THEN po.amount_cent ELSE 0 END), 0) AS total_flow_amount_cent,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' THEN po.amount_cent ELSE 0 END), 0) AS total_refund_amount_cent,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunding' THEN po.amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent,
COALESCE(SUM(CASE WHEN po.biz_type IN ('order_pay') AND po.status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' AND po.amount_cent >= COALESCE(orig.amount_cent, 0) THEN 1 ELSE 0 END), 0) AS full_refund_count,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' AND po.amount_cent < COALESCE(orig.amount_cent, 0) THEN 1 ELSE 0 END), 0) AS partial_refund_count,
COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`,
refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()).
payBizTypes(), refundBizTypes(), refundBizTypes(), payBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()).
Joins(`LEFT JOIN (
SELECT order_id, MAX(amount_cent) AS amount_cent
FROM payment_orders
WHERE biz_type = 'order_pay' AND status = 'paid'
WHERE biz_type IN ? AND status = 'paid'
GROUP BY order_id
) AS orig ON orig.order_id = po.order_id`).
) AS orig ON orig.order_id = po.order_id`, payBizTypes()).
Where("po.created_at >= ? AND po.created_at <= ?", query.StartDate, query.EndDate).
Group("DATE(po.created_at)").
Scan(&payments).Error; err != nil {
@@ -20,6 +20,10 @@ const (
financeStatusOfflineSettlementPending = "offline_settlement_pending"
)
func payBizTypes() []string {
return []string{"order_pay", "mohong_pay"}
}
func refundBizTypes() []string {
return []string{
"cancel_refund",
@@ -29,6 +33,7 @@ func refundBizTypes() []string {
"deposit_refund",
"rent_refund",
"arbitration_refund",
"mohong_refund",
}
}
@@ -23,3 +23,25 @@ func TestDailyDateKey(t *testing.T) {
})
}
}
func TestPayBizTypesIncludeMohongPay(t *testing.T) {
values := map[string]bool{}
for _, item := range payBizTypes() {
values[item] = true
}
for _, want := range []string{"order_pay", "mohong_pay"} {
if !values[want] {
t.Fatalf("payBizTypes missing %s", want)
}
}
}
func TestRefundBizTypesIncludeMohongRefund(t *testing.T) {
values := map[string]bool{}
for _, item := range refundBizTypes() {
values[item] = true
}
if !values["mohong_refund"] {
t.Fatal("refundBizTypes missing mohong_refund")
}
}
+5
View File
@@ -11,6 +11,11 @@ type StartPaymentRequest struct {
JSPayFlag string `json:"jspay_flag"`
}
type StartMohongRefundRequest struct {
AmountCent int64 `json:"amount_cent"`
Remark string `json:"remark"`
}
type PaymentDTO struct {
ID uint64 `json:"id"`
PaymentNo string `json:"payment_no"`
@@ -147,6 +147,24 @@ func (h *Handler) QueryMohong(c *gin.Context) {
response.OK(c, item)
}
func (h *Handler) AdminRefundMohong(c *gin.Context) {
orderID, ok := parseID(c)
if !ok {
return
}
var req StartMohongRefundRequest
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
response.BadRequest(c, "请求参数不正确")
return
}
item, err := h.service.StartMohongRefund(c.Request.Context(), orderID, req.AmountCent, req.Remark)
if err != nil {
writePaymentError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) QueryRefundStatus(c *gin.Context) {
orderID, ok := parseID(c)
if !ok {
+35 -6
View File
@@ -264,13 +264,13 @@ func (r *Repository) prepareRefundOrder(ctx context.Context, originalPayment mod
}
func (r *Repository) syncRefundPayment(ctx context.Context, payment *model.PaymentOrder, queryTerminal bool) (*model.PaymentOrder, error) {
if payment.Status == "refunded" && !queryTerminal {
if err := r.updateOrderRefundStatus(ctx, payment.OrderID, payment.AmountCent, payment.BizType); err != nil {
if err := r.updateRefundedOrderStatus(ctx, payment.OrderID, payment.AmountCent, payment.BizType); err != nil {
return nil, err
}
return payment, nil
}
if payment.Status == "failed" && !queryTerminal {
if err := r.markOrderRefundFailed(ctx, payment.OrderID, payment.AmountCent); err != nil {
if err := r.markRefundFailedOrderStatus(ctx, payment.OrderID, payment.AmountCent, payment.BizType); err != nil {
return nil, err
}
return payment, nil
@@ -290,7 +290,7 @@ func (r *Repository) syncRefundPayment(ctx context.Context, payment *model.Payme
if runtimeConfig.Channel == nil {
return nil, ErrPaymentUnavailable
}
originalPayment, err := r.findOriginalPayment(ctx, payment.OrderID)
originalPayment, err := r.findOriginalPaymentForRefund(ctx, payment)
if err != nil {
return nil, err
}
@@ -362,11 +362,11 @@ func (r *Repository) applyRefundChannelStatus(ctx context.Context, payment *mode
}
switch status {
case "refunded":
return r.updateOrderRefundStatus(ctx, payment.OrderID, payment.AmountCent, payment.BizType)
return r.updateRefundedOrderStatus(ctx, payment.OrderID, payment.AmountCent, payment.BizType)
case "failed":
return r.markOrderRefundFailed(ctx, payment.OrderID, payment.AmountCent)
return r.markRefundFailedOrderStatus(ctx, payment.OrderID, payment.AmountCent, payment.BizType)
default:
return r.markOrderRefunding(ctx, payment.OrderID, payment.AmountCent)
return r.markRefundingOrderStatus(ctx, payment.OrderID, payment.AmountCent, payment.BizType)
}
}
func isRefundBizType(bizType string) bool {
@@ -385,6 +385,35 @@ func refundQueryRequest(payment model.PaymentOrder, originalPayment model.Paymen
ProviderRefundID: payment.ProviderOrderID,
}
}
func (r *Repository) findOriginalPaymentForRefund(ctx context.Context, payment *model.PaymentOrder) (model.PaymentOrder, error) {
if payment != nil && isMohongRefundBizType(payment.BizType) {
return r.findOriginalMohongPayment(ctx, payment.OrderID)
}
return r.findOriginalPayment(ctx, payment.OrderID)
}
func (r *Repository) updateRefundedOrderStatus(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string) error {
if isMohongRefundBizType(bizType) {
return r.updateMohongOrderRefundStatus(ctx, orderID)
}
return r.updateOrderRefundStatus(ctx, orderID, refundAmountCent, bizType)
}
func (r *Repository) markRefundingOrderStatus(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string) error {
if isMohongRefundBizType(bizType) {
return nil
}
return r.markOrderRefunding(ctx, orderID, refundAmountCent)
}
func (r *Repository) markRefundFailedOrderStatus(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string) error {
if isMohongRefundBizType(bizType) {
return nil
}
return r.markOrderRefundFailed(ctx, orderID, refundAmountCent)
}
func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var order model.RentalOrder
@@ -0,0 +1,230 @@
package payment
import (
"context"
"hfb_sys/backend/internal/model"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const bizTypeMohongRefund = "mohong_refund"
func (r *Repository) StartMohongRefund(ctx context.Context, orderID uint64, refundAmountCent int64, remark string) (*RefundDTO, error) {
originalPayment, err := r.findOriginalMohongPayment(ctx, orderID)
if err != nil {
return nil, err
}
runtimeConfig, err := r.runtimeConfigForPayment(ctx, &originalPayment)
if err != nil {
return nil, ErrPaymentUnavailable
}
refundOrder, existing, err := r.prepareMohongRefundOrder(ctx, originalPayment, *runtimeConfig, refundAmountCent)
if err != nil {
return nil, err
}
if existing {
latest, syncErr := r.syncRefundPayment(ctx, refundOrder, refundOrder.Status != "refunded")
if syncErr != nil {
r.log().Warn("已有撞车退款单同步失败", paymentLogFields(ctx, appendFields(
paymentOrderFields(refundOrder),
[]zap.Field{zap.Error(syncErr)},
)...,
)...)
dto := toRefundDTO(*refundOrder)
return &dto, nil
}
dto := toRefundDTO(*latest)
return &dto, nil
}
if runtimeConfig.isMockMode() {
raw := map[string]string{"mock": "true"}
if remark != "" {
raw["remark"] = remark
}
if err := r.applyRefundChannelStatus(ctx, refundOrder, refundChannelStatusUpdate{
Status: "refunded",
ProviderRefundID: "MOCKREF" + refundOrder.ThirdOrderID,
Raw: raw,
Source: channelSourceMock,
}); err != nil {
return nil, err
}
latest, err := r.findPaymentByID(ctx, refundOrder.ID)
if err != nil {
return nil, err
}
dto := toRefundDTO(*latest)
return &dto, nil
}
if runtimeConfig.Channel == nil {
if err := r.markMohongRefundFailed(ctx, refundOrder.ID, map[string]string{"error": "payment channel unavailable"}, nil); err != nil {
r.log().Warn("撞车退款失败状态保存失败", paymentLogFields(ctx, appendFields(
paymentOrderFields(refundOrder),
[]zap.Field{zap.Error(err)},
)...,
)...)
}
return nil, ErrPaymentUnavailable
}
resp, err := runtimeConfig.Channel.CreateRefund(ctx, channelCreateRefundRequest{
ThirdOrderID: originalPayment.ThirdOrderID,
ProviderOrderID: refundOriginProviderOrderID(originalPayment),
MerchantRefundID: refundOrder.ThirdOrderID,
RefundAmountCent: refundOrder.AmountCent,
NotifyURL: runtimeConfig.NotifyURL,
Attach: originalPayment.OrderNo,
Remark: remark,
})
if err != nil {
var rawRequest map[string]string
if resp != nil {
rawRequest = resp.RawRequest
}
if markErr := r.markMohongRefundFailed(ctx, refundOrder.ID, map[string]string{"error": err.Error()}, rawRequest); markErr != nil {
r.log().Warn("撞车退款失败状态保存失败", paymentLogFields(ctx, appendFields(
paymentOrderFields(refundOrder),
[]zap.Field{zap.Error(markErr)},
)...,
)...)
}
return nil, err
}
if !resp.OK {
if markErr := r.markMohongRefundFailed(ctx, refundOrder.ID, resp.Raw, resp.RawRequest); markErr != nil {
r.log().Warn("撞车退款拒绝状态保存失败", paymentLogFields(ctx, appendFields(
paymentOrderFields(refundOrder),
[]zap.Field{zap.Error(markErr)},
)...,
)...)
}
return nil, ErrPaymentUnavailable
}
if err := r.applyRefundChannelStatus(ctx, refundOrder, refundChannelStatusUpdate{
Status: resp.Status,
ProviderRefundID: resp.ProviderRefundID,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
Source: channelSourceCreate,
}); err != nil {
return nil, err
}
latest, err := r.findPaymentByID(ctx, refundOrder.ID)
if err != nil {
return nil, err
}
dto := toRefundDTO(*latest)
return &dto, nil
}
func (r *Repository) findOriginalMohongPayment(ctx context.Context, orderID uint64) (model.PaymentOrder, error) {
var originalPayment model.PaymentOrder
if err := r.db.WithContext(ctx).
Where("order_id = ? AND status = 'paid' AND biz_type = ?", orderID, bizTypeMohongPay).
Order("id DESC").
First(&originalPayment).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return originalPayment, ErrPaymentNotFound
}
return originalPayment, err
}
return originalPayment, nil
}
func (r *Repository) prepareMohongRefundOrder(ctx context.Context, originalPayment model.PaymentOrder, runtimeConfig runtimePaymentConfig, refundAmountCent int64) (*model.PaymentOrder, bool, error) {
var paymentID uint64
existing := false
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var order model.MohongOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, originalPayment.OrderID).Error; err != nil {
return err
}
if order.Status != model.MohongOrderStatusCancelled {
return ErrRefundCannotStart
}
amountCent := refundAmountCent
if amountCent == 0 {
amountCent = order.AmountCent
}
if amountCent <= 0 || amountCent > originalPayment.AmountCent {
return ErrRefundCannotStart
}
var existingRefund model.PaymentOrder
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id = ? AND biz_type = ?", originalPayment.OrderID, bizTypeMohongRefund).
Order("id DESC").
First(&existingRefund).Error
if err == nil {
paymentID = existingRefund.ID
existing = true
return nil
}
if err != gorm.ErrRecordNotFound {
return err
}
paymentNo, err := newPaymentNo()
if err != nil {
return err
}
merchantRefundID := "REF" + paymentNo[3:]
refundOrder := model.PaymentOrder{
PaymentNo: paymentNo,
OrderID: originalPayment.OrderID,
OrderNo: originalPayment.OrderNo,
UserID: originalPayment.UserID,
PaymentConfigID: runtimeConfig.ID,
Provider: runtimeConfig.Provider,
MerchantID: runtimeConfig.MerchantID,
ThirdOrderID: merchantRefundID,
ProviderOrderID: "",
PayWay: originalPayment.PayWay,
JSPayFlag: originalPayment.JSPayFlag,
AmountCent: amountCent,
BizType: bizTypeMohongRefund,
Status: "refunding",
}
if err := tx.Create(&refundOrder).Error; err != nil {
return err
}
paymentID = refundOrder.ID
return nil
})
if err != nil {
return nil, false, err
}
payment, err := r.findPaymentByID(ctx, paymentID)
if err != nil {
return nil, false, err
}
return payment, existing, nil
}
func (r *Repository) updateMohongOrderRefundStatus(ctx context.Context, orderID uint64) error {
return r.db.WithContext(ctx).Model(&model.MohongOrder{}).Where("id = ?", orderID).Update("status", model.MohongOrderStatusRefunded).Error
}
func (r *Repository) markMohongRefundFailed(ctx context.Context, paymentID uint64, raw map[string]string, rawRequest map[string]string) error {
if raw == nil {
raw = map[string]string{"error": "refund failed"}
}
updates := map[string]any{
"status": "failed",
"raw_response": jsonMap(raw),
}
if rawRequest != nil {
updates["raw_request"] = jsonMap(rawRequest)
}
return r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", paymentID).Updates(updates).Error
}
func isMohongRefundBizType(bizType string) bool {
return bizType == bizTypeMohongRefund
}
@@ -3,6 +3,7 @@ package payment
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
@@ -514,6 +515,103 @@ func TestStartRefundDoesNotCreateNewOrderWhenFailedRefundExists(t *testing.T) {
}
}
func TestStartMohongRefundDefaultsFullAmountForCancelledPaidOrder(t *testing.T) {
db := setupPaymentTestDB(t)
if err := db.AutoMigrate(&model.MohongOrder{}); err != nil {
t.Fatalf("migrate mohong order failed: %v", err)
}
repo := NewRepository(db, nil, nil)
order := model.MohongOrder{
OrderNo: "MH202607310001",
UserID: 42,
ProductID: 8,
Quantity: 1,
UnitPriceCent: 13300,
AmountCent: 13300,
Status: model.MohongOrderStatusCancelled,
}
if err := db.Create(&order).Error; err != nil {
t.Fatalf("create mohong order failed: %v", err)
}
original := model.PaymentOrder{
PaymentNo: "PAYMOHONG0001",
OrderID: order.ID,
OrderNo: order.OrderNo,
UserID: order.UserID,
Provider: "mock",
ThirdOrderID: "PAYMOHONG0001",
ProviderOrderID: "MOCKPAYMOHONG0001",
AmountCent: order.AmountCent,
BizType: bizTypeMohongPay,
Status: "paid",
}
if err := db.Create(&original).Error; err != nil {
t.Fatalf("create mohong payment failed: %v", err)
}
dto, err := repo.StartMohongRefund(t.Context(), order.ID, 0, "后台撞车订单人工退款")
if err != nil {
t.Fatalf("StartMohongRefund() error = %v", err)
}
if dto.AmountCent != order.AmountCent {
t.Fatalf("refund amount = %d, want %d", dto.AmountCent, order.AmountCent)
}
if dto.BizType != bizTypeMohongRefund {
t.Fatalf("refund biz_type = %q, want %q", dto.BizType, bizTypeMohongRefund)
}
if dto.Status != "refunded" {
t.Fatalf("refund status = %q, want refunded", dto.Status)
}
var latest model.MohongOrder
if err := db.First(&latest, order.ID).Error; err != nil {
t.Fatalf("find mohong order failed: %v", err)
}
if latest.Status != model.MohongOrderStatusRefunded {
t.Fatalf("mohong order status = %q, want refunded", latest.Status)
}
}
func TestStartMohongRefundRequiresCancelledOrder(t *testing.T) {
db := setupPaymentTestDB(t)
if err := db.AutoMigrate(&model.MohongOrder{}); err != nil {
t.Fatalf("migrate mohong order failed: %v", err)
}
repo := NewRepository(db, nil, nil)
order := model.MohongOrder{
OrderNo: "MH202607310002",
UserID: 42,
ProductID: 8,
Quantity: 1,
UnitPriceCent: 13300,
AmountCent: 13300,
Status: model.MohongOrderStatusPaid,
}
if err := db.Create(&order).Error; err != nil {
t.Fatalf("create mohong order failed: %v", err)
}
original := model.PaymentOrder{
PaymentNo: "PAYMOHONG0002",
OrderID: order.ID,
OrderNo: order.OrderNo,
UserID: order.UserID,
Provider: "mock",
ThirdOrderID: "PAYMOHONG0002",
ProviderOrderID: "MOCKPAYMOHONG0002",
AmountCent: order.AmountCent,
BizType: bizTypeMohongPay,
Status: "paid",
}
if err := db.Create(&original).Error; err != nil {
t.Fatalf("create mohong payment failed: %v", err)
}
_, err := repo.StartMohongRefund(t.Context(), order.ID, 0, "后台撞车订单人工退款")
if !errors.Is(err, ErrRefundCannotStart) {
t.Fatalf("StartMohongRefund() error = %v, want ErrRefundCannotStart", err)
}
}
func TestConfirmPaidUpdatesOrderAndPaymentInOnePath(t *testing.T) {
db := setupPaymentTestDB(t)
orderRepo := ordermodule.NewRepository(db)
@@ -86,6 +86,16 @@ func (s *Service) StartRefund(ctx context.Context, orderID uint64, refundAmountC
return s.repo.StartRefund(ctx, orderID, refundAmountCent, bizType, remark)
}
func (s *Service) StartMohongRefund(ctx context.Context, orderID uint64, refundAmountCent int64, remark string) (*RefundDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if orderID == 0 || refundAmountCent < 0 {
return nil, ErrRefundCannotStart
}
return s.repo.StartMohongRefund(ctx, orderID, refundAmountCent, remark)
}
func (s *Service) QueryRefundStatus(ctx context.Context, orderID uint64) (*RefundDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
+1
View File
@@ -630,6 +630,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.POST(prefix+"/orders/:id/receive", requirePerm("mohong:order_manage"), mohongHandler.AdminReceiveOrder)
adminRoutes.POST(prefix+"/orders/:id/complete", requirePerm("mohong:order_manage"), mohongHandler.AdminCompleteOrder)
adminRoutes.POST(prefix+"/orders/:id/cancel", requirePerm("mohong:order_manage"), mohongHandler.AdminCancelOrder)
adminRoutes.POST(prefix+"/orders/:id/refund", requirePerm("mohong:order_manage"), paymentHandler.AdminRefundMohong)
adminRoutes.GET(prefix+"/config", requirePerm("mohong:config"), mohongHandler.AdminGetConfig)
adminRoutes.PUT(prefix+"/config", requirePerm("mohong:config"), mohongHandler.AdminUpdateConfig)
}