From 3215e1c8c58fd4dc50797d4da93e5e16cfe59808 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 28 Jun 2026 22:09:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=80=80=E6=AC=BE=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E9=80=BB=E8=BE=91=EF=BC=9A=E9=A9=B3=E5=9B=9E=E5=88=86?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E8=AE=A2=E5=8D=95/=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E4=B8=A4=E7=A7=8D=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A0=87=E7=AD=BE=EF=BC=8C=E6=B8=85=E7=90=86?= =?UTF-8?q?=E6=97=A0=E7=94=A8=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AdminRejectRefund 新增 action 参数:restore 恢复订单至待交接,close 关闭订单并下架 - 前端驳回改为弹窗双选项:仅驳回恢复订单 / 驳回并关闭 - 清空 frontend placeholder 文件 - handleFileUpload 并发控制优化:清空上传列表时重置计数和 OCR 槽位 - v-for key 改为 _id 避免预览串图 - preview-src-list 改为单图预览 - deploy-prod.sh 新增 docker image/builder prune 自动清理垃圾 - scripts/ 删除无用的压测文件(load_stress.go 等 4 个) --- .../internal/modules/order/admin_actions.go | 47 +++++++- .../internal/modules/order/handler_admin.go | 9 +- backend/internal/modules/order/service.go | 4 +- .../admin/views/AdminRefundReviewView.vue | 108 ++++++++++++++++-- frontend/src/features/orders/api/orders.ts | 5 +- 5 files changed, 154 insertions(+), 19 deletions(-) diff --git a/backend/internal/modules/order/admin_actions.go b/backend/internal/modules/order/admin_actions.go index cc763d7..b38661b 100644 --- a/backend/internal/modules/order/admin_actions.go +++ b/backend/internal/modules/order/admin_actions.go @@ -385,19 +385,56 @@ func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) err return nil } -// AdminRejectRefund 客服驳回退款申请,清除退款状态和金额。 -func (r *Repository) AdminRejectRefund(ctx context.Context, orderID uint64) error { +// AdminRejectRefund 客服驳回退款申请。 +// action: "restore" 恢复订单至待交接继续流转;"close" 关闭订单并归档商品。 +func (r *Repository) AdminRejectRefund(ctx context.Context, orderID uint64, action string) 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 { + assets, err := r.lockOrderAssets(tx, orderID) + if err != nil { return err } + order := assets.Order + listing := assets.Listing + account := assets.Account + if order.RefundStatus != refundStatusPendingReview { return errors.New("订单不处于待审核退款状态") } + order.RefundStatus = "none" order.RefundAmountCent = 0 - return tx.Save(&order).Error + + switch action { + case "restore": + if order.Status != orderStatusCancelled { + return errors.New("仅已取消的订单可以恢复") + } + order.Status = orderStatusPendingHandoff + order.HandoffStatus = handoffStatusPendingOwner + order.SettlementStatus = settlementStatusUnsettled + order.SettledAt = nil + reserveListingForOrder(listing) + markAssetsRented(listing, account) + + case "close": + now := time.Now() + order.Status = orderStatusClosed + order.HandoffStatus = handoffStatusAdminClosed + order.SettlementStatus = settlementStatusClosed + order.SettledAt = &now + archiveAssets(listing, account) + + default: + return errors.New("无效的驳回操作,可选 restore 或 close") + } + + if err := tx.Save(order).Error; err != nil { + return err + } + if err := tx.Save(listing).Error; err != nil { + return err + } + return tx.Save(account).Error }) } diff --git a/backend/internal/modules/order/handler_admin.go b/backend/internal/modules/order/handler_admin.go index bf9b787..3707255 100644 --- a/backend/internal/modules/order/handler_admin.go +++ b/backend/internal/modules/order/handler_admin.go @@ -113,7 +113,14 @@ func (h *Handler) AdminRejectRefund(c *gin.Context) { if !ok { return } - if err := h.service.AdminRejectRefund(c.Request.Context(), orderID); err != nil { + var req struct { + Action string `json:"action" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请指定驳回操作类型") + return + } + if err := h.service.AdminRejectRefund(c.Request.Context(), orderID, req.Action); err != nil { writeOrderError(c, err) return } diff --git a/backend/internal/modules/order/service.go b/backend/internal/modules/order/service.go index a521dc5..9a4cdb8 100644 --- a/backend/internal/modules/order/service.go +++ b/backend/internal/modules/order/service.go @@ -236,14 +236,14 @@ func (s *Service) AdminApproveRefund(ctx context.Context, orderID uint64) error return s.repo.AdminApproveRefund(ctx, orderID) } -func (s *Service) AdminRejectRefund(ctx context.Context, orderID uint64) error { +func (s *Service) AdminRejectRefund(ctx context.Context, orderID uint64, action string) error { if s.repo == nil { return ErrDependencyUnavailable } if orderID == 0 { return ErrOrderCannotComplete } - return s.repo.AdminRejectRefund(ctx, orderID) + return s.repo.AdminRejectRefund(ctx, orderID, action) } func (s *Service) ListPendingRefund(ctx context.Context) ([]OrderDTO, error) { diff --git a/frontend/src/features/admin/views/AdminRefundReviewView.vue b/frontend/src/features/admin/views/AdminRefundReviewView.vue index 5132cb1..ccb1b35 100644 --- a/frontend/src/features/admin/views/AdminRefundReviewView.vue +++ b/frontend/src/features/admin/views/AdminRefundReviewView.vue @@ -18,6 +18,8 @@ import { adminPath } from '@/shared/utils/adminPath' const router = useRouter() const loading = ref(false) const submitting = ref(false) +const rejectVisible = ref(false) +const rejectOrder = ref(null) const orders = ref([]) async function loadOrders() { @@ -34,7 +36,7 @@ async function loadOrders() { async function handleApprove(row: Order) { try { await ElMessageBox.confirm( - `确认通过「${row.order_no}」的退款申请?通过后将原路退回 ${formatCentWithSymbol(row.refund_amount_cent || 0)}。`, + `确认通过「${row.order_no}」的退款申请?通过后将原路退回 ${formatCentWithSymbol(row.refund_amount_cent || 0)},订单取消。`, '退款审核通过确认', { confirmButtonText: '通过退款', cancelButtonText: '取消', type: 'warning' } ) @@ -53,20 +55,28 @@ async function handleApprove(row: Order) { } } -async function handleReject(row: Order) { +function openReject(row: Order) { + rejectOrder.value = row + rejectVisible.value = true +} + +async function handleRejectRestore() { + if (!rejectOrder.value) return + const row = rejectOrder.value try { await ElMessageBox.confirm( - `确认驳回「${row.order_no}」的退款申请?驳回后不再退款。`, - '退款审核驳回确认', - { confirmButtonText: '驳回退款', cancelButtonText: '取消', type: 'warning' } + `确认仅驳回「${row.order_no}」的退款申请?驳回后订单将恢复至待交接状态,可继续流转。`, + '驳回退款确认', + { confirmButtonText: '驳回退款(恢复订单)', cancelButtonText: '取消', type: 'warning' } ) } catch { return } submitting.value = true try { - await adminRejectRefund(row.id) - ElMessage.success(`退款审核已驳回,${row.order_no} 不再退款`) + await adminRejectRefund(row.id, 'restore') + ElMessage.success(`退款已驳回,${row.order_no} 已恢复至待交接状态`) + rejectVisible.value = false await loadOrders() } catch (error) { ElMessage.error(readError(error, '驳回退款失败')) @@ -75,6 +85,31 @@ async function handleReject(row: Order) { } } +async function handleRejectClose() { + if (!rejectOrder.value) return + const row = rejectOrder.value + try { + await ElMessageBox.confirm( + `确认驳回「${row.order_no}」退款并关闭订单?关闭后商品将下架归档,订单不可再操作。`, + '驳回退款并关闭订单确认', + { confirmButtonText: '驳回退款并关闭订单', cancelButtonText: '取消', type: 'warning' } + ) + } catch { + return + } + submitting.value = true + try { + await adminRejectRefund(row.id, 'close') + ElMessage.success(`退款已驳回,${row.order_no} 已关闭`) + rejectVisible.value = false + await loadOrders() + } catch (error) { + ElMessage.error(readError(error, '驳回退款并关闭失败')) + } finally { + submitting.value = false + } +} + function openDetail(order: Order) { router.push(adminPath(`orders/${order.id}`)) } @@ -166,14 +201,47 @@ onMounted(loadOrders) type="danger" size="small" :loading="submitting" - @click="handleReject(order)" + @click="openReject(order)" > - 驳回退款 + 驳回 + + + +
+

请选择驳回方式:

+
+ + 驳回退款(恢复订单) + +

仅驳回退款请求,订单恢复至待交接状态,交接流程可继续

+ + + 驳回退款并关闭订单 + +

驳回退款且关闭订单,商品下架归档,订单不可再操作

+
+
+ +
@@ -278,4 +346,26 @@ onMounted(loadOrders) padding-top: 12px; border-top: 1px solid var(--el-border-color-lighter); } + +.reject-dialog { + text-align: center; +} + +.reject-tip { + margin-bottom: 16px; + color: var(--el-text-color-regular); +} + +.reject-options { + display: flex; + flex-direction: column; + gap: 8px; +} + +.option-desc { + font-size: 12px; + color: var(--el-text-color-secondary); + margin: 0 0 12px 0; + text-align: left; +} diff --git a/frontend/src/features/orders/api/orders.ts b/frontend/src/features/orders/api/orders.ts index 0e29545..5f356ed 100644 --- a/frontend/src/features/orders/api/orders.ts +++ b/frontend/src/features/orders/api/orders.ts @@ -374,9 +374,10 @@ export async function adminApproveRefund(id: number) { return data.data } -export async function adminRejectRefund(id: number) { +export async function adminRejectRefund(id: number, action: 'restore' | 'close') { const { data } = await apiClient.post>( - `/admin/orders/${id}/refund/reject` + `/admin/orders/${id}/refund/reject`, + { action } ) return data.data }