文件
Henglianshutong/tests/verify_voucher_isolation_goods_spec.js
chen b598d56483 feat(voucher): 兑换券业务严格隔离、前台全面去¥化、不足强拦截与售后闭环返还加固
1. 普通商品购买流程中严格禁止兑换券抵扣,仅限积分商城支持同等兑换
2. 积分商城卡片、详情、规格及结算页全面去除「¥」符号,强化「个兑换券 / 券」纯资产心智
3. 兑换券与积分逻辑完全对齐,兑换券不足时禁止掏现金补差,前端置灰禁用+后端抛出「兑换券不足」异常
4. 修复售后退款状态机识别偏差,并在订单失效/取消中增加已核销兑换券原路返还兜底
5. 新增及更新 6 套专项自动化测试(累计 200 项断言全部通过)
2026-09-21 11:49:28 +08:00

156 行
7.6 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 普通商品购买禁止兑换券抵扣 & 积分商城专享兑换券规范专项自动化测试
* 覆盖范围:confirm.vue, OrderCreate.php, voucher.vue, 算法防注入模拟
*/
const fs = require('fs');
const path = require('path');
let passCount = 0;
let failCount = 0;
function assert(condition, message) {
if (condition) {
console.log(` \x1b[32m[PASS]\x1b[0m ${message}`);
passCount++;
} else {
console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`);
failCount++;
}
}
console.log('========================================================');
console.log(' 普通商品购买禁止兑换券抵扣 & 积分商城兑换专项验证');
console.log('========================================================\n');
// 1. UniApp 确认订单页 confirm.vue 验证
const confirmVuePath = path.resolve(__dirname, '../uniapp/pages/order/confirm.vue');
assert(fs.existsSync(confirmVuePath), 'confirm.vue 文件存在');
if (fs.existsSync(confirmVuePath)) {
const content = fs.readFileSync(confirmVuePath, 'utf-8');
// 兑换券整栏限制
const voucherIdx = content.indexOf('<view class="item-title">兑换券</view>');
assert(voucherIdx !== -1, 'confirm.vue 包含兑换券标题栏');
const voucherBlock = content.slice(voucherIdx - 120, voucherIdx);
assert(voucherBlock.includes("state.orderPayload.order_type === 'score'"), 'confirm.vue 兑换券栏严格限制仅在 order_type === score 时展示');
// 弹层 popup 限制
const popupMatch = content.includes(":show=\"state.showVoucher && state.orderPayload.order_type === 'score'\"");
assert(popupMatch, 'confirm.vue 兑换券弹窗绑定 order_type === score 显隐安全保护');
// openVoucher 拦截
assert(content.includes("if (state.orderPayload.order_type !== 'score')"), 'confirm.vue openVoucher() 包含非积分订单直接拦截');
// getOrderInfo 清理
assert(
content.includes("else if (state.orderPayload.order_type !== 'score')") &&
content.includes("delete state.orderPayload.voucher_fee"),
'confirm.vue getOrderInfo() 对非积分订单主动清理 voucher_fee'
);
// submitOrder 清理
const submitOrderIdx = content.indexOf('async function submitOrder()');
const submitOrderBody = content.slice(submitOrderIdx, submitOrderIdx + 500);
assert(
submitOrderBody.includes("state.orderPayload.order_type !== 'score'") &&
submitOrderBody.includes("delete state.orderPayload.voucher_fee"),
'confirm.vue submitOrder() 对非积分订单提交前剔除 voucher_fee'
);
}
// 2. 后端 OrderCreate.php 核心隔离验证
const orderCreatePath = path.resolve(__dirname, '../addons/shopro/service/order/OrderCreate.php');
assert(fs.existsSync(orderCreatePath), 'OrderCreate.php 文件存在');
if (fs.existsSync(orderCreatePath)) {
const content = fs.readFileSync(orderCreatePath, 'utf-8');
// calcParams 白名单解析
assert(content.includes("\$this->voucher_fee = (\$this->order_type === 'score' && isset(\$params['voucher_fee']) && \$params['voucher_fee'] !== '')"), 'calcParams 仅允许 score 积分订单解析 voucher_fee 参数');
// calcVoucher 严格前置隔离
assert(content.includes("if (\$this->order_type !== 'score') {\n \$this->orderData['voucher_available'] = 0;\n \$this->orderData['voucher_fee'] = 0;\n return;\n }"), 'calcVoucher 增加严格隔离:非 score 订单 voucher_available 与 voucher_fee 均强制为 0');
// ext 持久化防护
assert(content.includes("\$ext['voucher_fee'] = (\$this->order_type === 'score') ? (\$result['voucher_fee'] ?? 0) : 0;"), '订单 ext[\'voucher_fee\'] 仅允许积分商城订单记录,普通订单强制为 0');
}
// 3. voucher.vue 文案优化验证
const voucherVuePath = path.resolve(__dirname, '../uniapp/pages/commission/voucher.vue');
assert(fs.existsSync(voucherVuePath), 'voucher.vue 文件存在');
if (fs.existsSync(voucherVuePath)) {
const content = fs.readFileSync(voucherVuePath, 'utf-8');
assert(content.includes('积分兑换专享'), 'voucher.vue 胶囊标签展示「积分兑换专享」');
assert(!content.includes('全场通用'), 'voucher.vue 已彻底移除「全场通用」误导文案');
}
// 4. 算法与防注入场景模拟验证
console.log('\n--- 场景模拟与抵扣安全性测试 ---');
function mockOrderCalc(orderType, userVoucherAvailable, orderPayFee, inputVoucherFee = null) {
let orderData = {
voucher_available: 0,
voucher_fee: 0,
pay_fee: orderPayFee,
total_discount_fee: 0
};
// 模拟 OrderCreate::calcVoucher 隔离机制
if (orderType !== 'score') {
orderData.voucher_available = 0;
orderData.voucher_fee = 0;
return orderData;
}
let available = userVoucherAvailable;
orderData.voucher_available = available;
let want = (inputVoucherFee === null) ? available : Math.max(0, Number(inputVoucherFee));
let apply = Math.min(want, available, orderData.pay_fee);
apply = apply > 0 ? apply : 0;
orderData.voucher_fee = apply;
if (apply > 0) {
orderData.pay_fee = Math.round((orderData.pay_fee - apply) * 100) / 100;
orderData.total_discount_fee = Math.round((orderData.total_discount_fee + apply) * 100) / 100;
}
return orderData;
}
// 场景 1:普通商品购买(¥1398.00),用户拥有 27 个兑换券,正常下单(不传 voucher_fee)
const scene1 = mockOrderCalc('goods', 27, 1398.00);
assert(scene1.voucher_fee === 0, '场景 1:普通商品购买 voucher_fee 为 0');
assert(scene1.voucher_available === 0, '场景 1:普通商品购买 voucher_available 为 0(不向前端泄露抵扣额)');
assert(scene1.pay_fee === 1398.00, '场景 1:普通商品购买应付现金全额保持 1398.00 元');
// 场景 2:普通商品购买(¥1398.00),攻击者尝试越权传参 voucher_fee = 27
const scene2 = mockOrderCalc('goods', 27, 1398.00, 27);
assert(scene2.voucher_fee === 0, '场景 2(恶意注入拦截):普通商品强行传 voucher_fee=27,被严格拦截置零');
assert(scene2.pay_fee === 1398.00, '场景 2(恶意注入拦截):应付金额分文不少,保持 1398.00 元');
// 场景 3:积分商城兑换(商品 50 元 + 10 积分),用户拥有 27 个兑换券,自动抵扣
const scene3 = mockOrderCalc('score', 27, 50.00);
assert(scene3.voucher_fee === 27, '场景 3:积分商城兑换成功抵扣 27 个兑换券');
assert(scene3.voucher_available === 27, '场景 3:积分商城兑换正常返回可用额度 27');
assert(scene3.pay_fee === 23.00, '场景 3:积分商品剩余 23.00 元现金需自付');
// 场景 4:积分商城兑换(商品 50 元 + 10 积分),用户拥有 100 个兑换券,全额抵扣现金
const scene4 = mockOrderCalc('score', 100, 50.00);
assert(scene4.voucher_fee === 50, '场景 4:积分商城兑换成功全额抵扣 50 个兑换券');
assert(scene4.pay_fee === 0.00, '场景 4:应付现金降至 0 元,结合 10 积分达成完全免现金兑换');
// 场景 5:模拟订单 ext 持久化安全
function mockSaveOrderExt(orderType, calcResultVoucherFee) {
return (orderType === 'score') ? (calcResultVoucherFee || 0) : 0;
}
assert(mockSaveOrderExt('goods', 100) === 0, '场景 5:普通订单 ext.voucher_fee 强制为 0');
assert(mockSaveOrderExt('score', 50) === 50, '场景 5:积分订单 ext.voucher_fee 正确记录为 50');
console.log('\n--------------------------------------------------------');
console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
console.log('========================================================\n');
if (failCount > 0) {
process.exit(1);
}