退款审核功能:待交接取消不再自动退款,改为客服审核后原路退回
- 租客在待交接状态取消订单不再自动退款,改为 refund_status=pending_review - 新增退款审核页面 /admin/orders/refund-review,客服可逐个审核 - 新增 POST /refund/approve 通过退款、POST /refund/reject 驳回退款 - 新增 GET /orders/refund-pending 查询待审核退款订单列表 - 优化状态标签表述:已取消→租客已取消、已关闭→客服已关闭、异常→客服介入 - 新增 refund_status: pending_review 及其前端标签「待客服审核退款」 - AdminOrderDetailView 退款状态改用统一 refundStatusLabel
This commit is contained in:
@@ -2,6 +2,7 @@ package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -356,6 +357,50 @@ func (r *Repository) buildRefundStatusDTO(order *model.RentalOrder) *RefundStatu
|
||||
}
|
||||
}
|
||||
|
||||
// AdminApproveRefund 客服审核通过退款申请,触发渠道原路退款。
|
||||
func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) 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.RefundStatus != refundStatusPendingReview {
|
||||
return errors.New("订单不处于待审核退款状态")
|
||||
}
|
||||
if order.RefundAmountCent <= 0 {
|
||||
return errors.New("退款金额无效")
|
||||
}
|
||||
action, err := r.prepareRefund(&order, order.RefundAmountCent, refundBizCancel, "取消订单原路退款(客服审核通过)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
return tx.Save(&order).Error
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(ctx, refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminRejectRefund 客服驳回退款申请,清除退款状态和金额。
|
||||
func (r *Repository) AdminRejectRefund(ctx context.Context, orderID uint64) 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 order.RefundStatus != refundStatusPendingReview {
|
||||
return errors.New("订单不处于待审核退款状态")
|
||||
}
|
||||
order.RefundStatus = "none"
|
||||
order.RefundAmountCent = 0
|
||||
return tx.Save(&order).Error
|
||||
})
|
||||
}
|
||||
|
||||
func isTerminalStatus(status string) bool {
|
||||
return status == orderStatusCompleted || status == orderStatusCancelled || status == orderStatusClosed
|
||||
}
|
||||
|
||||
@@ -39,8 +39,9 @@ const (
|
||||
checkoutStatusAccepted = "accepted"
|
||||
checkoutStatusDisputed = "disputed"
|
||||
|
||||
refundStatusPending = "pending"
|
||||
refundStatusRefunded = "refunded"
|
||||
refundStatusPending = "pending"
|
||||
refundStatusPendingReview = "pending_review"
|
||||
refundStatusRefunded = "refunded"
|
||||
|
||||
listingReviewStatusApproved = "approved"
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ type OrderDTO struct {
|
||||
Status string `json:"status"`
|
||||
HandoffStatus string `json:"handoff_status"`
|
||||
SettlementStatus string `json:"settlement_status"`
|
||||
RefundStatus string `json:"refund_status,omitempty"`
|
||||
RefundAmountCent int64 `json:"refund_amount_cent"`
|
||||
ActiveDispute *ActiveDisputeDTO `json:"active_dispute,omitempty"`
|
||||
Checkout *CheckoutDTO `json:"checkout,omitempty"`
|
||||
AdminActions *AdminActionsDTO `json:"admin_actions,omitempty"`
|
||||
|
||||
@@ -96,6 +96,39 @@ func (h *Handler) AdminRefundStatus(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminApproveRefund(c *gin.Context) {
|
||||
orderID, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.AdminApproveRefund(c.Request.Context(), orderID); err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"approved": true})
|
||||
}
|
||||
|
||||
func (h *Handler) AdminRejectRefund(c *gin.Context) {
|
||||
orderID, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.AdminRejectRefund(c.Request.Context(), orderID); err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"rejected": true})
|
||||
}
|
||||
|
||||
func (h *Handler) ListPendingRefund(c *gin.Context) {
|
||||
items, err := h.service.ListPendingRefund(c.Request.Context())
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) adminAction(c *gin.Context, fn func(context.Context, uint64, uint64, AdminActionRequest, AuditMeta) error, okData gin.H) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -227,7 +227,6 @@ func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint
|
||||
}
|
||||
|
||||
func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64) 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"}).
|
||||
@@ -251,18 +250,16 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64)
|
||||
orderID := order.ID
|
||||
if beforeStatus == orderStatusPendingHandoff {
|
||||
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||
action, err := r.prepareRefund(&order, totalCent, refundBizCancel, "取消订单原路退款")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
order.RefundStatus = refundStatusPendingReview
|
||||
order.RefundAmountCent = totalCent
|
||||
order.RefundedAt = nil
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "订单已取消",
|
||||
Content: "租客已取消订单,账号已重新释放。",
|
||||
Content: "租客已取消订单,退款待客服审核。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
@@ -270,7 +267,7 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64)
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单取消成功",
|
||||
Content: "订单已取消,退款将原路退回您的支付账户。",
|
||||
Content: "订单已取消,退款将由客服审核后原路退回。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
@@ -293,7 +290,6 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(ctx, refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
||||
Status: row.Status,
|
||||
HandoffStatus: row.HandoffStatus,
|
||||
SettlementStatus: row.SettlementStatus,
|
||||
RefundStatus: row.RefundStatus,
|
||||
RefundAmountCent: row.RefundAmountCent,
|
||||
AdminActions: adminActionsForOrder(row.RentalOrder),
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// ListPendingRefund 查询所有待客服审核退款的订单。
|
||||
func (r *Repository) ListPendingRefund(ctx context.Context) ([]OrderDTO, error) {
|
||||
var rows []orderRow
|
||||
err := r.adminQuery(ctx).
|
||||
Where("o.refund_status = ?", refundStatusPendingReview).
|
||||
Order("o.updated_at ASC, o.id ASC").
|
||||
Limit(200).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := r.db.WithContext(ctx)
|
||||
paymentTimeoutMinutes := pendingPaymentTimeoutMinutes(db)
|
||||
items := make([]OrderDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
dto := row.toAdminDTO()
|
||||
applyPaymentDeadline(&dto, row.RentalOrder, paymentTimeoutMinutes)
|
||||
items = append(items, dto)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -226,6 +226,33 @@ func (s *Service) AdminRefundStatus(ctx context.Context, orderID uint64) (*Refun
|
||||
return s.repo.AdminRefundStatus(ctx, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) AdminApproveRefund(ctx context.Context, orderID uint64) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 {
|
||||
return ErrOrderCannotComplete
|
||||
}
|
||||
return s.repo.AdminApproveRefund(ctx, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) AdminRejectRefund(ctx context.Context, orderID uint64) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 {
|
||||
return ErrOrderCannotComplete
|
||||
}
|
||||
return s.repo.AdminRejectRefund(ctx, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) ListPendingRefund(ctx context.Context) ([]OrderDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListPendingRefund(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) FindForUser(ctx context.Context, userID uint64, orderID uint64) (*OrderDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
|
||||
@@ -522,6 +522,9 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.POST("/orders/:id/reset-handoff", requirePerm("order:mark_abnormal"), orderHandler.AdminResetHandoff)
|
||||
adminRoutes.POST("/orders/:id/refund", requirePerm("order:close"), orderHandler.AdminRefund)
|
||||
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.GET("/orders/refund-pending", requirePerm("order:close"), orderHandler.ListPendingRefund)
|
||||
adminRoutes.GET("/listings", requirePerm("listing:view"), listingHandler.ListAdmin)
|
||||
adminRoutes.GET("/listings/pending", requirePerm("listing:approve"), listingHandler.ListPendingReview)
|
||||
adminRoutes.GET("/listings/:id", requirePerm("listing:view"), listingHandler.FindAdmin)
|
||||
|
||||
Reference in New Issue
Block a user