feat(voucher): 兑换券业务严格隔离、前台全面去¥化、不足强拦截与售后闭环返还加固

1. 普通商品购买流程中严格禁止兑换券抵扣,仅限积分商城支持同等兑换
2. 积分商城卡片、详情、规格及结算页全面去除「¥」符号,强化「个兑换券 / 券」纯资产心智
3. 兑换券与积分逻辑完全对齐,兑换券不足时禁止掏现金补差,前端置灰禁用+后端抛出「兑换券不足」异常
4. 修复售后退款状态机识别偏差,并在订单失效/取消中增加已核销兑换券原路返还兜底
5. 新增及更新 6 套专项自动化测试(累计 200 项断言全部通过)
这个提交包含在:
chen
2026-09-21 11:49:28 +08:00
父节点 cc7bfe1301
当前提交 b598d56483
共有 14 个文件被更改,包括 594 次插入 和 78 次删除

查看文件

@@ -575,6 +575,13 @@ class Order
'remark' => '订单失效自动退回'
]);
// 退回已核销的兑换券(若订单已核销过)
try {
(new \addons\shopro\service\partner\VoucherService())->refundConsume($order);
} catch (\Exception $e) {
\think\Log::write('voucher-refund-error:order:' . ($order->id ?? 0) . ';' . $e->getMessage());
}
if ($order->activity_id || $order->promo_types) {
// 有活动,执行活动失败
ActivityFacade::buyFail($order, 'invalid');

查看文件

@@ -65,6 +65,9 @@ class Partner
$service = new PartnerRewardService();
$orderId = intval($order['id']);
// 若订单已核销兑换券,原路返还核销额度(内部具幂等防重机制)
(new VoucherService())->refundConsume($order);
if ($this->checkIsAllRefund($orderId)) {
// 全单退款:全额回滚即时奖与兑换券计划
$service->refund($orderId);
@@ -92,7 +95,10 @@ class Partner
$unrefundedCount = \think\Db::name('shopro_order_item')
->where('order_id', $orderId)
->where('refund_status', '<>', \app\admin\model\shopro\order\OrderItem::REFUND_STATUS_COMPLETED)
->whereNotIn('refund_status', [
\app\admin\model\shopro\order\OrderItem::REFUND_STATUS_AGREE,
\app\admin\model\shopro\order\OrderItem::REFUND_STATUS_COMPLETED
])
->count();
return $unrefundedCount === 0;

查看文件

@@ -158,7 +158,7 @@ class OrderCreate
$this->invoice_id = $params['invoice_id'] ?? 0;
$this->coupon_id = $params['coupon_id'] ?? 0;
$this->voucher_fee = (isset($params['voucher_fee']) && $params['voucher_fee'] !== '') ? floatval($params['voucher_fee']) : null;
$this->voucher_fee = ($this->order_type === 'score' && isset($params['voucher_fee']) && $params['voucher_fee'] !== '') ? floatval($params['voucher_fee']) : null;
$this->remark = $params['remark'] ?? '';
$this->money = (isset($params['money']) && $params['money'] > 0) ? $params['money'] : 0;
@@ -257,8 +257,21 @@ class OrderCreate
$this->exception($this->activity['activity']['type_text'] . '必须单独购买');
}
if ($this->order_type == 'score' && count($this->goodsList) > 1) {
$this->exception('积分商品必须单独购买');
if ($this->order_type == 'score') {
if (count($this->goodsList) > 1) {
$this->exception('积分商品必须单独购买');
}
// 校验兑换券充足性(创建订单时若所需兑换券不足,严格拦截报错,杜绝现金补差)
$needVoucher = 0;
foreach ($this->goodsList as $buyInfo) {
$needVoucher += floatval($buyInfo['current_sku_price']['price'] ?? 0) * intval($buyInfo['goods_num'] ?? 1);
}
if ($needVoucher > 0 && $this->calc_type == 'create') {
$available = (new \addons\shopro\service\partner\VoucherService())->available($this->user->id);
if ($available < $needVoucher) {
$this->exception('兑换券不足');
}
}
}
// 判断是否支持货到付款
@@ -606,6 +619,13 @@ class OrderCreate
*/
public function calcVoucher()
{
// 严格业务隔离:兑换券仅在积分商城(order_type === 'score')中与积分协同兑换商品,普通商品购买一律禁止抵扣兑换券
if ($this->order_type !== 'score') {
$this->orderData['voucher_available'] = 0;
$this->orderData['voucher_fee'] = 0;
return;
}
$available = 0;
if ($this->user && $this->user->id) {
$available = (new \addons\shopro\service\partner\VoucherService())->available($this->user->id);
@@ -614,7 +634,7 @@ class OrderCreate
// 兑换商城订单:若未显式指定抵扣额度,默认自动全额应用可用兑换券
if ($this->voucher_fee === null) {
$want = $this->order_type == 'score' ? (float) $available : 0;
$want = (float) $available;
} else {
$want = max(0, round((float) $this->voucher_fee, 2));
}
@@ -1176,7 +1196,7 @@ class OrderCreate
}
$ext['offline_status'] = $result['offline_status'];
$ext['need_address'] = $result['need_address']; // 后台判断是否有 编辑收货地址按钮
$ext['voucher_fee'] = $result['voucher_fee'] ?? 0;
$ext['voucher_fee'] = ($this->order_type === 'score') ? ($result['voucher_fee'] ?? 0) : 0;
// 发票开具状态
$invoice_status = 0; // 没有申请