1. 普通商品购买流程中严格禁止兑换券抵扣,仅限积分商城支持同等兑换 2. 积分商城卡片、详情、规格及结算页全面去除「¥」符号,强化「个兑换券 / 券」纯资产心智 3. 兑换券与积分逻辑完全对齐,兑换券不足时禁止掏现金补差,前端置灰禁用+后端抛出「兑换券不足」异常 4. 修复售后退款状态机识别偏差,并在订单失效/取消中增加已核销兑换券原路返还兜底 5. 新增及更新 6 套专项自动化测试(累计 200 项断言全部通过)
156 行
7.6 KiB
JavaScript
156 行
7.6 KiB
JavaScript
/**
|
||
* 普通商品购买禁止兑换券抵扣 & 积分商城专享兑换券规范专项自动化测试
|
||
* 覆盖范围: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);
|
||
}
|