193 行
9.6 KiB
JavaScript
193 行
9.6 KiB
JavaScript
/**
|
||
* 钱包可用余额(money)提现全链路规格与算法模拟测试
|
||
* 覆盖范围:
|
||
* 1. uniapp/pages/pay/withdraw.vue 前端钱包余额提现默认绑定、全部提现与参数透传
|
||
* 2. addons/shopro/service/Withdraw.php 服务层 apply 余额扣除、checkApply 拦截与驳回/失败退还闭环
|
||
* 3. application/admin/model/shopro/Withdraw.php 后台模型 is_money_withdraw 与 display 资产标识
|
||
* 4. application/admin/model/shopro/user/WalletLog.php 钱包日志字典增加 withdraw / withdraw_error
|
||
* 5. application/admin/view/shopro/withdraw/index.html 后台表格专属「钱包余额」标签
|
||
* 6. 提现扣款、手续费计算、超额拦截、审核驳回原路退款守恒性算法模拟断言
|
||
*/
|
||
|
||
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(' 钱包可用余额(money)提现全链路专项测试');
|
||
console.log('========================================================\n');
|
||
|
||
// ----------------------------------------------------
|
||
// 1. withdraw.vue 前端绑定验证
|
||
// ----------------------------------------------------
|
||
console.log('--- 1. 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("walletType: 'money'"), 'state.walletType 默认初始化为 money');
|
||
testAssert(content.includes("userInfo.money || '0.00'"), '顶部主卡片绑定用户钱包可用余额 userInfo.money');
|
||
testAssert(content.includes("onWithdrawAllMoney"), '提供一键全部提现钱包余额方法 onWithdrawAllMoney');
|
||
testAssert(content.includes("onWithdrawAllCommission"), '兼容提供全部提现存量佣金方法 onWithdrawAllCommission');
|
||
testAssert(content.includes("payload.wallet_type = 'money'"), '提交时对齐传递 wallet_type: money');
|
||
testAssert(content.includes("Number(payload.money) > Number(userInfo.value?.money || 0)"), '严格校验提现金额不得超过钱包可用余额');
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 2. Withdraw.php 服务层业务闭环
|
||
// ----------------------------------------------------
|
||
console.log('\n--- 2. 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("$walletType = $params['wallet_type'] ?? 'money';"), '提现钱包默认缺省读取为 money');
|
||
testAssert(content.includes("$walletType = 'money';"), '支持自动升级或使用 money 提现类型');
|
||
testAssert(content.includes("WalletService::change($this->user, 'money'"), 'apply 支持扣除用户 money 钱包');
|
||
testAssert(content.includes("ShoproException('钱包可用余额不足')"), 'checkApply 校验余额不足抛出明确中文异常');
|
||
testAssert(content.includes("$isMoney = (isset($info['wallet_type']) && $info['wallet_type'] === 'money')"), '识别 isMoney 提现类型');
|
||
testAssert(content.includes("'提现驳回退还余额'"), 'handleRefuse 识别余额提现驳回并原路退回余额');
|
||
testAssert(content.includes("'提现失败退还余额'"), 'handleFail 识别余额提现打款失败并原路退回余额');
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 3. 后台 Withdraw.php 模型与 index.html 视图验证
|
||
// ----------------------------------------------------
|
||
console.log('\n--- 3. 后台 Withdraw 模型与审核视图展示验证 ---');
|
||
const adminWithdrawModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/Withdraw.php');
|
||
testAssert(fs.existsSync(adminWithdrawModelPath), 'application/admin/model/shopro/Withdraw.php 存在');
|
||
if (fs.existsSync(adminWithdrawModelPath)) {
|
||
const content = fs.readFileSync(adminWithdrawModelPath, 'utf-8');
|
||
testAssert(content.includes("'is_money_withdraw'"), '$append 追加属性包含 is_money_withdraw');
|
||
testAssert(content.includes("public function getIsMoneyWithdrawAttr"), '提供 getIsMoneyWithdrawAttr 获取器方法');
|
||
testAssert(content.includes("$display['提现资产'] = '钱包余额'"), 'getWithdrawInfoDisplayAttr 注入 提现资产: 钱包余额');
|
||
}
|
||
|
||
const adminWithdrawViewPath = path.resolve(__dirname, '../../application/admin/view/shopro/withdraw/index.html');
|
||
testAssert(fs.existsSync(adminWithdrawViewPath), 'application/admin/view/shopro/withdraw/index.html 存在');
|
||
if (fs.existsSync(adminWithdrawViewPath)) {
|
||
const content = fs.readFileSync(adminWithdrawViewPath, 'utf-8');
|
||
testAssert(content.includes("scope.row.is_money_withdraw"), '表格金额列支持判断 is_money_withdraw');
|
||
testAssert(content.includes("钱包余额"), '展示专属 钱包余额 标签');
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 4. WalletLog 模型字典验证
|
||
// ----------------------------------------------------
|
||
console.log('\n--- 4. WalletLog 钱包日志事件字典注册验证 ---');
|
||
const walletLogModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/user/WalletLog.php');
|
||
testAssert(fs.existsSync(walletLogModelPath), 'application/admin/model/shopro/user/WalletLog.php 存在');
|
||
if (fs.existsSync(walletLogModelPath)) {
|
||
const content = fs.readFileSync(walletLogModelPath, 'utf-8');
|
||
testAssert(content.includes("'withdraw' => '申请提现'"), 'money 事件字典注册 withdraw');
|
||
testAssert(content.includes("'withdraw_error' => '提现失败-返还余额'"), 'money 事件字典注册 withdraw_error');
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 5. 算法逻辑与资金守恒模拟
|
||
// ----------------------------------------------------
|
||
console.log('\n--- 5. 余额提现扣款、手续费与退回资金守恒算法模拟 ---');
|
||
|
||
// 模拟用户账户:余额 48602.00,佣金 0.00
|
||
const userState = {
|
||
money: 48602.00,
|
||
commission: 0.00
|
||
};
|
||
|
||
const withdrawConfig = {
|
||
min_amount: 100.00,
|
||
max_amount: 1000.00,
|
||
charge_rate: 0.01 // 1%
|
||
};
|
||
|
||
function simulateApplyWithdraw(user, amount, walletType) {
|
||
if (amount < withdrawConfig.min_amount) {
|
||
throw new Error(`提现金额不能少于 ${withdrawConfig.min_amount}元`);
|
||
}
|
||
if (amount > withdrawConfig.max_amount) {
|
||
throw new Error(`提现金额不能大于 ${withdrawConfig.max_amount}元`);
|
||
}
|
||
const charge = Math.round(amount * withdrawConfig.charge_rate * 100) / 100;
|
||
const totalDeduct = Math.round((amount + charge) * 100) / 100;
|
||
|
||
if (walletType === 'money') {
|
||
if (user.money < totalDeduct) {
|
||
throw new Error('钱包可用余额不足');
|
||
}
|
||
user.money = Math.round((user.money - totalDeduct) * 100) / 100;
|
||
} else {
|
||
if (user.commission < totalDeduct) {
|
||
throw new Error('可提现佣金不足');
|
||
}
|
||
user.commission = Math.round((user.commission - totalDeduct) * 100) / 100;
|
||
}
|
||
|
||
return {
|
||
amount,
|
||
charge_fee: charge,
|
||
wallet_type: walletType,
|
||
total_deduct: totalDeduct
|
||
};
|
||
}
|
||
|
||
function simulateRefuseWithdraw(user, record) {
|
||
const totalRefund = Math.round((record.amount + record.charge_fee) * 100) / 100;
|
||
if (record.wallet_type === 'money') {
|
||
user.money = Math.round((user.money + totalRefund) * 100) / 100;
|
||
} else {
|
||
user.commission = Math.round((user.commission + totalRefund) * 100) / 100;
|
||
}
|
||
}
|
||
|
||
// 模拟场景 A:正常提现 1000 元
|
||
const initialMoney = userState.money;
|
||
const withdrawRecord = simulateApplyWithdraw(userState, 1000.00, 'money');
|
||
testAssert(withdrawRecord.amount === 1000.00, '申请提现本金 1000.00 元');
|
||
testAssert(withdrawRecord.charge_fee === 10.00, '1% 提现手续费 10.00 元');
|
||
testAssert(userState.money === 47592.00, '用户钱包余额原子扣减 1010.00 元,剩余 47592.00 元');
|
||
testAssert(userState.commission === 0.00, '佣金账户不受影响保持 0.00 元');
|
||
|
||
// 模拟场景 B:提现驳回退款
|
||
simulateRefuseWithdraw(userState, withdrawRecord);
|
||
testAssert(userState.money === initialMoney, '提现驳回后原路全额退还本金及手续费,余额精准恢复 48602.00 元');
|
||
|
||
// 模拟场景 C:超额提现拦截
|
||
let caughtError = null;
|
||
try {
|
||
simulateApplyWithdraw(userState, 50000.00, 'money');
|
||
} catch (e) {
|
||
caughtError = e.message;
|
||
}
|
||
testAssert(caughtError.includes('提现金额不能大于'), '超出配置最大单笔提现限额被拦截');
|
||
|
||
// 模拟场景 D:余额不足拦截
|
||
let balanceError = null;
|
||
try {
|
||
const poorUser = { money: 50.00, commission: 0.00 };
|
||
simulateApplyWithdraw(poorUser, 500.00, 'money');
|
||
} catch (e) {
|
||
balanceError = e.message;
|
||
}
|
||
testAssert(balanceError === '钱包可用余额不足', '余额不足提现请求被严密拦截并输出友好提示');
|
||
|
||
console.log('\n========================================================');
|
||
console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
|
||
console.log('========================================================');
|
||
|
||
if (failCount > 0) {
|
||
process.exit(1);
|
||
}
|