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; } + } }