From 28aab2e006655f70be1e3c06f3959ed38f536b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=98=8A=E5=A4=A9?= <442384644@qq.com> Date: Tue, 13 Oct 2026 16:01:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E7=BD=91=E7=9B=98=E5=95=86=E5=93=81):=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AE=A2=E5=8D=95=E7=8A=B6=E6=80=81=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=8C=E8=B5=84=E6=BA=90=E5=8F=91=E6=94=BE=E5=90=8E?= =?UTF-8?q?=E8=BF=9B=E5=85=A5=E5=BE=85=E8=AF=84=E4=BB=B7=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E9=80=80=E6=AC=BE=E8=A7=84=E5=88=99=E4=BB=A5=E5=8F=8D?= =?UTF-8?q?=E6=98=A0=E6=96=B0=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/modules/product/virtual/Handler.php | 39 ++++++---- .../handler/VirtualAfterSaleHandler.php | 4 +- .../virtual/handler/VirtualOrderAdapter.php | 74 +++++++++++++++---- .../virtual/listener/OrderPaidListener.php | 6 +- .../virtual/pipeline/OrderCreatePipeline.php | 2 +- .../virtual/service/RefundServices.php | 6 +- 6 files changed, 94 insertions(+), 37 deletions(-) diff --git a/backend/modules/product/virtual/Handler.php b/backend/modules/product/virtual/Handler.php index 71693dd7d..fc544b96e 100644 --- a/backend/modules/product/virtual/Handler.php +++ b/backend/modules/product/virtual/Handler.php @@ -25,7 +25,7 @@ use modules\product\virtual\dao\VirtualOrderDao; /** * 网盘产品处理器 * 网盘产品无需物流,支付后自动发放网盘链接与提取码 - * 状态流程:待付款 → 待发货(自动) → 已完成 + * 状态流程:待付款 → 自动发放 → 待评价 → 已完成 */ class Handler extends BaseProductTypeHandler { @@ -292,7 +292,7 @@ class Handler extends BaseProductTypeHandler /** * 支付成功履约 - * 网盘商品:自动发货,更新状态为已完成,并同步中台状态 + * 网盘商品:自动发货,更新状态为待评价,并同步中台状态 */ public function afterPay(string $orderNo, array $cartInfoList): void { @@ -307,23 +307,32 @@ class Handler extends BaseProductTypeHandler if (!$order || in_array($order['status'], [VirtualOrderAdapter::EXT_STATUS_CANCELED, VirtualOrderAdapter::EXT_STATUS_REFUNDED], true)) { throw new \RuntimeException('订单当前状态不可发放网盘资源'); } - if ($order['status'] === VirtualOrderAdapter::EXT_STATUS_COMPLETED) { + // 支付回调可能重复到达;资源已发放的订单保持当前状态不重复处理。 + if (in_array($order['status'], [ + VirtualOrderAdapter::EXT_STATUS_PENDING_REVIEW, + VirtualOrderAdapter::EXT_STATUS_COMPLETED, + ], true)) { return; } if (empty($cartInfoList)) { - throw new \RuntimeException('订单网盘资源快照缺失,暂不能完成订单'); + throw new \RuntimeException('订单网盘资源快照缺失,暂不能发放资源'); } foreach ($cartInfoList as $item) { $attrInfo = $item['attr_info'] ?? []; if (empty($attrInfo['download_url'])) { - throw new \RuntimeException('网盘商品未配置下载链接,暂不能完成订单'); + throw new \RuntimeException('网盘商品未配置下载链接,暂不能发放资源'); } } + $fulfilledAt = time(); $orderDao->update(['order_no' => $orderNo], [ - 'status' => VirtualOrderAdapter::EXT_STATUS_COMPLETED, - 'complete_time' => time(), + 'status' => VirtualOrderAdapter::EXT_STATUS_PENDING_REVIEW, + 'send_time' => (int)($order['send_time'] ?? 0) ?: $fulfilledAt, + ]); + // 虚拟商品自动发放等同于发货和收货,写入时间后自动评价任务才能识别待评价订单。 + $centerServices->syncStatus($orderNo, VirtualOrderAdapter::STATUS_PENDING_REVIEW, [ + 'delivery_time' => (int)($center['delivery_time'] ?? 0) ?: $fulfilledAt, + 'take_time' => (int)($center['take_time'] ?? 0) ?: $fulfilledAt, ]); - $centerServices->syncStatus($orderNo, 'completed'); }); } @@ -578,7 +587,7 @@ class Handler extends BaseProductTypeHandler /** * 订单履约 - * 网盘商品:更新状态为已完成,并同步中台状态 + * 网盘商品:更新状态为待评价,并同步中台状态 * @param string $orderNo 订单号 * @param array $orderInfo 订单完整信息 * @return bool @@ -587,14 +596,18 @@ class Handler extends BaseProductTypeHandler { try { $orderDao = app(VirtualOrderDao::class); + $fulfilledAt = time(); $orderDao->update(['order_no' => $orderNo], [ - 'status' => VirtualOrderAdapter::EXT_STATUS_COMPLETED, - 'complete_time' => time(), + 'status' => VirtualOrderAdapter::EXT_STATUS_PENDING_REVIEW, + 'send_time' => $fulfilledAt, ]); - // 网盘商品履约即完成,同步中台订单状态 + // 网盘商品履约后进入待评价,同步中台订单状态。 $centerServices = app(\app\services\order\OrderCenterServices::class); - $centerServices->syncStatus($orderNo, 'completed'); + $centerServices->syncStatus($orderNo, VirtualOrderAdapter::STATUS_PENDING_REVIEW, [ + 'delivery_time' => $fulfilledAt, + 'take_time' => $fulfilledAt, + ]); return true; } catch (\Throwable $e) { diff --git a/backend/modules/product/virtual/handler/VirtualAfterSaleHandler.php b/backend/modules/product/virtual/handler/VirtualAfterSaleHandler.php index c39ccc34b..e109c153d 100644 --- a/backend/modules/product/virtual/handler/VirtualAfterSaleHandler.php +++ b/backend/modules/product/virtual/handler/VirtualAfterSaleHandler.php @@ -18,7 +18,7 @@ use modules\product\virtual\dao\VirtualOrderDao; * 网盘商品售后处理器 * 提供售后规则定义与生命周期回调 * - * 规则:仅支持退款(refund_only),支付后 24 小时内可申请,已发放(完成)后不支持 + * 规则:仅支持退款(refund_only),支付后 24 小时内可申请,资源已发放(待评价/已完成)后不支持 */ class VirtualAfterSaleHandler extends BaseAfterSaleHandler { @@ -39,7 +39,7 @@ class VirtualAfterSaleHandler extends BaseAfterSaleHandler /** * 获取售后规则 - * 网盘商品仅支持退款:未发放前可退,已发放(completed)不可退 + * 网盘商品仅支持退款:未发放前可退,已发放(pending_review/completed)不可退 * @param array $orderCenter 中台订单数据 * @param array $sourceOrder 源表订单数据 * @return array diff --git a/backend/modules/product/virtual/handler/VirtualOrderAdapter.php b/backend/modules/product/virtual/handler/VirtualOrderAdapter.php index 1e6ab26a4..3315f2125 100644 --- a/backend/modules/product/virtual/handler/VirtualOrderAdapter.php +++ b/backend/modules/product/virtual/handler/VirtualOrderAdapter.php @@ -11,14 +11,17 @@ use modules\product\virtual\model\VirtualOrderModel; /** * 网盘商品订单流转定义 * - * 网盘商品:无需物流,支付后自动发货即完成 - * 状态流程:pending_payment → completed(直达终态,无中间环节) + * 网盘商品:无需物流,支付后自动发货,进入待评价 + * 状态流程:pending_payment → pending_review → completed * 任意节点可触发:canceled(取消)/ refunded(退款) * * ═══════════════ 完整状态流转图 ═══════════════ * * pending_payment 待支付 ──────→ 中台 pending_payment - * │ 支付成功 → Event → Listener → Flow::advance('completed') + * │ 支付成功 → Event → Listener → Flow::advance('pending_review') + * ▼ + * pending_review 待评价 ──────→ 中台 pending_review + * │ 评价完成 → OrderCenterServices::checkOrderOver() * ▼ * completed 已完成 ──────→ 中台 completed * @@ -33,7 +36,9 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter /** 待支付 — 订单刚创建 */ public const EXT_STATUS_PENDING_PAYMENT = 'pending_payment'; - /** 已完成 — 支付后即完成(网盘商品无物流环节) */ + /** 待评价 — 资源已自动发放,等待用户评价 */ + public const EXT_STATUS_PENDING_REVIEW = 'pending_review'; + /** 已完成 — 用户完成评价 */ public const EXT_STATUS_COMPLETED = 'completed'; /** 已取消 — 订单已取消 */ public const EXT_STATUS_CANCELED = 'canceled'; @@ -79,10 +84,20 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter Button::primary('去支付', 'payOrder'), ]), + // ───── 待评价 ───── + OrderFlowNode::make(self::EXT_STATUS_PENDING_REVIEW) + ->centerStatus(self::STATUS_PENDING_REVIEW) + ->label('待评价') + ->adminButtons([]) + ->userButtons([ + Button::primary('去评价', 'goComment'), + ]), + // ───── 已完成(终态) ───── OrderFlowNode::make(self::EXT_STATUS_COMPLETED) ->centerStatus(self::STATUS_COMPLETED) ->label('已完成') + ->onEnter('onCompleted') ->adminButtons([]) ->userButtons([ Button::make('申请售后', 'applyAfterSale'), @@ -125,7 +140,7 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter /** * 推导统一状态码 - * 优先级:取消 > 退款完成 > 售后中 > 未支付 > 已支付→已完成 + * 优先级:取消 > 退款完成 > 售后中 > 未支付 > 扩展状态映射 */ public function resolveStatusCode(array $sourceOrder): string { @@ -139,13 +154,16 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter if ($refundStatus === 1) { return self::STATUS_AFTER_SALE; } - // 扩展表不一定携带 paid 字段,网盘商品 status=completed 即视为已支付 + $status = (string)($sourceOrder['status'] ?? ''); + // 扩展表不一定携带 paid 字段,资源已发放的状态均视为已支付 $isPaid = (bool)($sourceOrder['paid'] ?? 0) - || ($sourceOrder['status'] ?? '') === self::EXT_STATUS_COMPLETED; + || in_array($status, [self::EXT_STATUS_PENDING_REVIEW, self::EXT_STATUS_COMPLETED], true); if (!$isPaid) { return self::STATUS_PENDING_PAYMENT; } - return self::STATUS_COMPLETED; + return $status === self::EXT_STATUS_PENDING_REVIEW + ? self::STATUS_PENDING_REVIEW + : self::STATUS_COMPLETED; } /** @@ -159,8 +177,9 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter $payPrice = (float)($sourceOrder['pay_price'] ?? 0); return $refundPrice >= $payPrice ? self::PAY_FULL_REFUND : self::PAY_PARTIAL_REFUND; } + $status = (string)($sourceOrder['status'] ?? ''); $isPaid = (bool)($sourceOrder['paid'] ?? 0) - || ($sourceOrder['status'] ?? '') === self::EXT_STATUS_COMPLETED; + || in_array($status, [self::EXT_STATUS_PENDING_REVIEW, self::EXT_STATUS_COMPLETED], true); return $isPaid ? self::PAY_PAID : self::PAY_UNPAID; } @@ -174,7 +193,7 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter /** * 推导业务状态 - * 网盘商品无中间业务状态,已支付即已完成 + * 网盘商品通过统一状态码表达待评价/已完成,不额外注入业务 Badge */ public function resolveBusinessStatus(array $sourceOrder): ?array { @@ -191,7 +210,7 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter /** * 网盘商品订单生命周期进度条 - * 节点:提交订单 → 支付成功 → 订单完成(网盘商品无物流) + * 节点:提交订单 → 支付成功 → 待评价 → 订单完成 */ public function getProgress(array $detail): array { @@ -199,12 +218,14 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter $isCancel = $statusCode === self::STATUS_CANCELED; $isRefunded = $statusCode === self::STATUS_REFUNDED; $isAfterSale = $statusCode === self::STATUS_AFTER_SALE; + $isPendingReview = $statusCode === self::STATUS_PENDING_REVIEW; $isCompleted = $statusCode === self::STATUS_COMPLETED; $isPendingPayment = $statusCode === self::STATUS_PENDING_PAYMENT; $nodes = [ ['title' => '提交订单', 'key' => 'submit', 'time_field' => 'add_time'], ['title' => '支付成功', 'key' => 'pay', 'time_field' => 'pay_time'], + ['title' => '待评价', 'key' => 'review', 'time_field' => 'take_time'], ['title' => '订单完成', 'key' => 'complete', 'time_field' => ''], ]; @@ -217,9 +238,16 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter unset($node); // 已完成节点由状态码决定,不能仅凭 pay_time 判断 + // 待评价节点时间取自动发放时间;兼容历史订单没有 take_time 的情况,使用发货时间/支付时间兜底 + $reviewTime = (int)($detail['take_time'] ?? 0) + ?: (int)($detail['delivery_time'] ?? 0) + ?: (int)($detail['send_time'] ?? 0) + ?: (int)($detail['pay_time'] ?? 0); + $nodes[2]['time'] = $reviewTime ? date('Y-m-d H:i:s', $reviewTime) : ''; $nodes[2]['completed'] = $isCompleted; + $nodes[3]['completed'] = $isCompleted; if ($isCompleted) { - $nodes[2]['time'] = $detail['pay_time'] ? date('Y-m-d H:i:s', (int)$detail['pay_time']) : ''; + $nodes[3]['time'] = $reviewTime ? date('Y-m-d H:i:s', $reviewTime) : ''; } // 待支付状态:支付/完成节点不应显示已完成,避免脏数据误导 @@ -228,6 +256,8 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter $nodes[1]['time'] = ''; $nodes[2]['completed'] = false; $nodes[2]['time'] = ''; + $nodes[3]['completed'] = false; + $nodes[3]['time'] = ''; } $current = -1; @@ -236,6 +266,8 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter } elseif ($isRefunded || $isAfterSale) { $current = 1; } elseif ($isCompleted) { + $current = 3; + } elseif ($isPendingReview) { $current = 2; } elseif ($isPendingPayment) { // 待支付状态以状态码为准,不依赖时间字段 @@ -271,10 +303,10 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter public function getStatusButtons(string $statusCode, array $detail = []): array { - // 网盘商品理论上不存在 processing 状态,但过渡期内部分旧数据/脏数据可能残留 - // 将 processing 状态按 completed 处理,保证用户端能看到操作按钮 + // 网盘商品理论上不存在 processing 状态,但过渡期内部分旧数据/脏数据可能残留。 + // 已支付的旧 processing 订单按待评价处理,避免跳过评价。 if ($statusCode === self::STATUS_PROCESSING) { - $statusCode = self::STATUS_COMPLETED; + $statusCode = self::STATUS_PENDING_REVIEW; } $node = $this->centerStatusMap[$statusCode][0] ?? null; @@ -283,4 +315,16 @@ class VirtualOrderAdapter extends BaseOrderTypeAdapter } return $this->extStatusMap[$statusCode]['user_buttons'] ?? []; } + + /** + * 评价完成后记录扩展表完成时间。 + * 状态推进由订单中台统一触发,这里只维护网盘扩展表自己的时间字段。 + */ + protected function onCompleted(string $orderNo, array $extra = []): void + { + app()->make(VirtualOrderModel::class)->db() + ->where('order_no', $orderNo) + ->where('complete_time', 0) + ->update(['complete_time' => time()]); + } } diff --git a/backend/modules/product/virtual/listener/OrderPaidListener.php b/backend/modules/product/virtual/listener/OrderPaidListener.php index 4bc6ca340..65994c2e4 100644 --- a/backend/modules/product/virtual/listener/OrderPaidListener.php +++ b/backend/modules/product/virtual/listener/OrderPaidListener.php @@ -41,8 +41,8 @@ class OrderPaidListener } $statusCode = $meta['status_code'] ?? ''; - // 幂等:已经是终态的订单不再重复处理 - if (in_array($statusCode, ['completed', 'refunded', 'canceled'], true)) { + // 幂等:资源已发放或订单已结束时不再重复处理 + if (in_array($statusCode, ['pending_review', 'completed', 'refunded', 'canceled'], true)) { return; } @@ -75,7 +75,7 @@ class OrderPaidListener $lifecycleServices = app(\app\services\order\OrderLifecycleServices::class); $lifecycleServices->handlePaySuccess($orderInfo); - // 3. 构建购物车信息并调用类型 Handler 完成履约(扩展表更新为 completed,并同步中台为 completed) + // 3. 构建购物车信息并调用类型 Handler 完成履约(扩展表更新为 pending_review,并同步中台) $cartInfoList = $this->buildCartInfoList($center, $orderInfo); $handler = app(Handler::class); $handler->afterPay($orderNo, $cartInfoList); diff --git a/backend/modules/product/virtual/pipeline/OrderCreatePipeline.php b/backend/modules/product/virtual/pipeline/OrderCreatePipeline.php index 659bd5c0d..0b96878df 100644 --- a/backend/modules/product/virtual/pipeline/OrderCreatePipeline.php +++ b/backend/modules/product/virtual/pipeline/OrderCreatePipeline.php @@ -49,6 +49,6 @@ class OrderCreatePipeline extends AbstractOrderCreatePipeline // TODO: 迁移网盘商品自动发货逻辑 // 1. 获取商品下载链接 // 2. 发送给用户(站内信/模板消息) - // 3. 更新订单状态为已完成 + // 3. 更新订单状态为待评价,评价完成后再进入已完成 } } diff --git a/backend/modules/product/virtual/service/RefundServices.php b/backend/modules/product/virtual/service/RefundServices.php index 64b484c3a..80be81667 100644 --- a/backend/modules/product/virtual/service/RefundServices.php +++ b/backend/modules/product/virtual/service/RefundServices.php @@ -15,7 +15,7 @@ use modules\product\virtual\dao\VirtualOrderDao; /** * 网盘商品退款服务 * 退款处理业务逻辑 - * 网盘商品退款规则:已完成不可退 + * 网盘商品退款规则:资源发放后不可退 */ class RefundServices { @@ -32,7 +32,7 @@ class RefundServices /** * 退款校验 * 网盘商品退款规则: - * - 已完成不可退(网盘商品支付后自动发货完成) + * - 待评价、已完成不可退(网盘商品支付后自动发放资源) * - 已取消、已退款不可退 * @param string $orderNo * @return bool @@ -44,7 +44,7 @@ class RefundServices return false; } $status = $order['status'] ?? ''; - return !in_array($status, ['canceled', 'refunded', 'completed']); + return !in_array($status, ['canceled', 'refunded', 'pending_review', 'completed'], true); } /**