文件
Henglianshutong/tests/partner/verify_money_withdraw_flow_spec.js

193 行
9.6 KiB
JavaScript

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

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

/**
* 钱包可用余额(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);
}