132 行
6.7 KiB
JavaScript
132 行
6.7 KiB
JavaScript
/**
|
||
* 提现手续费「内扣模式」核心规格断言与算法模拟测试
|
||
*
|
||
* 核心业务规则:
|
||
* 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, '所有内扣测试必须全部通过');
|