文件
Henglianshutong/tests/partner/verify_internal_charge_withdraw_spec.js

132 行
6.7 KiB
JavaScript

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

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

/**
* 提现手续费「内扣模式」核心规格断言与算法模拟测试
*
* 核心业务规则:
* 1. 用户提现申请本金为 M,手续费为 C(M * charge_rate);
* 2. 用户余额仅扣除申请本金 M(支持余额有多少就提多少,全部提现不报错);
* 3. 实际打款金额(paid_fee / transfer_amount / trans_amount)为 (M - C);
* 4. 审核驳回或打款失败时,原路全额退还本金 M(平台不倒贴、用户不少退);
* 5. 前端实时计算并展示内扣后预计到账金额。
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');
let passCount = 0;
let failCount = 0;
function testAssert(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. 服务层 Withdraw.php 静态代码结构验证
console.log('--- 1. addons/shopro/service/Withdraw.php 服务层内扣闭环验证 ---');
const withdrawServicePath = path.resolve(__dirname, '../../addons/shopro/service/Withdraw.php');
testAssert(fs.existsSync(withdrawServicePath), 'Withdraw.php 服务文件存在');
if (fs.existsSync(withdrawServicePath)) {
const content = fs.readFileSync(withdrawServicePath, 'utf-8');
testAssert(content.includes("WalletService::change($this->user, 'money', -$money, 'withdraw'"), 'apply 提现申请仅从钱包余额扣除本金 -$money(内扣模式)');
testAssert(content.includes("bccomp((string)($this->user->money ?? 0), (string)$money, 2) < 0"), 'checkApply 校验余额只需大于等于提现本金 money(无需累加手续费)');
testAssert(content.includes("ShoproException('扣除手续费后实际到账金额必须大于0元')"), 'checkApply 严格拦截扣除手续费后实际到账小于等于0的请求');
testAssert(content.includes("$withdraw->paid_fee = bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2);"), 'handleWithdraw 实际打款金额为 (amount - charge_fee)');
testAssert(content.includes("'transfer_amount' => bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2)"), 'handleWechatTransfer 微信转账金额为 (amount - charge_fee)');
testAssert(content.includes("'trans_amount' => bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2)"), 'handleTransfer 支付宝转账金额为 (amount - charge_fee)');
testAssert(content.includes("WalletService::change($this->user, 'money', $withdraw->amount, 'withdraw_error'"), 'handleRefuse 驳回退还金额精准为本金 amount(绝不多退或少退)');
}
// 2. 数据模型层 Withdraw.php 验证
console.log('\n--- 2. application/admin/model/shopro/Withdraw.php 模型预计到账验证 ---');
const withdrawModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/Withdraw.php');
testAssert(fs.existsSync(withdrawModelPath), 'Withdraw.php 模型存在');
if (fs.existsSync(withdrawModelPath)) {
const content = fs.readFileSync(withdrawModelPath, 'utf-8');
testAssert(content.includes("'real_amount'"), '$append 追加 real_amount 属性');
testAssert(content.includes("getRealAmountAttr"), '提供 getRealAmountAttr 获取器计算 net 净到账金额');
testAssert(content.includes("$display['预计到账']"), 'withdraw_info_display 注入「预计到账」字段供后台审核核对');
}
// 3. 前端提现界面 withdraw.vue 验证
console.log('\n--- 3. uniapp/pages/pay/withdraw.vue 前端内扣提示验证 ---');
const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw.vue');
testAssert(fs.existsSync(withdrawVuePath), 'withdraw.vue 文件存在');
if (fs.existsSync(withdrawVuePath)) {
const content = fs.readFileSync(withdrawVuePath, 'utf-8');
testAssert(content.includes("estimatedArrivalMoney"), '提供 estimatedArrivalMoney 实时计算内扣到账金额');
testAssert(content.includes("estimatedChargeFee"), '提供 estimatedChargeFee 实时计算内扣手续费');
testAssert(content.includes("预计到账:¥{{ estimatedArrivalMoney }}"), '界面输入金额时实时展现预计到账');
testAssert(content.includes("手续费内扣"), '界面明确提示手续费从提现金额内扣');
}
// 4. 内扣业务逻辑与资金守恒算法模拟
console.log('\n--- 4. 内扣模式算法与资金守恒严格断言 ---');
const chargeRate = 0.06; // 6% 手续费
// 场景 A:用户有 1000 元余额,执行“一键全部提现 1000 元”
const userA = {
money: 1000.00,
commission: 500.00
};
function applyWithdrawInternal(user, amount) {
if (user.money < amount) {
throw new Error('钱包可用余额不足');
}
const charge = Math.round(amount * chargeRate * 100) / 100;
const realPaid = Math.round((amount - charge) * 100) / 100;
if (realPaid <= 0) {
throw new Error('扣除手续费后实际到账金额必须大于0元');
}
// 内扣:仅扣除提现申请本金
user.money = Math.round((user.money - amount) * 100) / 100;
return {
amount,
charge_fee: charge,
paid_fee: realPaid,
wallet_type: 'money'
};
}
function refuseWithdrawInternal(user, order) {
// 驳回全额退还本金
user.money = Math.round((user.money + order.amount) * 100) / 100;
}
const orderA = applyWithdrawInternal(userA, 1000.00);
testAssert(orderA.amount === 1000.00, '申请提现本金 1000.00 元');
testAssert(orderA.charge_fee === 60.00, '手续费 60.00 元(6%)');
testAssert(orderA.paid_fee === 940.00, '实际打款到账 940.00 元(1000 - 60)');
testAssert(userA.money === 0.00, '【核心】用户余额精准扣减 1000.00 元,剩余 0.00 元,全部提现零报错');
testAssert(userA.commission === 500.00, '佣金钱包完全不受影响保持 500.00 元');
// 场景 B:审核驳回
refuseWithdrawInternal(userA, orderA);
testAssert(userA.money === 1000.00, '审核驳回后钱包余额原路全额退还至 1000.00 元');
// 场景 C:超额提现拦截
let hasIntercept = false;
try {
applyWithdrawInternal(userA, 1500.00);
} catch (e) {
hasIntercept = (e.message === '钱包可用余额不足');
}
testAssert(hasIntercept, '超额提现精准拦截并抛出【钱包可用余额不足】');
console.log(`\n========================================================`);
console.log(` 测试结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
console.log(`========================================================\n`);
assert.strictEqual(failCount, 0, '所有内扣测试必须全部通过');