From 944774bd446bdb87ceb2fa09b114478ba044fbe9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=90=B4=E6=98=8A=E5=A4=A9?= <442384644@qq.com>
Date: Thu, 22 Oct 2026 14:19:07 +0800
Subject: [PATCH] =?UTF-8?q?fix(=E5=94=AE=E5=90=8E):=20=E5=AE=8C=E5=96=84?=
=?UTF-8?q?=E5=94=AE=E5=90=8E=E5=95=86=E5=93=81=E9=87=91=E9=A2=9D=E4=B8=8E?=
=?UTF-8?q?=E7=94=B3=E8=AF=B7=E8=A1=A8=E5=8D=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 未发货订单仅允许仅退款,可用类型与提交校验同时限制
- 售后商品卡展示规格原价;申请页展示商品分摊实付+运费
- 顶部仅保留订单编号,退款金额/退还积分移动到售后类型上方
- 退款金额标签统一为退款金额,旧售后快照回填原价
---
.../app/services/order/AfterSaleServices.php | 88 ++++++++++++++++++-
.../order/OrderCenterUserServices.php | 7 +-
.../order/aftersale/BaseAfterSaleHandler.php | 2 +-
.../dynamic-pages/engines/AfterSaleEngine.vue | 60 ++++++++++---
4 files changed, 140 insertions(+), 17 deletions(-)
diff --git a/backend/app/services/order/AfterSaleServices.php b/backend/app/services/order/AfterSaleServices.php
index 6db0a0086..7c98c9358 100644
--- a/backend/app/services/order/AfterSaleServices.php
+++ b/backend/app/services/order/AfterSaleServices.php
@@ -120,7 +120,7 @@ class AfterSaleServices extends BaseService
return [
'allowed' => true,
- 'types' => $rule['available_types'] ?? [],
+ 'types' => $this->filterTypesByDelivery($rule['available_types'] ?? [], $center),
'deadline' => $rule['deadline'] ?? 0,
'max_num' => $totalRefundable,
// 用户实际支付的运费作为可退款金额的一部分下发,前端用于展示退款上限。
@@ -131,6 +131,21 @@ class AfterSaleServices extends BaseService
];
}
+ /**
+ * 按发货状态收敛售后类型
+ * 未发货订单(delivery_time=0)只能申请仅退款,避免用户选择退货退款/换货等需要先收货的类型。
+ * @param array $types 规则声明的可用类型
+ * @param array $center 中台订单
+ * @return array
+ */
+ protected function filterTypesByDelivery(array $types, array $center): array
+ {
+ if ((int)($center['delivery_time'] ?? 0) <= 0) {
+ return ['refund_only'];
+ }
+ return array_values($types);
+ }
+
/**
* 加载订单商品级可退数量
* 查询订单的全部 order_items,聚合已申请售后单的 refund_num,计算每个 cart_id 的剩余可退数量
@@ -236,6 +251,7 @@ class AfterSaleServices extends BaseService
// 组装返回
$deductionIntegral = $integralUnitPrice > 0 ? [] : $this->allocateDeductionIntegral($center, $orderItems);
$paidAmounts = $this->allocatePaidAmounts($center, $orderItems);
+ $isActivityOrder = !in_array((string)($center['activity_type'] ?? ''), ['', 'normal'], true);
$result = [];
foreach ($orderItems as $item) {
$cartId = (int)($item['cart_id'] ?? 0);
@@ -264,6 +280,10 @@ class AfterSaleServices extends BaseService
'refunded_num' => $refundedNum,
'refundable_num' => $refundableNum,
'true_price' => (string)($item['true_price'] ?? $item['price'] ?? '0.00'),
+ // 商品信息卡展示规格原价;活动订单展示活动价(true_price 为等级/会员优惠后单价)
+ 'original_price' => $isActivityOrder
+ ? (string)($item['true_price'] ?? $item['price'] ?? '0.00')
+ : (string)($item['price'] ?? $item['attr_info']['price'] ?? $item['true_price'] ?? '0.00'),
// 分摊后的整行实付现金(商品原价 - 优惠券/积分抵扣/促销等优惠),退款现金上限按此累计
'paid_amount_total' => $paidAmounts[$cartId] ?? '0.00',
// 积分参考价:非积分订单为 0,仅积分活动订单有值
@@ -794,6 +814,57 @@ class AfterSaleServices extends BaseService
}));
}
+ /**
+ * 为售后商品快照补充原价
+ * 旧售后单快照只保存 true_price(等级/会员优惠后单价),详情商品卡需展示规格原价;
+ * 新快照已带 original_price,只有缺失时才查订单明细回填。
+ * @param array $snapshot 售后商品快照
+ * @param string $orderNo 订单号
+ * @return array
+ */
+ protected function enrichSnapshotOriginalPrice(array $snapshot, string $orderNo): array
+ {
+ $items = $snapshot['items'] ?? [];
+ if (!is_array($items) || empty($items)) {
+ return $snapshot;
+ }
+ $needEnrich = false;
+ foreach ($items as $item) {
+ if (!isset($item['original_price']) || $item['original_price'] === '') {
+ $needEnrich = true;
+ break;
+ }
+ }
+ if (!$needEnrich || $orderNo === '') {
+ return $snapshot;
+ }
+ $center = $this->centerServices->getOne(['order_no' => $orderNo]);
+ if (empty($center)) {
+ return $snapshot;
+ }
+ if (is_object($center)) {
+ $center = $center->toArray();
+ }
+ $orderItems = $this->centerServices->loadOrderItems($center);
+ $itemMap = [];
+ foreach ($orderItems as $orderItem) {
+ $itemMap[(string)($orderItem['cart_id'] ?? '')] = $orderItem;
+ }
+ $isActivityOrder = !in_array((string)($center['activity_type'] ?? ''), ['', 'normal'], true);
+ foreach ($items as &$item) {
+ if (isset($item['original_price']) && $item['original_price'] !== '') {
+ continue;
+ }
+ $orderItem = $itemMap[(string)($item['cart_id'] ?? '')] ?? [];
+ $item['original_price'] = $isActivityOrder
+ ? (string)($orderItem['true_price'] ?? $item['true_price'] ?? '0.00')
+ : (string)($orderItem['price'] ?? $orderItem['attr_info']['price'] ?? $item['true_price'] ?? '0.00');
+ }
+ unset($item);
+ $snapshot['items'] = $items;
+ return $snapshot;
+ }
+
/**
* 获取售后详情
* 含售后单信息、时间线、用户可操作按钮,前端详情模式据此渲染
@@ -813,6 +884,16 @@ class AfterSaleServices extends BaseService
// 构造时间线(按售后类型不同节点不同,终态特殊处理)
$timeline = $this->buildTimeline($afterSale);
+ // 旧售后快照只有 true_price(优惠后),从订单商品明细回填原价供商品卡展示
+ $productSnapshot = $afterSale['product_snapshot'] ?? [];
+ if (is_string($productSnapshot)) {
+ $productSnapshot = json_decode($productSnapshot, true) ?: [];
+ }
+ $productSnapshot = $this->enrichSnapshotOriginalPrice(
+ is_array($productSnapshot) ? $productSnapshot : [],
+ (string)($afterSale['order_no'] ?? '')
+ );
+
// 获取用户可操作按钮(由产品类型 Handler 提供)
$productType = $afterSale['product_type'] ?? 'normal';
$handler = AfterSaleRuleRegistry::getProvider('product_type', $productType);
@@ -837,7 +918,7 @@ class AfterSaleServices extends BaseService
'remark' => $afterSale['remark'] ?? '',
'reject_reason' => $afterSale['reject_reason'] ?? '',
'evidence_images' => $afterSale['evidence_images'] ?? [],
- 'product_snapshot' => $afterSale['product_snapshot'] ?? [],
+ 'product_snapshot' => $productSnapshot,
// v8.1.1 商品级售后:返回 cart_ids 和 refund_num,供前端展示售后范围
'cart_ids' => $afterSale['cart_ids'] ?? [],
'refund_num' => (int)($afterSale['refund_num'] ?? 0),
@@ -1038,7 +1119,8 @@ class AfterSaleServices extends BaseService
if (!$rule['allow']) {
throw new \RuntimeException($rule['reject_reason'] ?: '当前订单不支持售后');
}
- if (!empty($rule['available_types']) && !in_array($type, $rule['available_types'])) {
+ $availableTypes = $this->filterTypesByDelivery($rule['available_types'] ?? [], $center);
+ if (!empty($availableTypes) && !in_array($type, $availableTypes, true)) {
throw new \RuntimeException('当前订单不支持该售后类型');
}
diff --git a/backend/app/services/order/OrderCenterUserServices.php b/backend/app/services/order/OrderCenterUserServices.php
index 261d595df..009397b77 100644
--- a/backend/app/services/order/OrderCenterUserServices.php
+++ b/backend/app/services/order/OrderCenterUserServices.php
@@ -741,14 +741,15 @@ class OrderCenterUserServices extends OrderCenterServices
// 构建 _status,兼容旧版格式
$data['_status'] = $this->buildRefundOrderStatus($detail);
- // 判断是否支持退货:由各产品类型 Handler 独立声明(supportsReturn),中台不硬编码产品类型
+ // 判断是否支持退货:未发货订单只能仅退款;已发货再由产品类型 Handler 声明是否支持退货
try {
$productTypeRegistry = app()->make(ProductTypeRegistry::class);
$productType = (string)($detail['product_type'] ?? '');
if ($productType === '') throw new ApiException('订单产品类型缺失');
- $data['_status']['_is_back'] = $productTypeRegistry->get($productType)->supportsReturn();
+ $isShipped = (int)($detail['delivery_time'] ?? 0) > 0;
+ $data['_status']['_is_back'] = $isShipped && $productTypeRegistry->get($productType)->supportsReturn();
} catch (\Throwable $e) {
- // Registry 异常时兜底支持退货,保证退款页可正常渲染
+ // Registry 异常时降级为仅退款,避免未发货订单出现退货入口
$data['_status']['_is_back'] = false;
}
$data['cartInfo'] = $data['cartInfo'] ?? $cartInfo;
diff --git a/backend/crmeb/trade/order/aftersale/BaseAfterSaleHandler.php b/backend/crmeb/trade/order/aftersale/BaseAfterSaleHandler.php
index 21adfc283..a39e9f008 100644
--- a/backend/crmeb/trade/order/aftersale/BaseAfterSaleHandler.php
+++ b/backend/crmeb/trade/order/aftersale/BaseAfterSaleHandler.php
@@ -147,7 +147,7 @@ abstract class BaseAfterSaleHandler implements AfterSaleHandlerInterface
if (in_array($type, ['refund_only', 'return_refund'])) {
$fields[] = [
'key' => 'refund_price',
- 'label' => '申请退款金额',
+ 'label' => '退款金额',
'type' => 'price',
'required' => true,
'placeholder' => '请输入退款金额',
diff --git a/frontend/uni-app/src/dynamic-pages/engines/AfterSaleEngine.vue b/frontend/uni-app/src/dynamic-pages/engines/AfterSaleEngine.vue
index d57ac2030..dfe81586c 100644
--- a/frontend/uni-app/src/dynamic-pages/engines/AfterSaleEngine.vue
+++ b/frontend/uni-app/src/dynamic-pages/engines/AfterSaleEngine.vue
@@ -28,13 +28,9 @@
- {{ $t('订单号') }}
+ {{ $t('订单编号') }}
{{ orderNo }}
-
- {{ $t('订单金额') }}
- ¥{{ orderDetail.pay_price || '0.00' }}
-
@@ -58,7 +54,7 @@
{{ item.store_name }}
{{ $t('规格') }}:{{ item.sku }}
- ¥{{ item.true_price }}
+ ¥{{ getProductPaidAmount(item) }}
@@ -90,6 +86,26 @@
+
+
+ {{ $t('退款金额') }}
+
+
+
+ {{ $t('退还积分') }}
+
+
+
{{ $t('售后类型') }}
@@ -102,7 +118,7 @@
-
+
{{ $t(field.label) }}
@@ -205,7 +221,7 @@
{{ detail.after_sale_no }}
- {{ $t('订单号') }}
+ {{ $t('订单编号') }}
{{ detail.order_no }}
@@ -254,7 +270,7 @@
{{ item.store_name }}
{{ $t('规格') }}:{{ item.sku }}
- ¥{{ item.true_price }}
+ ¥{{ item.original_price || item.true_price }}
x{{ item.cart_num }}
@@ -468,6 +484,12 @@ const pendingAction = ref(null);
// ===== computed =====
/** 售后类型选项文案列表(优先后端 type_text,前端映射 fallback) */
const typeOptions = computed(() => availableTypes.value.map(type => i18n.global.t(AFTER_SALE_TYPE_TEXT[type] || type)));
+/** 退款金额字段(置顶展示) */
+const refundPriceField = computed(() => formFields.value.find(field => field.key === 'refund_price') || null);
+/** 退还积分字段(紧跟退款金额展示) */
+const refundIntegralField = computed(() => formFields.value.find(field => field.key === 'refund_integral') || null);
+/** 其余动态字段(退款金额/积分已单独置顶,避免重复渲染) */
+const otherFields = computed(() => formFields.value.filter(field => !['refund_price', 'refund_integral'].includes(field.key)));
/** 状态样式类(用于详情页顶部横幅配色) */
const statusClass = computed(() => {
const status = detail.value.status || '';
@@ -768,6 +790,20 @@ function uploadImage(fieldKey, max) {
});
}
+/**
+ * 商品实际付款金额(单价):商品分摊实付 + 按实付占比分摊的运费
+ * @param {Object} item 可售后商品
+ * @return {string} 两位小数金额
+ */
+function getProductPaidAmount(item) {
+ const cartNum = Number(item.cart_num || 1) || 1;
+ const rowTotal = rowPaidTotal(item);
+ const totalPaid = refundableItems.value.reduce((sum, row) => sum + rowPaidTotal(row), 0);
+ const payPostage = Number(orderDetail.value.pay_postage || 0);
+ const postageShare = totalPaid > 0 && rowTotal > 0 ? payPostage * rowTotal / totalPaid : 0;
+ return ((rowTotal + postageShare) / cartNum).toFixed(2);
+}
+
/** 构建售后商品快照(v8.1.1 按 cart_id 选择范围) */
function buildSnapshot() {
const items = selectedItems.value.map(item => ({
@@ -778,6 +814,7 @@ function buildSnapshot() {
sku: item.sku,
cart_num: refundNumMap.value[item.cart_id] || 0, // 售后数量(部分数量支持)
true_price: item.true_price,
+ original_price: item.original_price || item.true_price,
}));
return {
product_type: orderDetail.value.product_type || 'normal',
@@ -1050,7 +1087,9 @@ defineExpose({
padding: 8rpx 0;
.label {
- color: #999;
+ color: #333;
+ font-size: 30rpx;
+ font-weight: 600;
flex-shrink: 0;
margin-right: 24rpx;
}
@@ -1065,6 +1104,7 @@ defineExpose({
color: #e93323;
font-weight: 600;
}
+
}
}