fix(网盘商品): 更新订单状态流程,资源发放后进入待评价,调整退款规则以反映新状态

This commit is contained in:
吴昊天 2026-10-13 16:01:25 +08:00
parent 46ab4571e5
commit 28aab2e006
6 changed files with 94 additions and 37 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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()]);
}
}

View File

@ -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);

View File

@ -49,6 +49,6 @@ class OrderCreatePipeline extends AbstractOrderCreatePipeline
// TODO: 迁移网盘商品自动发货逻辑
// 1. 获取商品下载链接
// 2. 发送给用户(站内信/模板消息)
// 3. 更新订单状态为已完成
// 3. 更新订单状态为待评价,评价完成后再进入已完成
}
}

View File

@ -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);
}
/**