添加订单押金暂扣与归还

This commit is contained in:
yml2213
2026-07-05 19:35:57 +08:00
parent 9ef0843eb0
commit 090a90a268
17 changed files with 587 additions and 36 deletions
+48
View File
@@ -29,6 +29,12 @@ type RentalOrder struct {
RefundStatus string `gorm:"size:32;not null;default:'none';index" json:"refund_status"`
RefundAmountCent int64 `gorm:"not null;default:0" json:"refund_amount_cent"`
RefundedAt *time.Time `json:"refunded_at"`
DepositHoldStatus string `gorm:"size:16;not null;default:'none';index" json:"deposit_hold_status"`
DepositHoldAmountCent int64 `gorm:"not null;default:0" json:"deposit_hold_amount_cent"`
DepositHoldReason string `gorm:"size:255;not null;default:''" json:"deposit_hold_reason"`
DepositHeldBy *uint64 `json:"deposit_held_by"`
DepositHeldAt *time.Time `json:"deposit_held_at"`
DepositHoldReleasedAt *time.Time `json:"deposit_hold_released_at"`
OwnerSettledAt *time.Time `json:"owner_settled_at"`
SettledAt *time.Time `json:"settled_at"`
CreatedAt time.Time `json:"created_at"`
@@ -38,3 +44,45 @@ type RentalOrder struct {
func (RentalOrder) TableName() string {
return "rental_orders"
}
// 押金暂扣状态:客服可对进行中订单的押金退款进行暂扣,订单照常结算,
// 但本应原路退给租客的押金部分挂起不退,后续由客服手动归还。
const (
DepositHoldStatusNone = "none" // 未暂扣
DepositHoldStatusHeld = "held" // 已暂扣(押金退款被拦截/挂起)
DepositHoldStatusReleased = "released" // 已归还
)
// IsDepositHeld 判断订单当前是否处于押金暂扣状态。
func (o *RentalOrder) IsDepositHeld() bool {
return o.DepositHoldStatus == DepositHoldStatusHeld
}
// ApplyDepositHold 在任一结算退款路径中拦截押金退款。
// totalRefundCent 是本次本应原路退还给租客的总金额(含租金退款和押金退款),
// depositPortionCent 是其中属于押金退款的部分。
// 若订单已暂扣,则把押金部分(不超过本次退款总额)从退款中扣除,
// 并把尚未记录过的部分累加到暂扣金额(累计不超过实付押金),返回扣除后实际发起原路退款的金额;
// 未暂扣时原样返回 totalRefundCent。
// 注意:本函数只做“少退、挂起”,绝不多退,因此在任何路径下都不会造成租客损失或平台多付。
func (o *RentalOrder) ApplyDepositHold(totalRefundCent, depositPortionCent int64) int64 {
if !o.IsDepositHeld() {
return totalRefundCent
}
withheld := depositPortionCent
if withheld > totalRefundCent {
withheld = totalRefundCent
}
if withheld <= 0 {
return totalRefundCent
}
remainingDepositHoldCent := o.DepositAmountCent - o.DepositHoldAmountCent
if remainingDepositHoldCent > 0 {
newHold := withheld
if newHold > remainingDepositHoldCent {
newHold = remainingDepositHoldCent
}
o.DepositHoldAmountCent += newHold
}
return totalRefundCent - withheld
}
+49
View File
@@ -0,0 +1,49 @@
package model
import "testing"
func TestRentalOrderApplyDepositHold(t *testing.T) {
order := RentalOrder{
DepositAmountCent: 1000,
DepositHoldStatus: DepositHoldStatusHeld,
}
actualRefundCent := order.ApplyDepositHold(1500, 1000)
if actualRefundCent != 500 {
t.Fatalf("actual refund = %d, want 500", actualRefundCent)
}
if order.DepositHoldAmountCent != 1000 {
t.Fatalf("hold amount = %d, want 1000", order.DepositHoldAmountCent)
}
}
func TestRentalOrderApplyDepositHoldKeepsWithholdingAfterAmountRecorded(t *testing.T) {
order := RentalOrder{
DepositAmountCent: 1000,
DepositHoldStatus: DepositHoldStatusHeld,
DepositHoldAmountCent: 1000,
}
actualRefundCent := order.ApplyDepositHold(1500, 1000)
if actualRefundCent != 500 {
t.Fatalf("actual refund = %d, want 500", actualRefundCent)
}
if order.DepositHoldAmountCent != 1000 {
t.Fatalf("hold amount = %d, want 1000", order.DepositHoldAmountCent)
}
}
func TestRentalOrderApplyDepositHoldSkipsWhenNotHeld(t *testing.T) {
order := RentalOrder{
DepositAmountCent: 1000,
DepositHoldStatus: DepositHoldStatusNone,
}
actualRefundCent := order.ApplyDepositHold(1500, 1000)
if actualRefundCent != 1500 {
t.Fatalf("actual refund = %d, want 1500", actualRefundCent)
}
if order.DepositHoldAmountCent != 0 {
t.Fatalf("hold amount = %d, want 0", order.DepositHoldAmountCent)
}
}
@@ -79,8 +79,10 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
return err
}
if settlement.RenterRefundAmountCent > 0 {
action, err := r.prepareRefund(&order, settlement.RenterRefundAmountCent, "arbitration_refund", "仲裁退款原路退还")
// 押金暂扣:若订单被暂扣,则把本次退款中的押金部分挂起不退,仅退还剩余部分。
refundAmountCent := order.ApplyDepositHold(settlement.RenterRefundAmountCent, settlement.RenterDepositRefundCent)
if refundAmountCent > 0 {
action, err := r.prepareRefund(&order, refundAmountCent, "arbitration_refund", "仲裁退款原路退还")
if err != nil {
return err
}
@@ -169,6 +171,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
type arbitrationSettlement struct {
Entries []wallet.Entry
RenterRefundAmountCent int64
RenterDepositRefundCent int64
OwnerIncomeAmountCent int64
DepositDeductAmountCent int64
}
@@ -223,15 +226,19 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
switch req.Result {
case "full_refund":
addRenterRefund(totalCent, "仲裁全额退款")
settlement.RenterDepositRefundCent = depositAmountCent
case "partial_refund":
if req.AmountCent <= 0 || req.AmountCent > totalCent {
return settlement, ErrInvalidDispute
}
addRenterRefund(req.AmountCent, "仲裁部分退款")
// 部分退款为合并金额,无法精确拆分租金/押金,按押金优先归类以便暂扣。
settlement.RenterDepositRefundCent = money.MinCent(req.AmountCent, depositAmountCent)
addOwnerIncome(money.MinCent(totalCent-req.AmountCent, ownerRentAmountCent+depositAmountCent), "仲裁剩余金额结算给号主")
case "release_deposit":
addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主")
addRenterRefund(depositAmountCent, "仲裁释放押金给租客")
settlement.RenterDepositRefundCent = depositAmountCent
case "deduct_deposit", "compensate_owner":
deductAmountCent := req.AmountCent
if deductAmountCent <= 0 {
@@ -243,6 +250,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
settlement.DepositDeductAmountCent = deductAmountCent
addOwnerIncome(ownerRentAmountCent+deductAmountCent, "仲裁订单金额及押金赔付结算给号主")
addRenterRefund(depositAmountCent-deductAmountCent, "仲裁退回剩余押金给租客")
settlement.RenterDepositRefundCent = depositAmountCent - deductAmountCent
case "order_close":
// Only release frozen funds. No available-balance settlement happens in development mode.
case "mark_abnormal":
+54 -18
View File
@@ -38,7 +38,9 @@ func (r *Repository) AdminClose(ctx context.Context, adminID uint64, orderID uin
archiveAssets(listing, account)
if beforeOrderStatus != orderStatusPendingPayment {
totalCent := order.RentAmountCent + order.DepositAmountCent
action, err := r.prepareRefund(order, totalCent, refundBizAdminClose, "客服关闭订单原路退款")
// 押金暂扣:若订单已暂扣,拦截退款中的押金部分挂起不退,其余照常原路退。
refundAmountCent := order.ApplyDepositHold(totalCent, order.DepositAmountCent)
action, err := r.prepareRefund(order, refundAmountCent, refundBizAdminClose, "客服关闭订单原路退款")
if err != nil {
return err
}
@@ -307,26 +309,50 @@ func resetTargetForOrder(order model.RentalOrder) (adminResetTarget, bool) {
// AdminRefund 触发后台人工退款,退款由 payment 模块走渠道原路退回。
func (r *Repository) AdminRefund(ctx context.Context, orderID uint64) (*RefundStatusDTO, error) {
var order model.RentalOrder
db := r.db.WithContext(ctx)
if err := db.First(&order, orderID).Error; err != nil {
return nil, err
}
if order.RefundStatus == refundStatusRefunded {
return r.buildRefundStatusDTO(&order), nil
}
if r.refundStarter == nil {
return nil, ErrDependencyUnavailable
}
totalCent := order.RentAmountCent + order.DepositAmountCent
if totalCent <= 0 {
return nil, ErrInvalidCheckoutAmount
}
status, err := r.refundStarter.StartRefund(ctx, orderID, totalCent, refundBizAdmin, "后台人工退款")
var refund *refundAction
err := db.Transaction(func(tx *gorm.DB) error {
var order model.RentalOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
return err
}
if order.RefundStatus == refundStatusRefunded {
return nil
}
totalCent := order.RentAmountCent + order.DepositAmountCent
if totalCent <= 0 {
return ErrInvalidCheckoutAmount
}
// 押金暂扣:人工退款也必须尊重暂扣状态,避免绕过押金挂起直接全额退款。
refundAmountCent := order.ApplyDepositHold(totalCent, order.DepositAmountCent)
if refundAmountCent <= 0 {
order.RefundStatus = "none"
order.RefundAmountCent = 0
return tx.Save(&order).Error
}
if r.refundStarter == nil {
return ErrDependencyUnavailable
}
action, err := r.prepareRefund(&order, refundAmountCent, refundBizAdmin, "后台人工退款")
if err != nil {
return err
}
refund = action
return tx.Save(&order).Error
})
if err != nil {
return nil, err
}
status := ""
if refund != nil {
var err error
status, err = r.refundStarter.StartRefund(ctx, refund.OrderID, refund.RefundAmountCent, refund.BizType, refund.Remark)
if err != nil {
return nil, err
}
}
// 重新读取订单,拿到 payment 模块更新后的退款字段。
var order model.RentalOrder
if err := db.First(&order, orderID).Error; err != nil {
return nil, err
}
@@ -371,7 +397,15 @@ func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) err
if order.RefundAmountCent <= 0 {
return errors.New("退款金额无效")
}
action, err := r.prepareRefund(&order, order.RefundAmountCent, refundBizCancel, "取消订单原路退款(客服审核通过)")
// 押金暂扣:若订单已暂扣,拦截退款中的押金部分挂起不退,其余照常原路退。
refundAmountCent := order.ApplyDepositHold(order.RefundAmountCent, order.DepositAmountCent)
if refundAmountCent <= 0 {
// 全部押金被暂扣且无其他可退金额,直接标记为已处理,等待后续手动退押金。
order.RefundStatus = "none"
order.RefundAmountCent = 0
return tx.Save(&order).Error
}
action, err := r.prepareRefund(&order, refundAmountCent, refundBizCancel, "取消订单原路退款(客服审核通过)")
if err != nil {
return err
}
@@ -469,7 +503,9 @@ func (r *Repository) AdminSeal(ctx context.Context, adminID uint64, orderID uint
sealAssets(listing, account)
if beforeOrderStatus != orderStatusPendingPayment {
totalCent := order.RentAmountCent + order.DepositAmountCent
action, err := r.prepareRefund(order, totalCent, refundBizAdminSeal, "客服封存订单原路退款")
// 押金暂扣:若订单已暂扣,拦截退款中的押金部分挂起不退,其余照常原路退。
refundAmountCent := order.ApplyDepositHold(totalCent, order.DepositAmountCent)
action, err := r.prepareRefund(order, refundAmountCent, refundBizAdminSeal, "客服封存订单原路退款")
if err != nil {
return err
}
@@ -77,10 +77,12 @@ func appendCheckoutOwnerIncome(tx *gorm.DB, order *model.RentalOrder, settlement
func (r *Repository) prepareCheckoutRefund(order *model.RentalOrder, settlement checkoutSettlement) (*refundAction, error) {
renterRefundTotalCent := settlement.RentRefundCent + settlement.DepositRefundCent
if renterRefundTotalCent <= 0 {
// 押金暂扣:若订单已暂扣,拦截本次结账中应退还租客的押金部分挂起不退,其余(租金退款)照常原路退。
actualRefundCent := order.ApplyDepositHold(renterRefundTotalCent, settlement.DepositRefundCent)
if actualRefundCent <= 0 {
return nil, nil
}
return r.prepareRefund(order, renterRefundTotalCent, refundBizCheckout, "结账退款原路退还")
return r.prepareRefund(order, actualRefundCent, refundBizCheckout, "结账退款原路退还")
}
func applyCheckoutSettlement(checkout *model.OrderCheckout, settlement checkoutSettlement) {
@@ -1,5 +1,7 @@
package order
import "hfb_sys/backend/internal/model"
const (
orderStatusPendingPayment = "pending_payment"
orderStatusPendingHandoff = "pending_handoff"
@@ -66,4 +68,9 @@ const (
refundBizAdminSeal = "admin_seal_refund"
refundBizAdmin = "admin_refund"
refundBizCheckout = "checkout_refund"
refundBizDeposit = "deposit_refund"
depositHoldStatusNone = model.DepositHoldStatusNone
depositHoldStatusHeld = model.DepositHoldStatusHeld
depositHoldStatusReleased = model.DepositHoldStatusReleased
)
@@ -0,0 +1,110 @@
package order
import (
"context"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// AdminHoldDeposit 客服对进行中订单的押金进行暂扣。
// 暂扣本身不移动任何资金:租客押金全程在支付渠道,系统只记录“暂扣中”标记;
// 待订单结算/取消/关闭/仲裁时,本应原路退还租客的押金部分会被拦截挂起,
// 后续由客服通过 AdminReleaseDeposit 手动原路归还。
func (r *Repository) AdminHoldDeposit(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var order model.RentalOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
return err
}
if !canHoldDeposit(order) {
return ErrDepositCannotHold
}
beforeStatus := order.DepositHoldStatus
now := time.Now()
order.DepositHoldStatus = depositHoldStatusHeld
order.DepositHoldReason = req.Reason
order.DepositHeldBy = &adminID
order.DepositHeldAt = &now
order.DepositHoldReleasedAt = nil
if err := appendAuditLog(tx, adminID, "order.deposit_hold", "order", order.ID, meta, map[string]any{
"order_id": order.ID,
"order_no": order.OrderNo,
"renter_id": order.RenterID,
"reason": req.Reason,
"before_hold_status": beforeStatus,
"after_hold_status": order.DepositHoldStatus,
"deposit_amount_cent": order.DepositAmountCent,
}); err != nil {
return err
}
return tx.Save(&order).Error
})
}
// AdminReleaseDeposit 客服手动归还此前暂扣的押金,通过支付渠道原路退回。
// 仅支持归还,不支持扣款(系统内无平台资金池承接扣款)。
func (r *Repository) AdminReleaseDeposit(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
var refund *refundAction
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var order model.RentalOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
return err
}
if order.DepositHoldStatus != depositHoldStatusHeld {
return ErrDepositNotHeld
}
holdAmountCent := order.DepositHoldAmountCent
if holdAmountCent <= 0 {
return ErrDepositHoldAmountEmpty
}
now := time.Now()
order.DepositHoldStatus = depositHoldStatusReleased
order.DepositHoldReleasedAt = &now
action, err := r.prepareRefund(&order, holdAmountCent, refundBizDeposit, "暂扣押金归还原路退回")
if err != nil {
return err
}
refund = action
if err := appendAuditLog(tx, adminID, "order.deposit_release", "order", order.ID, meta, map[string]any{
"order_id": order.ID,
"order_no": order.OrderNo,
"renter_id": order.RenterID,
"reason": req.Reason,
"deposit_hold_amount_cent": holdAmountCent,
}); err != nil {
return err
}
return tx.Save(&order).Error
})
if err != nil {
return err
}
r.startRefundBestEffort(ctx, refund)
return nil
}
// canHoldDeposit 判断订单当前是否允许暂扣押金:
// 仅进行中(非终态、押金退款尚未发生)且有实付押金、且未处于暂扣态的订单。
func canHoldDeposit(order model.RentalOrder) bool {
if order.DepositHoldStatus != depositHoldStatusNone {
return false
}
if order.DepositAmountCent <= 0 {
return false
}
switch order.Status {
case orderStatusPendingHandoff,
orderStatusRenting,
orderStatusOverdue,
orderStatusPendingCheckoutConfirm,
orderStatusPendingCheckoutAccept,
orderStatusCheckoutDisputing:
return true
default:
return false
}
}
+5
View File
@@ -38,6 +38,11 @@ type OrderDTO struct {
SettlementStatus string `json:"settlement_status"`
RefundStatus string `json:"refund_status,omitempty"`
RefundAmountCent int64 `json:"refund_amount_cent"`
DepositHoldStatus string `json:"deposit_hold_status,omitempty"`
DepositHoldAmountCent int64 `json:"deposit_hold_amount_cent"`
DepositHoldReason string `json:"deposit_hold_reason,omitempty"`
DepositHeldAt *time.Time `json:"deposit_held_at,omitempty"`
DepositHoldReleasedAt *time.Time `json:"deposit_hold_released_at,omitempty"`
ActiveDispute *ActiveDisputeDTO `json:"active_dispute,omitempty"`
Checkout *CheckoutDTO `json:"checkout,omitempty"`
AdminActions *AdminActionsDTO `json:"admin_actions,omitempty"`
@@ -74,6 +74,14 @@ func (h *Handler) AdminResetHandoff(c *gin.Context) {
h.adminAction(c, h.service.AdminResetHandoff, gin.H{"reset": true})
}
func (h *Handler) AdminHoldDeposit(c *gin.Context) {
h.adminAction(c, h.service.AdminHoldDeposit, gin.H{"held": true})
}
func (h *Handler) AdminReleaseDeposit(c *gin.Context) {
h.adminAction(c, h.service.AdminReleaseDeposit, gin.H{"released": true})
}
func (h *Handler) AdminRefund(c *gin.Context) {
orderID, ok := parseID(c)
if !ok {
@@ -47,6 +47,12 @@ func writeOrderError(c *gin.Context, err error) {
response.BadRequest(c, "结账金额不符合规则")
case errors.Is(err, ErrPermissionDenied):
response.Error(c, http.StatusForbidden, "permission_denied", "无权操作该订单")
case errors.Is(err, ErrDepositCannotHold):
response.Error(c, http.StatusConflict, "deposit_cannot_hold", "当前订单不可暂扣押金")
case errors.Is(err, ErrDepositNotHeld):
response.Error(c, http.StatusConflict, "deposit_not_held", "该订单押金未处于暂扣状态")
case errors.Is(err, ErrDepositHoldAmountEmpty):
response.Error(c, http.StatusConflict, "deposit_hold_amount_empty", "暂扣押金尚未形成可归还金额")
case IsNotFound(err):
response.Error(c, http.StatusNotFound, "not_found", "订单不存在")
default:
@@ -58,6 +58,11 @@ func (row orderRow) toAdminDTO() OrderDTO {
SettlementStatus: row.SettlementStatus,
RefundStatus: row.RefundStatus,
RefundAmountCent: row.RefundAmountCent,
DepositHoldStatus: row.DepositHoldStatus,
DepositHoldAmountCent: row.DepositHoldAmountCent,
DepositHoldReason: row.DepositHoldReason,
DepositHeldAt: row.DepositHeldAt,
DepositHoldReleasedAt: row.DepositHoldReleasedAt,
AdminActions: adminActionsForOrder(row.RentalOrder),
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
@@ -67,6 +72,12 @@ func (row orderRow) toAdminDTO() OrderDTO {
func (row orderRow) toDTOForUser(userID uint64) OrderDTO {
dto := row.toAdminDTO()
dto.AdminActions = nil
// 押金暂扣属于客服内部处置,不向租客/号主暴露。
dto.DepositHoldStatus = ""
dto.DepositHoldAmountCent = 0
dto.DepositHoldReason = ""
dto.DepositHeldAt = nil
dto.DepositHoldReleasedAt = nil
applyOrderPriceView(&dto, row.RentalOrder, userID)
return dto
}
@@ -0,0 +1,41 @@
package order
import (
"testing"
"time"
"hfb_sys/backend/internal/model"
)
func TestOrderDTOForUserHidesDepositHoldFields(t *testing.T) {
now := time.Now()
row := orderRow{
RentalOrder: model.RentalOrder{
ID: 1,
OwnerID: 10,
RenterID: 20,
DepositHoldStatus: model.DepositHoldStatusHeld,
DepositHoldAmountCent: 1000,
DepositHoldReason: "风控复核",
DepositHeldAt: &now,
DepositHoldReleasedAt: &now,
},
}
dto := row.toDTOForUser(20)
if dto.DepositHoldStatus != "" {
t.Fatalf("deposit hold status = %q, want empty", dto.DepositHoldStatus)
}
if dto.DepositHoldAmountCent != 0 {
t.Fatalf("deposit hold amount = %d, want 0", dto.DepositHoldAmountCent)
}
if dto.DepositHoldReason != "" {
t.Fatalf("deposit hold reason = %q, want empty", dto.DepositHoldReason)
}
if dto.DepositHeldAt != nil {
t.Fatalf("deposit held at = %v, want nil", dto.DepositHeldAt)
}
if dto.DepositHoldReleasedAt != nil {
t.Fatalf("deposit hold released at = %v, want nil", dto.DepositHoldReleasedAt)
}
}
+23
View File
@@ -24,6 +24,9 @@ var (
ErrCheckoutCannotCounter = errors.New("checkout cannot counter")
ErrInvalidCheckoutAmount = errors.New("invalid checkout amount")
ErrPermissionDenied = errors.New("permission denied")
ErrDepositCannotHold = errors.New("deposit cannot hold")
ErrDepositNotHeld = errors.New("deposit not held")
ErrDepositHoldAmountEmpty = errors.New("deposit hold amount empty")
)
const internalOrderHours = 24
@@ -216,6 +219,26 @@ func (s *Service) AdminResetHandoff(ctx context.Context, adminID uint64, orderID
return s.repo.AdminResetHandoff(ctx, adminID, orderID, req, meta)
}
func (s *Service) AdminHoldDeposit(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
if s.repo == nil {
return ErrDependencyUnavailable
}
if orderID == 0 || req.Reason == "" {
return ErrDepositCannotHold
}
return s.repo.AdminHoldDeposit(ctx, adminID, orderID, req, meta)
}
func (s *Service) AdminReleaseDeposit(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
if s.repo == nil {
return ErrDependencyUnavailable
}
if orderID == 0 || req.Reason == "" {
return ErrDepositCannotHold
}
return s.repo.AdminReleaseDeposit(ctx, adminID, orderID, req, meta)
}
func (s *Service) AdminRefund(ctx context.Context, orderID uint64) (*RefundStatusDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
+2
View File
@@ -543,6 +543,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.GET("/orders/:id/refund-status", requirePerm("order:view"), orderHandler.AdminRefundStatus)
adminRoutes.POST("/orders/:id/refund/approve", requirePerm("order:close"), orderHandler.AdminApproveRefund)
adminRoutes.POST("/orders/:id/refund/reject", requirePerm("order:close"), orderHandler.AdminRejectRefund)
adminRoutes.POST("/orders/:id/deposit-hold", requirePerm("order:deposit_hold"), orderHandler.AdminHoldDeposit)
adminRoutes.POST("/orders/:id/deposit-release", requirePerm("order:deposit_hold"), orderHandler.AdminReleaseDeposit)
adminRoutes.GET("/orders/refund-pending", requirePerm("order:close"), orderHandler.ListPendingRefund)
// 管理员线下提号(独立于正常订单流程)