feat: 完善商品计奖隔离、积分商城兑换券抵扣与下线售后退款入口
这个提交包含在:
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 计奖商品收益隔离与混合下单防套利专项验证测试
|
||||
* 覆盖范围:PartnerRewardService, VoucherService, RateService, DailySettleService, ActiveService
|
||||
*/
|
||||
|
||||
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. PartnerRewardService.php 验证
|
||||
const partnerRewardServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/PartnerRewardService.php');
|
||||
assert(fs.existsSync(partnerRewardServicePath), 'PartnerRewardService.php 文件存在');
|
||||
if (fs.existsSync(partnerRewardServicePath)) {
|
||||
const content = fs.readFileSync(partnerRewardServicePath, 'utf-8');
|
||||
assert(content.includes('public function getQualifiedPayFee'), '包含 getQualifiedPayFee() 方法');
|
||||
assert(content.includes('CommissionGoods::GOODS_COMMISSION_STATUS_ON'), 'getQualifiedPayFee 校验商品计奖启用状态');
|
||||
assert(content.includes("isset($ext['is_commission']) && !$ext['is_commission']"), 'getQualifiedPayFee 排除活动不可计奖商品项');
|
||||
assert(content.includes('$qualifiedPayFee = $this->getQualifiedPayFee($order)'), 'isQualified() 复用 getQualifiedPayFee()');
|
||||
assert(content.includes("PartnerConfig::get('partner_order_min_amount'"), 'isQualified() 支持配置订单最低计奖金额门槛');
|
||||
assert(content.includes('$isCommissionItem'), 'refundPartial() 判定子商品是否为计奖商品');
|
||||
assert(content.includes('if ($itemPayFee > 0 && $isCommissionItem)'), 'refundPartial() 仅对计奖商品缩减兑换券');
|
||||
}
|
||||
|
||||
// 2. VoucherService.php 验证
|
||||
const voucherServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/VoucherService.php');
|
||||
assert(fs.existsSync(voucherServicePath), 'VoucherService.php 文件存在');
|
||||
if (fs.existsSync(voucherServicePath)) {
|
||||
const content = fs.readFileSync(voucherServicePath, 'utf-8');
|
||||
assert(content.includes('$rewardService->getQualifiedPayFee($order)'), 'grant() 使用 getQualifiedPayFee 获取计奖实付金额');
|
||||
assert(content.includes('$total = round((float) $qualifiedPayFee * $times, 2)'), '兑换券总额基于计奖实付金额而非整单实付');
|
||||
assert(content.includes("计奖实付{$qualifiedPayFee}"), '兑换券日志 formula 明确标记计奖实付基数');
|
||||
assert(content.includes("'qualified_pay_fee' => $qualifiedPayFee"), '兑换券日志 ext 记录 qualified_pay_fee');
|
||||
}
|
||||
|
||||
// 3. RateService.php 验证
|
||||
const rateServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/RateService.php');
|
||||
assert(fs.existsSync(rateServicePath), 'RateService.php 文件存在');
|
||||
if (fs.existsSync(rateServicePath)) {
|
||||
const content = fs.readFileSync(rateServicePath, 'utf-8');
|
||||
assert(content.includes('public function qualifiedGmv'), 'RateService 包含 qualifiedGmv() 方法');
|
||||
assert(content.includes('CommissionGoods::GOODS_COMMISSION_STATUS_ON'), 'qualifiedGmv 关联计奖状态表');
|
||||
assert(content.includes('"is_commission":false'), 'qualifiedGmv SQL 排除非分销活动项');
|
||||
assert(content.includes("$query->sum('oi.pay_fee')"), 'qualifiedGmv 针对 oi.pay_fee 单品金额求和');
|
||||
}
|
||||
|
||||
// 4. DailySettleService.php 验证
|
||||
const dailySettleServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/DailySettleService.php');
|
||||
assert(fs.existsSync(dailySettleServicePath), 'DailySettleService.php 文件存在');
|
||||
if (fs.existsSync(dailySettleServicePath)) {
|
||||
const content = fs.readFileSync(dailySettleServicePath, 'utf-8');
|
||||
assert(content.includes('$gmv = $rateService->qualifiedGmv($start, $end)'), 'DailySettleService 日结 GMV 改用 qualifiedGmv 计算');
|
||||
assert(!content.includes("bcadd((string) $gmv, (string) $order['pay_fee']"), 'DailySettleService 彻底移除按整单累加 GMV 的漏洞逻辑');
|
||||
}
|
||||
|
||||
// 5. ActiveService.php 验证
|
||||
const activeServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/ActiveService.php');
|
||||
assert(fs.existsSync(activeServicePath), 'ActiveService.php 文件存在');
|
||||
if (fs.existsSync(activeServicePath)) {
|
||||
const content = fs.readFileSync(activeServicePath, 'utf-8');
|
||||
assert(content.includes('$rateService->qualifiedOrderQuery($start, $end)'), 'ActiveService 兜底保活查询复用 qualifiedOrderQuery');
|
||||
}
|
||||
|
||||
// 6. 算法逻辑模拟运行断言
|
||||
console.log('\n--- 场景模拟算法断言 ---');
|
||||
|
||||
function mockGetQualifiedPayFee(order, commissionGoodsMap) {
|
||||
let total = 0;
|
||||
const items = order.items || [];
|
||||
for (const item of items) {
|
||||
if (item.ext && item.ext.is_commission === false) {
|
||||
continue;
|
||||
}
|
||||
if (commissionGoodsMap[item.goods_id] && commissionGoodsMap[item.goods_id].status === 1) {
|
||||
total += Number(item.pay_fee || 0);
|
||||
}
|
||||
}
|
||||
return Math.round(total * 100) / 100;
|
||||
}
|
||||
|
||||
const mockCommissionGoods = {
|
||||
101: { status: 1 }, // 指定计奖酒水
|
||||
102: { status: 0 }, // 未开启计奖
|
||||
// 103 不在映射表中,相当于新添加未配置的商品
|
||||
};
|
||||
|
||||
// 场景 A:纯非计奖新品下单(商品 103,实付 500 元)
|
||||
const pureNewOrder = {
|
||||
id: 1,
|
||||
pay_fee: 500,
|
||||
items: [
|
||||
{ goods_id: 103, pay_fee: 500, ext: {} }
|
||||
]
|
||||
};
|
||||
const pureFee = mockGetQualifiedPayFee(pureNewOrder, mockCommissionGoods);
|
||||
assert(pureFee === 0, '模拟场景 A:纯新品订单计奖实付金额为 0');
|
||||
assert(pureFee > 0 === false, '模拟场景 A:纯新品订单无法通过 isQualified 校验');
|
||||
|
||||
// 场景 B:混合下单(商品 101 计奖 100 元,商品 103 新品 900 元,整单实付 1000 元)
|
||||
const mixedOrder = {
|
||||
id: 2,
|
||||
pay_fee: 1000,
|
||||
items: [
|
||||
{ goods_id: 101, pay_fee: 100, ext: {} },
|
||||
{ goods_id: 103, pay_fee: 900, ext: {} }
|
||||
]
|
||||
};
|
||||
const mixedFee = mockGetQualifiedPayFee(mixedOrder, mockCommissionGoods);
|
||||
assert(mixedFee === 100, '模拟场景 B:混合订单精确提取计奖实付 100 元(排除新品 900 元)');
|
||||
const voucherTimes = 1.5;
|
||||
const voucherTotal = Math.round(mixedFee * voucherTimes * 100) / 100;
|
||||
assert(voucherTotal === 150, '模拟场景 B:兑换券按 100 * 1.5 = 150 发放(修复前会发 1500,成功堵住漏洞)');
|
||||
|
||||
// 场景 C:活动标记不参与分销(商品 101 秒杀特惠 is_commission=false)
|
||||
const promoOrder = {
|
||||
id: 3,
|
||||
pay_fee: 100,
|
||||
items: [
|
||||
{ goods_id: 101, pay_fee: 100, ext: { is_commission: false } }
|
||||
]
|
||||
};
|
||||
const promoFee = mockGetQualifiedPayFee(promoOrder, mockCommissionGoods);
|
||||
assert(promoFee === 0, '模拟场景 C:即便属于计奖商品,活动标记 is_commission=false 亦被正确排除');
|
||||
|
||||
console.log('\n--------------------------------------------------------');
|
||||
console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
|
||||
console.log('========================================================');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* uniapp 退款与售后功能关闭专项自动化验证测试
|
||||
* 覆盖文件:
|
||||
* 1. uniapp/sheep/components/s-order-card/s-order-card.vue (个人中心订单卡片)
|
||||
* 2. uniapp/pages/order/list.vue (订单列表)
|
||||
* 3. uniapp/pages/order/detail.vue (订单详情)
|
||||
* 4. uniapp/sheep/router/index.js (全局路由拦截)
|
||||
* 5. uniapp/pages/order/aftersale/apply.vue (售后申请页面兜底防护)
|
||||
* 6. uniapp/pages/order/aftersale/list.vue (售后列表页面兜底防护)
|
||||
* 7. uniapp/pages/order/aftersale/detail.vue (售后详情页面兜底防护)
|
||||
* 8. uniapp/pages/order/aftersale/log.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(' uniapp 退款与售后功能关闭验证测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// 1. 个人中心订单卡片 (s-order-card.vue)
|
||||
const orderCardPath = path.resolve(__dirname, '../uniapp/sheep/components/s-order-card/s-order-card.vue');
|
||||
assert(fs.existsSync(orderCardPath), 's-order-card.vue 文件存在');
|
||||
if (fs.existsSync(orderCardPath)) {
|
||||
const content = fs.readFileSync(orderCardPath, 'utf-8');
|
||||
assert(!content.includes("title: '售后单'"), 's-order-card 不再包含“售后单”菜单项');
|
||||
assert(!content.includes("type: 'aftersale'"), 's-order-card 不再包含 aftersale 类型的角标');
|
||||
assert(content.includes("title: '待发货'"), 's-order-card 已平滑切换为“待发货”');
|
||||
assert(content.includes("type: 'nosend'"), 's-order-card “待发货”绑定 nosend 待发货角标');
|
||||
assert(content.includes("value: '2'"), 's-order-card “待发货”指向 tabMaps 对应索引');
|
||||
}
|
||||
|
||||
// 2. 订单列表 (list.vue)
|
||||
const orderListPath = path.resolve(__dirname, '../uniapp/pages/order/list.vue');
|
||||
assert(fs.existsSync(orderListPath), 'order/list.vue 文件存在');
|
||||
if (fs.existsSync(orderListPath)) {
|
||||
const content = fs.readFileSync(orderListPath, 'utf-8');
|
||||
assert(!content.includes("btns.includes('aftersale')"), 'order/list.vue 商品条目已移除“申请售后”按钮');
|
||||
assert(!content.includes("btns.includes('re_aftersale')"), 'order/list.vue 商品条目已移除“重新售后”按钮');
|
||||
assert(!content.includes("btns.includes('aftersale_info')"), 'order/list.vue 商品条目已移除“售后详情”按钮');
|
||||
assert(!content.includes("btns.includes('apply_refund')"), 'order/list.vue 订单条目已移除“申请退款”按钮');
|
||||
assert(!content.includes("btns.includes('re_apply_refund')"), 'order/list.vue 订单条目已移除“重新退款”按钮');
|
||||
}
|
||||
|
||||
// 3. 订单详情 (detail.vue)
|
||||
const orderDetailPath = path.resolve(__dirname, '../uniapp/pages/order/detail.vue');
|
||||
assert(fs.existsSync(orderDetailPath), 'order/detail.vue 文件存在');
|
||||
if (fs.existsSync(orderDetailPath)) {
|
||||
const content = fs.readFileSync(orderDetailPath, 'utf-8');
|
||||
assert(!content.includes("btns.includes('aftersale')"), 'order/detail.vue 商品卡片已移除“申请售后”按钮');
|
||||
assert(!content.includes("btns.includes('re_aftersale')"), 'order/detail.vue 商品卡片已移除“重新售后”按钮');
|
||||
assert(!content.includes("btns.includes('aftersale_info')"), 'order/detail.vue 商品卡片已移除“售后详情”按钮');
|
||||
assert(!content.includes("btns?.includes('apply_refund')"), 'order/detail.vue 底部操作栏已移除“申请退款”按钮');
|
||||
assert(!content.includes("btns?.includes('re_apply_refund')"), 'order/detail.vue 底部操作栏已移除“重新退款”按钮');
|
||||
}
|
||||
|
||||
// 4. 全局路由拦截 (router/index.js)
|
||||
const routerPath = path.resolve(__dirname, '../uniapp/sheep/router/index.js');
|
||||
assert(fs.existsSync(routerPath), 'router/index.js 文件存在');
|
||||
if (fs.existsSync(routerPath)) {
|
||||
const content = fs.readFileSync(routerPath, 'utf-8');
|
||||
assert(
|
||||
content.includes("startsWith(page, '/pages/order/aftersale')"),
|
||||
'router/index.js 已拦截 /pages/order/aftersale 路由'
|
||||
);
|
||||
assert(
|
||||
content.includes('退款与售后功能暂未开放'),
|
||||
'router/index.js 具备友好中文拦截提示'
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 售后申请页兜底 (aftersale/apply.vue)
|
||||
const applyPath = path.resolve(__dirname, '../uniapp/pages/order/aftersale/apply.vue');
|
||||
assert(fs.existsSync(applyPath), 'aftersale/apply.vue 文件存在');
|
||||
if (fs.existsSync(applyPath)) {
|
||||
const content = fs.readFileSync(applyPath, 'utf-8');
|
||||
assert(
|
||||
content.includes('退款与售后功能暂未开放'),
|
||||
'aftersale/apply.vue onLoad 与 submit 具备拦截与防提交处理'
|
||||
);
|
||||
assert(
|
||||
content.includes('sheep.$router.back()'),
|
||||
'aftersale/apply.vue 具备自动退回上一页保护'
|
||||
);
|
||||
}
|
||||
|
||||
// 6. 售后列表页兜底 (aftersale/list.vue)
|
||||
const aftersaleListPath = path.resolve(__dirname, '../uniapp/pages/order/aftersale/list.vue');
|
||||
assert(fs.existsSync(aftersaleListPath), 'aftersale/list.vue 文件存在');
|
||||
if (fs.existsSync(aftersaleListPath)) {
|
||||
const content = fs.readFileSync(aftersaleListPath, 'utf-8');
|
||||
assert(
|
||||
content.includes('退款与售后功能暂未开放') && content.includes('sheep.$router.back()'),
|
||||
'aftersale/list.vue 具备自动退回上一页保护'
|
||||
);
|
||||
}
|
||||
|
||||
// 7. 售后详情页兜底 (aftersale/detail.vue)
|
||||
const aftersaleDetailPath = path.resolve(__dirname, '../uniapp/pages/order/aftersale/detail.vue');
|
||||
assert(fs.existsSync(aftersaleDetailPath), 'aftersale/detail.vue 文件存在');
|
||||
if (fs.existsSync(aftersaleDetailPath)) {
|
||||
const content = fs.readFileSync(aftersaleDetailPath, 'utf-8');
|
||||
assert(
|
||||
content.includes('退款与售后功能暂未开放') && content.includes('sheep.$router.back()'),
|
||||
'aftersale/detail.vue 具备自动退回上一页保护'
|
||||
);
|
||||
}
|
||||
|
||||
// 8. 售后进度日志页兜底 (aftersale/log.vue)
|
||||
const aftersaleLogPath = path.resolve(__dirname, '../uniapp/pages/order/aftersale/log.vue');
|
||||
assert(fs.existsSync(aftersaleLogPath), 'aftersale/log.vue 文件存在');
|
||||
if (fs.existsSync(aftersaleLogPath)) {
|
||||
const content = fs.readFileSync(aftersaleLogPath, 'utf-8');
|
||||
assert(
|
||||
content.includes('退款与售后功能暂未开放') && content.includes('sheep.$router.back()'),
|
||||
'aftersale/log.vue 具备自动退回上一页保护'
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(` 测试结果: 共 ${passCount + failCount} 项, 通过: ${passCount}, 失败: ${failCount}`);
|
||||
console.log('========================================================\n');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 积分商城与兑换券完整关联专项自动化测试
|
||||
* 覆盖范围:OrderCreate, Partner listener, confirm.vue, voucher.vue, score-shop.vue, score.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. 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');
|
||||
assert(!content.includes("$this->order_type !== 'score'"), 'calcVoucher 移除了对 score 订单可用额度查询的封锁');
|
||||
assert(!content.includes("$this->order_type === 'score' ? 0"), 'calcVoucher 移除了对 score 订单抵扣额置零的拦截');
|
||||
assert(content.includes('$this->orderData[\'voucher_available\'] = $available'), '积分订单正常赋值 voucher_available');
|
||||
assert(content.includes('$want = max(0, round((float) $this->voucher_fee, 2))'), '积分订单支持提取 voucher_fee 并参与应付现金抵扣');
|
||||
}
|
||||
|
||||
// 2. Partner.php 监听器验证
|
||||
const partnerListenerPath = path.resolve(__dirname, '../addons/shopro/listener/Partner.php');
|
||||
assert(fs.existsSync(partnerListenerPath), 'Partner.php 文件存在');
|
||||
if (fs.existsSync(partnerListenerPath)) {
|
||||
const content = fs.readFileSync(partnerListenerPath, 'utf-8');
|
||||
const consumePos = content.indexOf('(new VoucherService())->consumeByOrder($orderModel)');
|
||||
const scoreReturnPos = content.indexOf("if (($order['type'] ?? '') === 'score')");
|
||||
assert(consumePos !== -1, 'orderPaidAfter 包含 consumeByOrder 核销调用');
|
||||
assert(scoreReturnPos !== -1, 'orderPaidAfter 包含 score 类型拦截');
|
||||
assert(consumePos < scoreReturnPos, '兑换券核销 consumeByOrder 优先于 score 类型 return,确保积分订单成功核销兑换券');
|
||||
}
|
||||
|
||||
// 2.1 VoucherService.php 幂等防重退款验证
|
||||
const voucherServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/VoucherService.php');
|
||||
assert(fs.existsSync(voucherServicePath), 'VoucherService.php 文件存在');
|
||||
if (fs.existsSync(voucherServicePath)) {
|
||||
const content = fs.readFileSync(voucherServicePath, 'utf-8');
|
||||
assert(content.includes('voucher_consume_refund'), 'refundConsume 记录兑换券退款流水');
|
||||
assert(content.includes('$alreadyRefunded'), 'refundConsume 具备幂等防重金额统计');
|
||||
assert(content.includes('$remainVoucher'), 'refundConsume 严格限制退回上限不超过剩余未退额');
|
||||
}
|
||||
|
||||
// 3. 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 voucherBlock = content.slice(content.indexOf('<view class="item-title">兑换券</view>') - 100, content.indexOf('<view class="item-title">兑换券</view>'));
|
||||
assert(!voucherBlock.includes("order_type != 'score'"), 'confirm.vue 移除了兑换券行的 order_type != score 隐藏限制');
|
||||
assert(content.includes('@tap="openVoucher"'), '确认订单页提供 openVoucher 交互入口');
|
||||
assert(content.includes('state.orderInfo.voucher_available'), '展示可用兑换券额度');
|
||||
}
|
||||
|
||||
// 4. 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("sheep.$router.go('/pages/app/score-shop')"), '兑换券中心「去兑换」按钮成功直达积分商城');
|
||||
}
|
||||
|
||||
// 5. score-shop.vue 验证
|
||||
const scoreShopVuePath = path.resolve(__dirname, '../uniapp/pages/app/score-shop.vue');
|
||||
assert(fs.existsSync(scoreShopVuePath), 'score-shop.vue 文件存在');
|
||||
if (fs.existsSync(scoreShopVuePath)) {
|
||||
const content = fs.readFileSync(scoreShopVuePath, 'utf-8');
|
||||
assert(content.includes('userVoucherAvailable'), '积分商城首页引入可用兑换券计算属性');
|
||||
assert(content.includes('当前积分'), '展示当前积分余额看板');
|
||||
assert(content.includes('可用兑换券'), '展示可用兑换券余额看板');
|
||||
assert(content.includes('💡 兑换商品时支持使用兑换券 1:1 抵扣现金部分'), '包含兑换券抵扣说明提示');
|
||||
assert(content.includes('/pages/commission/voucher'), '提供快捷跳转兑换券明细入口');
|
||||
}
|
||||
|
||||
// 6. score.vue 验证
|
||||
const scoreDetailVuePath = path.resolve(__dirname, '../uniapp/pages/goods/score.vue');
|
||||
assert(fs.existsSync(scoreDetailVuePath), 'score.vue 文件存在');
|
||||
if (fs.existsSync(scoreDetailVuePath)) {
|
||||
const content = fs.readFileSync(scoreDetailVuePath, 'utf-8');
|
||||
assert(content.includes('voucher-tip-tag'), '积分商品详情包含兑换券抵扣标签组件');
|
||||
assert(content.includes('支持兑换券抵扣'), '积分商品详情标明支持兑换券抵扣现金');
|
||||
}
|
||||
|
||||
// 7. 结算抵扣算法模拟
|
||||
console.log('\n--- 积分商城订单兑换券抵扣算法模拟 ---');
|
||||
|
||||
function mockCalcVoucher(orderType, userVoucherAvailable, orderPayFee, inputVoucherFee) {
|
||||
let available = userVoucherAvailable;
|
||||
let want = Math.max(0, Number(inputVoucherFee || 0));
|
||||
let apply = Math.min(want, available, orderPayFee);
|
||||
apply = apply > 0 ? apply : 0;
|
||||
let finalPayFee = Math.round((orderPayFee - apply) * 100) / 100;
|
||||
return {
|
||||
applyVoucher: apply,
|
||||
finalPayFee: finalPayFee
|
||||
};
|
||||
}
|
||||
|
||||
// 场景 1:积分商品需 10 积分 + 50 元现金,用户持有 100 元兑换券,申请全额抵扣 50 元
|
||||
const case1 = mockCalcVoucher('score', 100, 50, 50);
|
||||
assert(case1.applyVoucher === 50, '场景 1:成功申请抵扣 50 元兑换券');
|
||||
assert(case1.finalPayFee === 0, '场景 1:现金实付降为 0 元(纯积分 + 兑换券换走)');
|
||||
|
||||
// 场景 2:积分商品需 10 积分 + 50 元现金,用户仅持有 30 元兑换券,申请抵扣 50 元
|
||||
const case2 = mockCalcVoucher('score', 30, 50, 50);
|
||||
assert(case2.applyVoucher === 30, '场景 2:受限于可用额度,最多抵扣 30 元');
|
||||
assert(case2.finalPayFee === 20, '场景 2:差额 20 元需用现金/微信支付');
|
||||
|
||||
// 场景 3:纯积分商品(100 积分 + 0 元现金)
|
||||
const case3 = mockCalcVoucher('score', 100, 0, 50);
|
||||
assert(case3.applyVoucher === 0, '场景 3:应付现金为 0 时,不误扣兑换券');
|
||||
assert(case3.finalPayFee === 0, '场景 3:现金实付保持 0 元');
|
||||
|
||||
// 场景 4:退款防重复返还(幂等保护)模拟
|
||||
function mockRefundConsume(totalVoucher, alreadyRefunded, requestRefundFee = null) {
|
||||
const remain = Math.max(0, totalVoucher - alreadyRefunded);
|
||||
if (remain <= 0) return 0;
|
||||
const fee = requestRefundFee !== null ? Math.min(remain, requestRefundFee) : remain;
|
||||
return fee > 0 ? fee : 0;
|
||||
}
|
||||
const refundRound1 = mockRefundConsume(50, 0);
|
||||
assert(refundRound1 === 50, '场景 4:首次全额退款成功返还 50 元兑换券');
|
||||
const refundRound2 = mockRefundConsume(50, 50); // 重复触发
|
||||
assert(refundRound2 === 0, '场景 4:幂等拦截!重复退款触发时不再返还兑换券,避免双倍扣减已用额度');
|
||||
|
||||
// 场景 5:防套利保护——兑换券抵扣部分不计入新兑换券返还基数
|
||||
function mockGetQualifiedPayFee(orderType, itemsTotal, orderPayFee) {
|
||||
if (orderType === 'score') return 0;
|
||||
return Math.min(itemsTotal, Math.max(0, orderPayFee));
|
||||
}
|
||||
const qualifiedCashOnly = mockGetQualifiedPayFee('normal', 100, 80); // 100元商品,20元券抵扣,实付80
|
||||
assert(qualifiedCashOnly === 80, '场景 5:100元商品使用20元券后,计奖实付严格锁定在80元现金');
|
||||
const qualifiedZeroCash = mockGetQualifiedPayFee('normal', 100, 0); // 100元券全额抵扣,实付0
|
||||
assert(qualifiedZeroCash === 0, '场景 5:全额使用兑换券抵扣时,计奖实付为0,杜绝凭空无限刷券套利');
|
||||
|
||||
console.log('\n--------------------------------------------------------');
|
||||
console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
|
||||
console.log('========================================================');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户