fix(售后): 修复确认收货状态与退款流水号覆盖

- 退货退款/换货确认收货后按 receive_time 展示待退款/待补发,并切换确认收货、执行退款、补发按钮
- 后台填写的退款流水号写入售后单,退款完成时优先使用,不再被系统退款单号覆盖
- 退款流水号限制 64 个字符
This commit is contained in:
吴昊天 2026-10-22 13:18:51 +08:00
parent b2fbcf257a
commit 137b719187
4 changed files with 63 additions and 15 deletions

View File

@ -696,6 +696,29 @@ class AfterSaleServices extends BaseService
'completed' => '已完成',
];
/**
* 解析售后单展示状态文案
* 退货退款/换货在商家确认收货后仍沿用 receiving 状态(退款/补发动作依赖该状态),
* 展示层按 receive_time 细分为“待退款”“待补发”,避免确认收货后仍显示“待收货”。
* @param array|object $afterSale 售后单数据
* @return string
*/
protected function resolveStatusText($afterSale): string
{
$data = is_object($afterSale) ? $afterSale->toArray() : (array)$afterSale;
$status = (string)($data['status'] ?? '');
$type = (string)($data['type'] ?? '');
if ($status === 'receiving' && (int)($data['receive_time'] ?? 0) > 0) {
if ($type === 'return_refund') {
return '待退款';
}
if ($type === 'exchange') {
return '待补发';
}
}
return self::STATUS_TEXT_MAP[$status] ?? $status;
}
/**
* 售后状态分组映射
* 用于用户端列表 tab 筛选:processing=处理中,completed=已完成,canceled=已取消
@ -908,7 +931,7 @@ class AfterSaleServices extends BaseService
'type' => $type,
'type_text' => self::TYPE_TEXT_MAP[$type] ?? $type,
'status' => $status,
'status_text' => self::STATUS_TEXT_MAP[$status] ?? $status,
'status_text' => $this->resolveStatusText($afterSale),
'refund_price' => (float)($afterSale['refund_price'] ?? 0),
'actual_refund_price' => (float)($afterSale['actual_refund_price'] ?? 0),
// 退款标的物与退还积分:积分订单售后展示退还积分数
@ -933,6 +956,8 @@ class AfterSaleServices extends BaseService
],
'timeline' => $timeline,
'actions' => $actions,
// 商家确认收货时间:前端据此区分“待收货”与“待退款/待补发”
'receive_time' => (int)($afterSale['receive_time'] ?? 0),
'add_time' => (int)($afterSale['add_time'] ?? 0),
];
}
@ -1786,7 +1811,7 @@ class AfterSaleServices extends BaseService
$specOrders = [];
foreach ($list as &$item) {
$item['type_text'] = self::TYPE_TEXT_MAP[$item['type'] ?? ''] ?? ($item['type'] ?? '');
$item['status_text'] = self::STATUS_TEXT_MAP[$item['status'] ?? ''] ?? ($item['status'] ?? '');
$item['status_text'] = $this->resolveStatusText($item);
$item['status_group'] = $this->resolveStatusGroup($item['status'] ?? '');
// 产品类型中文名:动态从注册中心获取,新增产品类型注册即生效
$productType = $item['product_type'] ?? '';
@ -1945,7 +1970,7 @@ class AfterSaleServices extends BaseService
// 附加文案
foreach ($list as &$item) {
$item['type_text'] = self::TYPE_TEXT_MAP[$item['type'] ?? ''] ?? ($item['type'] ?? '');
$item['status_text'] = self::STATUS_TEXT_MAP[$item['status'] ?? ''] ?? ($item['status'] ?? '');
$item['status_text'] = $this->resolveStatusText($item);
}
unset($item);
@ -2507,7 +2532,7 @@ class AfterSaleServices extends BaseService
$refundStatus = 1;
}
$item['refund_status'] = $refundStatus;
$item['refund_status_text'] = self::STATUS_TEXT_MAP[$status] ?? $status;
$item['refund_status_text'] = $this->resolveStatusText($item);
$item['refund_type_text'] = self::TYPE_TEXT_MAP[$item['type'] ?? ''] ?? ($item['type'] ?? '');
// 旧 _status 结构:-1=申请中 -2=已退款 -3=已拒绝
@ -2560,7 +2585,7 @@ class AfterSaleServices extends BaseService
// 旧结构兼容键
$detail['order_id'] = $detail['after_sale_no'] ?? '';
$detail['store_order_id'] = (int)($detail['order_center_id'] ?? 0);
$detail['refund_status_text'] = self::STATUS_TEXT_MAP[$detail['status'] ?? ''] ?? ($detail['status'] ?? '');
$detail['refund_status_text'] = $this->resolveStatusText($detail);
$detail['cartInfo'] = $detail['cart_info'] = $this->tidyOutItems($detail['product_snapshot'] ?? []);
$detail['refunded_price'] = $detail['actual_refund_price'] ?? '0.00';
$detail['_add_time'] = !empty($detail['add_time']) ? date('Y-m-d H:i:s', (int)$detail['add_time']) : '';

View File

@ -174,16 +174,22 @@ class RefundEffectServices extends BaseService implements RefundEffectContextInt
$handler->applyRefundEffects($this, $lockedArray, $afterSale, $order, $items);
$now = time();
// 后台手工填写的退款流水号优先;未填写时才回退渠道退款号/系统退款单号
$refundContext = (array)($lockedArray['context'] ?? []);
$manualTradeNo = trim((string)($refundContext['refund_trade_no'] ?? ''));
$refundTradeNo = $manualTradeNo !== ''
? $manualTradeNo
: (string)($locked['channel_refund_id'] ?: $locked['refund_no']);
$this->afterSaleDao->update((int)$afterSale['id'], [
'status' => 'completed',
'actual_refund_price' => (string)$locked['amount'],
'refund_type' => (string)$locked['refund_method'],
'refund_trade_no' => (string)($locked['channel_refund_id'] ?: $locked['refund_no']),
'refund_trade_no' => $refundTradeNo,
'refund_time' => $now,
'complete_time' => $now,
]);
$handlerContext = (array)($lockedArray['context'] ?? []);
$handlerContext = $refundContext;
$handlerContext['order_no'] = (string)$afterSale['order_no'];
$handlerContext['refund_no'] = (string)$locked['refund_no'];
$handlerContext['total_refund_amount'] = $projectedTotal;

View File

@ -43,6 +43,12 @@ class RefundOrderServices extends BaseService
throw new \RuntimeException('退款金额格式错误');
}
$amount = bcadd((string)$amount, '0', 2);
// 后台选填的退款流水号:去空格并限制字段长度(eb_after_sale.refund_trade_no varchar(64))
$refundTradeNo = trim((string)($context['refund_trade_no'] ?? ''));
if ($refundTradeNo !== '' && mb_strlen($refundTradeNo) > 64) {
throw new \RuntimeException('退款流水号不能超过64个字符');
}
$context['refund_trade_no'] = $refundTradeNo;
$refund = $this->transaction(function () use ($afterSaleId, $amount, $context) {
$afterSale = $this->afterSaleDao->getByIdForUpdate($afterSaleId);
if (!$afterSale) {
@ -115,7 +121,12 @@ class RefundOrderServices extends BaseService
'add_time' => time(),
'update_time' => time(),
]);
$this->afterSaleDao->update($afterSaleId, ['status' => 'refunding']);
$afterSaleUpdate = ['status' => 'refunding'];
if ((string)($context['refund_trade_no'] ?? '') !== '') {
// 先落库后台填写的流水号,退款处理中即可在详情看到,成功回调也不会被覆盖
$afterSaleUpdate['refund_trade_no'] = (string)$context['refund_trade_no'];
}
$this->afterSaleDao->update($afterSaleId, $afterSaleUpdate);
/** @var OrderCenterServices $centerServices */
$centerServices = $this->app->make(OrderCenterServices::class);
$centerServices->syncStatus((string)$afterSale['order_no'], OrderCenter::STATUS_REFUNDING, [

View File

@ -364,7 +364,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="退款流水号">
<el-input v-model="refundForm.refund_trade_no" placeholder="退款交易流水号(选填)"></el-input>
<el-input v-model="refundForm.refund_trade_no" maxlength="64" placeholder="退款交易流水号(选填)"></el-input>
</el-form-item>
</el-form>
<template #footer
@ -525,26 +525,32 @@ const hasResendDelivery = computed(() => {
return d && (d.type || d.no);
});
/**
* 商家是否已确认收到退货
* 退货退款/换货确认收货后仍沿用 receiving 状态,用 receive_time 区分收货前后动作
*/
const isReceived = computed(() => Number((detail.value && detail.value.receive_time) || 0) > 0);
/**
* 是否可执行退款
* - 仅退款:audit_passed 状态可退款
* - 退货退款:receiving 状态(确认收货后)可退款
* - 退货退款:receiving 状态且已确认收货后可退款
*/
const canRefund = computed(() => {
if (!detail.value) return false;
const { status, type } = detail.value;
if (type === 'refund_only' && status === 'audit_passed') return true;
if (type === 'return_refund' && status === 'receiving') return true;
if (type === 'return_refund' && status === 'receiving' && isReceived.value) return true;
return false;
});
/**
* 是否可确认收货(退货退款/换货类型,receiving 状态)
* 是否可确认收货(退货退款/换货类型,receiving 状态且尚未确认收货)
*/
const canReceive = computed(() => {
if (!detail.value) return false;
const { status, type } = detail.value;
return status === 'receiving' && ['return_refund', 'exchange'].includes(type);
return status === 'receiving' && ['return_refund', 'exchange'].includes(type) && !isReceived.value;
});
/**
@ -559,14 +565,14 @@ const canStartRepair = computed(() => {
/**
* 是否可补发商品
* - 补发类型:audit_passed 状态可补发
* - 换货类型:receiving 状态(确认收货后)可补发
* - 换货类型:receiving 状态且已确认收货后可补发
* - 维修类型:repair_complete 状态可补发
*/
const canResend = computed(() => {
if (!detail.value) return false;
const { status, type } = detail.value;
if (type === 'resend' && status === 'audit_passed') return true;
if (type === 'exchange' && status === 'receiving') return true;
if (type === 'exchange' && status === 'receiving' && isReceived.value) return true;
if (type === 'repair' && status === 'repair_complete') return true;
return false;
});