From 02c11e3e191ac26f3a19abe21c4fe724fbeb354f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=98=8A=E5=A4=A9?= <442384644@qq.com> Date: Mon, 26 Oct 2026 15:17:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=94=AE=E5=90=8E=E7=BB=B4=E4=BF=AE):=20?= =?UTF-8?q?=E7=BB=B4=E4=BF=AE=E5=AE=8C=E6=88=90=E5=8F=AF=E5=A1=AB=E5=AF=84?= =?UTF-8?q?=E5=9B=9E=E7=89=A9=E6=B5=81=E5=B9=B6=E5=AE=8C=E7=BB=93=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E8=AE=A2=E5=8D=95=EF=BC=8C=E9=80=80=E6=AC=BE=E7=BB=88?= =?UTF-8?q?=E6=80=81=E7=BB=9F=E4=B8=80=20refunded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 已退款 Tab 兼容历史 completed;售后状态 Tab 避免空字符串被 EP 序号化。 Co-authored-by: Cursor --- .../controller/order/AfterSaleController.php | 13 +- backend/app/dao/order/AfterSaleDao.php | 10 +- .../app/services/order/AfterSaleServices.php | 111 ++++++++++++----- .../services/order/RefundEffectServices.php | 3 +- .../normal/handler/NormalAfterSaleHandler.php | 115 ++++++++++++++++++ .../src/pages/order/afterSale/detail.vue | 26 ++-- .../admin/src/pages/order/afterSale/index.vue | 37 ++---- 7 files changed, 245 insertions(+), 70 deletions(-) diff --git a/backend/app/adminapi/controller/order/AfterSaleController.php b/backend/app/adminapi/controller/order/AfterSaleController.php index 261f88bb3..ce792c16b 100644 --- a/backend/app/adminapi/controller/order/AfterSaleController.php +++ b/backend/app/adminapi/controller/order/AfterSaleController.php @@ -233,7 +233,7 @@ class AfterSaleController extends AuthController /** * 维修完成 - * 维修完成后商家寄回商品,进入补发流程 + * 可附带寄回物流;完成后售后完结,订单恢复为申请前状态 * @param int $id 售后单主键 * @return mixed */ @@ -242,12 +242,17 @@ class AfterSaleController extends AuthController if ($id <= 0) { return app('json')->fail('参数错误'); } - [$repairRemark] = $this->request->postMore([ + [$repairRemark, $deliveryType, $deliveryNo] = $this->request->postMore([ ['repair_remark', ''], + ['delivery_type', ''], + ['delivery_no', ''], ], true); try { - $this->services->repairComplete($id, $repairRemark); - return app('json')->success('维修完成'); + $this->services->repairComplete($id, (string)$repairRemark, [ + 'delivery_type' => (string)$deliveryType, + 'delivery_no' => (string)$deliveryNo, + ]); + return app('json')->success('维修完成,订单已恢复原状态'); } catch (\RuntimeException $e) { return app('json')->fail($e->getMessage()); } diff --git a/backend/app/dao/order/AfterSaleDao.php b/backend/app/dao/order/AfterSaleDao.php index 5a934ac72..dd120b0a8 100644 --- a/backend/app/dao/order/AfterSaleDao.php +++ b/backend/app/dao/order/AfterSaleDao.php @@ -266,9 +266,13 @@ class AfterSaleDao extends BaseDao $processingStatuses = ['pending_audit', 'audit_passed', 'refunding', 'pending_return', 'receiving', 'resending', 'repairing', 'repair_complete']; $query->whereIn('status', $processingStatuses); } elseif ($mode === 'refunded') { - // 已退款完成:退款/退货退款类型且已完结 - $query->whereIn('status', ['refunded', 'completed']) - ->whereIn('type', ['refund_only', 'return_refund']); + // 已退款:退款落账终态;兼容历史误写入 completed 的退款型售后 + $query->where(function ($q) { + $q->where('status', 'refunded')->whereOr(function ($q2) { + $q2->where('status', 'completed') + ->whereIn('type', ['refund_only', 'return_refund']); + }); + }); } // all 模式:不追加状态条件,返回所有有售后记录的订单号 diff --git a/backend/app/services/order/AfterSaleServices.php b/backend/app/services/order/AfterSaleServices.php index e63c4ad96..705956755 100644 --- a/backend/app/services/order/AfterSaleServices.php +++ b/backend/app/services/order/AfterSaleServices.php @@ -1624,30 +1624,59 @@ class AfterSaleServices extends BaseService /** * 维修完成 + * 记录维修结果与可选寄回物流后,直接完结售后并恢复中台订单到申请前状态。 + * 兼容已停在 repair_complete、尚未完结的历史单。 + * * @param int $afterSaleId * @param string $repairRemark + * @param array $deliveryData 可选寄回物流 delivery_type/delivery_no * @return void */ - public function repairComplete(int $afterSaleId, string $repairRemark = ''): void + public function repairComplete(int $afterSaleId, string $repairRemark = '', array $deliveryData = []): void { $afterSale = $this->dao->get($afterSaleId); if (empty($afterSale)) { throw new \RuntimeException('售后单不存在'); } - if ($afterSale['status'] !== 'repairing') { + if (!in_array($afterSale['status'], ['repairing', 'repair_complete'], true)) { throw new \RuntimeException('当前状态不可维修完成'); } - - $this->dao->update($afterSaleId, [ - 'status' => 'repair_complete', - 'repair_remark' => $repairRemark, - 'repair_complete_time'=> time(), - ]); + if (($afterSale['type'] ?? '') !== 'repair') { + throw new \RuntimeException('非维修类型售后不可执行维修完成'); + } $lifecycle = $this->getLifecycleHandler($afterSale['product_type']); - if ($lifecycle) { + $update = []; + if ($afterSale['status'] === 'repairing') { + $update['status'] = 'repair_complete'; + $update['repair_complete_time'] = time(); + } + if ($repairRemark !== '') { + $update['repair_remark'] = $repairRemark; + } + $deliveryType = trim((string)($deliveryData['delivery_type'] ?? '')); + $deliveryNo = trim((string)($deliveryData['delivery_no'] ?? '')); + if ($deliveryType !== '' || $deliveryNo !== '') { + $update['resend_delivery_type'] = $deliveryType; + $update['resend_delivery_no'] = $deliveryNo; + $update['resend_time'] = time(); + } + if ($update) { + $this->dao->update($afterSaleId, $update); + } + + if ($afterSale['status'] === 'repairing' && $lifecycle) { $lifecycle->afterRepair($afterSale['after_sale_no'], 'repair_complete'); } + if (($deliveryType !== '' || $deliveryNo !== '') && $lifecycle) { + $lifecycle->afterResend($afterSale['after_sale_no'], [ + 'delivery_type' => $deliveryType, + 'delivery_no' => $deliveryNo, + ]); + } + + // 维修流程终结:完结售后单并恢复订单原状态(不再等待用户二次确认收货) + $this->complete($afterSaleId); } /** @@ -1945,26 +1974,31 @@ class AfterSaleServices extends BaseService */ public function getListForAdmin(array $where, int $page, int $limit): array { - $map = []; - // 售后单号模糊查询 + // 关联键走模型搜索器(status/type 等);模糊/区间条件用三元数组 + $map = ['is_del' => 0]; if (!empty($where['after_sale_no'])) { - $map[] = ['after_sale_no', 'like', '%' . $where['after_sale_no'] . '%']; + $map[] = ['after_sale_no', 'like', '%' . trim((string)$where['after_sale_no']) . '%']; } - // 订单号模糊查询 if (!empty($where['order_no'])) { - $map[] = ['order_no', 'like', '%' . $where['order_no'] . '%']; + $map[] = ['order_no', 'like', '%' . trim((string)$where['order_no']) . '%']; } - // 精确筛选 - foreach (['status', 'type', 'product_type', 'order_category'] as $field) { - if (!empty($where[$field])) { - $map[] = [$field, '=', $where[$field]]; + // 状态:单值精确;逗号串/数组走 whereIn(兼容流程分组) + $status = $where['status'] ?? ''; + if ($status !== '' && $status !== null) { + if (is_string($status) && str_contains($status, ',')) { + $map['status'] = array_values(array_filter(array_map('trim', explode(',', $status)))); + } else { + $map['status'] = $status; + } + } + foreach (['type', 'product_type', 'order_category'] as $field) { + if (!empty($where[$field])) { + $map[$field] = $where[$field]; } } - // 时间范围 if (!empty($where['start_time']) && !empty($where['end_time'])) { $map[] = ['add_time', 'between', [(int)$where['start_time'], (int)$where['end_time']]]; } - $map[] = ['is_del', '=', 0]; $list = $this->dao->getList($map, $page, $limit); // 附加文案 @@ -2383,8 +2417,13 @@ class AfterSaleServices extends BaseService $query->where('status', 'audit_rejected'); break; case 'refunded': - // 已退款:refunded 为退款落账瞬时态、completed 为终态,双取兜底历史数据 - $query->whereIn('status', ['refunded', 'completed']); + // 已退款:退款型终态;兼容历史误写入 completed 的退款单 + $query->where(function ($q) { + $q->where('status', 'refunded')->whereOr(function ($q2) { + $q2->where('status', 'completed') + ->whereIn('type', ['refund_only', 'return_refund']); + }); + }); break; default: return 0; @@ -2482,8 +2521,13 @@ class AfterSaleServices extends BaseService case 3: // 退货待收货 $query->where('status', 'receiving'); break; - case 4: // 已退款 - $query->whereIn('status', ['refunded', 'completed']); + case 4: // 已退款(含历史误标 completed 的退款型) + $query->where(function ($q) { + $q->where('status', 'refunded')->whereOr(function ($q2) { + $q2->where('status', 'completed') + ->whereIn('type', ['refund_only', 'return_refund']); + }); + }); break; } } @@ -2509,7 +2553,11 @@ class AfterSaleServices extends BaseService 3 => ['name' => '拒绝退款', 'num' => (clone $base)->where('status', 'audit_rejected')->count()], 4 => ['name' => '商品待退货', 'num' => (clone $base)->where('status', 'pending_return')->count()], 5 => ['name' => '退货待收货', 'num' => (clone $base)->where('status', 'receiving')->count()], - 6 => ['name' => '已退款', 'num' => (clone $base)->whereIn('status', ['refunded', 'completed'])->count()], + 6 => ['name' => '已退款', 'num' => (clone $base)->where(function ($q) { + $q->where('status', 'refunded')->whereOr(function ($q2) { + $q2->where('status', 'completed')->whereIn('type', ['refund_only', 'return_refund']); + }); + })->count()], ]; return ['list' => $list, 'count' => $count, 'num' => $num]; } @@ -2523,8 +2571,11 @@ class AfterSaleServices extends BaseService protected function tidyKefuListItem(array $item, array $orderMap): array { $status = $item['status'] ?? ''; + $type = $item['type'] ?? ''; // 旧数字 refund_status:1=处理中 2=已退款 3=已拒绝 - if (in_array($status, ['refunded', 'completed'])) { + // completed 仅对退款型计为已退款;换货/补发/维修的 completed 仍属处理完结而非退款 + if ($status === 'refunded' + || ($status === 'completed' && in_array($type, ['refund_only', 'return_refund'], true))) { $refundStatus = 2; } elseif ($status === 'audit_rejected' || $status === 'canceled') { $refundStatus = 3; @@ -2629,11 +2680,11 @@ class AfterSaleServices extends BaseService $query->whereBetween('add_time', [strtotime($start), strtotime($end) + 86399]); } if (($where['refund_status'] ?? 0) == 1) { - // 进行中:未完结且未取消/驳回 - $query->whereNotIn('status', ['completed', 'canceled', 'audit_rejected']); + // 进行中:未完结且未取消/驳回(含已退款终态) + $query->whereNotIn('status', ['completed', 'refunded', 'canceled', 'audit_rejected']); } elseif (($where['refund_status'] ?? 0) == 2) { - // 已完成 - $query->where('status', 'completed'); + // 已完结:已完成 + 已退款 + $query->whereIn('status', ['completed', 'refunded']); } $count = (clone $query)->count(); $list = $query->field('id, after_sale_no, order_no, uid, type, status, refund_price, actual_refund_price, refund_integral, reason, reject_reason, remark, refund_num, add_time, refund_time') diff --git a/backend/app/services/order/RefundEffectServices.php b/backend/app/services/order/RefundEffectServices.php index 2ef970107..ce8f799b6 100644 --- a/backend/app/services/order/RefundEffectServices.php +++ b/backend/app/services/order/RefundEffectServices.php @@ -181,8 +181,9 @@ class RefundEffectServices extends BaseService implements RefundEffectContextInt $refundTradeNo = $manualTradeNo !== '' ? $manualTradeNo : (string)($locked['channel_refund_id'] ?: $locked['refund_no']); + // 退款型售后终态为 refunded(已退款 tab);换货/补发/维修走 complete() 才是 completed $this->afterSaleDao->update((int)$afterSale['id'], [ - 'status' => 'completed', + 'status' => 'refunded', 'actual_refund_price' => (string)$locked['amount'], 'refund_type' => (string)$locked['refund_method'], 'refund_trade_no' => $refundTradeNo, diff --git a/backend/modules/product/normal/handler/NormalAfterSaleHandler.php b/backend/modules/product/normal/handler/NormalAfterSaleHandler.php index 45460d29d..0046cc7ef 100644 --- a/backend/modules/product/normal/handler/NormalAfterSaleHandler.php +++ b/backend/modules/product/normal/handler/NormalAfterSaleHandler.php @@ -14,6 +14,7 @@ use crmeb\trade\order\aftersale\BaseAfterSaleHandler; use app\dao\order\AfterSaleDao; use modules\product\normal\dao\NormalOrderDao; use modules\product\normal\dao\NormalProductDao; +use modules\product\normal\service\OrderServices as NormalOrderServices; use app\services\order\OrderCenterServices; /** @@ -271,4 +272,118 @@ class NormalAfterSaleHandler extends BaseAfterSaleHandler 'refund_status' => 0, ]); } + + /** + * 补发/维修寄回后 + * 扩展表进入待收货,并同步中台发货信息 + * @param string $afterSaleNo + * @param array $deliveryData + * @return void + */ + public function afterResend(string $afterSaleNo, array $deliveryData): void + { + $orderNo = $this->getOrderNo($afterSaleNo); + if ($orderNo === '') { + return; + } + $deliveryName = trim((string)($deliveryData['delivery_type'] ?? $deliveryData['delivery_name'] ?? '')); + $deliveryId = trim((string)($deliveryData['delivery_no'] ?? $deliveryData['delivery_id'] ?? '')); + if ($deliveryName === '' && $deliveryId === '') { + return; + } + + /** @var NormalOrderServices $orderServices */ + $orderServices = app(NormalOrderServices::class); + $orderServices->updateDeliveryInfo( + $orderNo, + $deliveryName !== '' ? $deliveryName : '快递', + $deliveryId, + 'express' + ); + } + + /** + * 非退款型售后完结后 + * 必须清掉扩展表 refund_status=1,否则适配器仍会推导为「售后中」 + * 维修/换货/补发若已寄回,确保扩展状态为待收货 + * @param string $afterSaleNo + * @return void + */ + public function afterComplete(string $afterSaleNo): void + { + $orderNo = $this->getOrderNo($afterSaleNo); + if ($orderNo === '') { + return; + } + + $afterSale = $this->getAfterSaleRecord($afterSaleNo); + $type = (string)($afterSale['type'] ?? ''); + $deliveryName = trim((string)($afterSale['resend_delivery_type'] ?? '')); + $deliveryId = trim((string)($afterSale['resend_delivery_no'] ?? '')); + $needPendingReceive = in_array($type, ['repair', 'exchange', 'resend'], true) + && ($deliveryName !== '' || $deliveryId !== ''); + + $sourceUpdate = ['refund_status' => 0]; + if ($needPendingReceive) { + $sourceUpdate['status'] = NormalOrderAdapter::EXT_STATUS_PENDING_RECEIVE; + $sourceUpdate['delivery_type'] = 'express'; + if ($deliveryName !== '') { + $sourceUpdate['delivery_name'] = $deliveryName; + } + if ($deliveryId !== '') { + $sourceUpdate['delivery_id'] = $deliveryId; + } + } + $this->orderDao->update(['order_no' => $orderNo], $sourceUpdate); + + $centerOrder = $this->centerServices->getOne(['order_no' => $orderNo], 'status_code,extra_info,delivery_time'); + $centerUpdate = ['refund_status' => 0]; + if (($centerOrder['status_code'] ?? '') === 'after_sale') { + $centerUpdate['status_code'] = \app\model\order\OrderCenter::STATUS_PROCESSING; + } + if ($needPendingReceive) { + $extraInfo = $centerOrder['extra_info'] ?? []; + if (is_string($extraInfo)) { + $extraInfo = json_decode($extraInfo, true) ?: []; + } + $extraInfo['delivery'] = [ + 'delivery_type' => 'express', + 'delivery_name' => $deliveryName !== '' ? $deliveryName : (string)($extraInfo['delivery']['delivery_name'] ?? ''), + 'delivery_id' => $deliveryId !== '' ? $deliveryId : (string)($extraInfo['delivery']['delivery_id'] ?? ''), + 'delivery_uid' => (int)($extraInfo['delivery']['delivery_uid'] ?? 0), + ]; + $extraInfo['business_status'] = [ + 'code' => NormalOrderAdapter::EXT_STATUS_PENDING_RECEIVE, + 'text' => '待收货', + 'detail' => '维修/补发商品运输中', + 'extend' => new \stdClass(), + ]; + $centerUpdate['extra_info'] = json_encode($extraInfo, JSON_UNESCAPED_UNICODE); + if (empty($centerOrder['delivery_time'])) { + $centerUpdate['delivery_time'] = time(); + } + // restoreOrderStatus 可能已把中台写成 processing;寄回场景显式保持 processing + $centerUpdate['status_code'] = \app\model\order\OrderCenter::STATUS_PROCESSING; + } + $this->centerServices->updateRecord($orderNo, $centerUpdate); + } + + /** + * @param string $afterSaleNo + * @return array + */ + protected function getAfterSaleRecord(string $afterSaleNo): array + { + try { + /** @var AfterSaleDao $afterSaleDao */ + $afterSaleDao = app(AfterSaleDao::class); + $record = $afterSaleDao->getByNo($afterSaleNo); + if (empty($record)) { + return []; + } + return is_object($record) ? $record->toArray() : (array)$record; + } catch (\Throwable $e) { + return []; + } + } } diff --git a/frontend/admin/src/pages/order/afterSale/detail.vue b/frontend/admin/src/pages/order/afterSale/detail.vue index 07725fb67..b6e47ac80 100644 --- a/frontend/admin/src/pages/order/afterSale/detail.vue +++ b/frontend/admin/src/pages/order/afterSale/detail.vue @@ -278,9 +278,9 @@ v-auth="['admin-order-afterSale-start_repair']" >开始维修 - + - + -
维修完成后将进入补发流程,请填写补发物流寄回用户。
+ + + + + + +
填写寄回物流并确认后,售后完结,订单将变为「待收货」。