194 行
9.0 KiB
JavaScript
194 行
9.0 KiB
JavaScript
/**
|
|
* 历史收益全量结转至钱包余额专项测试
|
|
* 覆盖范围:
|
|
* 1. WalletMigrationService 业务服务契约与原子性逻辑
|
|
* 2. PartnerTransferCommission 控制台命令
|
|
* 3. Shopro.php 注册命令配置
|
|
* 4. transfer_all_commission_to_money.php 生产独立运维脚本
|
|
* 5. Reward.php 后台接口与防误触确认校验
|
|
* 6. 算法行为模拟:多用户结转、资金守恒、零余额跳过、幂等重入、单用户异常容灾隔离
|
|
*/
|
|
|
|
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. WalletMigrationService.php 验证
|
|
// ----------------------------------------------------
|
|
console.log('--- 1. WalletMigrationService.php 业务规范验证 ---');
|
|
const migrationServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/WalletMigrationService.php');
|
|
assert(fs.existsSync(migrationServicePath), 'WalletMigrationService.php 文件存在');
|
|
if (fs.existsSync(migrationServicePath)) {
|
|
const content = fs.readFileSync(migrationServicePath, 'utf-8');
|
|
assert(content.includes('public function preview()'), '包含 preview() 试算预览方法');
|
|
assert(content.includes('public function transferAll('), '包含 transferAll() 全量结转执行方法');
|
|
assert(content.includes('Db::transaction(function ()'), '结转过程使用数据库事务');
|
|
assert(content.includes('lock(true)'), '读取用户使用排他行锁 lock(true) 防并发脏读');
|
|
assert(content.includes("Wallet::change($user, 'commission', -$amount, 'transfer_to_money'"), '调用 Wallet::change 扣减佣金钱包');
|
|
assert(content.includes("Wallet::change($user, 'money', $amount, 'transfer_by_commission'"), '调用 Wallet::change 增加钱包余额');
|
|
assert(content.includes('bcadd('), '资金累计采用高精度 bcadd 避免浮点误差');
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// 2. PartnerTransferCommission.php 控制台命令验证
|
|
// ----------------------------------------------------
|
|
console.log('\n--- 2. PartnerTransferCommission.php 控制台命令验证 ---');
|
|
const consoleCmdPath = path.resolve(__dirname, '../../addons/shopro/console/PartnerTransferCommission.php');
|
|
assert(fs.existsSync(consoleCmdPath), 'PartnerTransferCommission.php 文件存在');
|
|
if (fs.existsSync(consoleCmdPath)) {
|
|
const content = fs.readFileSync(consoleCmdPath, 'utf-8');
|
|
assert(content.includes("'shopro:partner:transfer-commission'"), '命令名称定义为 shopro:partner:transfer-commission');
|
|
assert(content.includes("'dry-run'"), '命令支持 --dry-run 试算选项');
|
|
assert(content.includes("'force'"), '命令支持 --force 强制执行选项');
|
|
assert(content.includes('WalletMigrationService'), '命令调用 WalletMigrationService 服务');
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// 3. Shopro.php 自动命令注册验证
|
|
// ----------------------------------------------------
|
|
console.log('\n--- 3. Shopro.php 注册命令验证 ---');
|
|
const shoproPath = path.resolve(__dirname, '../../addons/shopro/Shopro.php');
|
|
assert(fs.existsSync(shoproPath), 'Shopro.php 文件存在');
|
|
if (fs.existsSync(shoproPath)) {
|
|
const content = fs.readFileSync(shoproPath, 'utf-8');
|
|
assert(content.includes("'addons\\shopro\\console\\PartnerTransferCommission'"), 'Shopro.php addDefaultCommands 已注册 PartnerTransferCommission');
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// 4. transfer_all_commission_to_money.php 生产脚本验证
|
|
// ----------------------------------------------------
|
|
console.log('\n--- 4. transfer_all_commission_to_money.php 运维脚本验证 ---');
|
|
const scriptPath = path.resolve(__dirname, '../../scripts/transfer_all_commission_to_money.php');
|
|
assert(fs.existsSync(scriptPath), 'transfer_all_commission_to_money.php 脚本存在');
|
|
if (fs.existsSync(scriptPath)) {
|
|
const content = fs.readFileSync(scriptPath, 'utf-8');
|
|
assert(content.includes('WalletMigrationService'), '脚本引入 WalletMigrationService 执行');
|
|
assert(content.includes('--dry-run'), '脚本支持 --dry-run');
|
|
assert(content.includes('--force'), '脚本安全防御支持 --force');
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// 5. Reward.php 后台接口验证
|
|
// ----------------------------------------------------
|
|
console.log('\n--- 5. Reward.php 后台接口验证 ---');
|
|
const rewardPath = path.resolve(__dirname, '../../application/admin/controller/shopro/partner/Reward.php');
|
|
assert(fs.existsSync(rewardPath), 'Reward.php 文件存在');
|
|
if (fs.existsSync(rewardPath)) {
|
|
const content = fs.readFileSync(rewardPath, 'utf-8');
|
|
assert(content.includes('public function transferAllCommissionPreview()'), '包含 transferAllCommissionPreview 后台预览接口');
|
|
assert(content.includes('public function transferAllCommission()'), '包含 transferAllCommission 后台执行接口');
|
|
assert(content.includes('confirm !== 1'), '正式结转接口必须传递 confirm=1 进行二次安全防御');
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// 6. 算法行为模拟断言
|
|
// ----------------------------------------------------
|
|
console.log('\n--- 6. 资金结转算法与守恒性模拟断言 ---');
|
|
|
|
function mockTransferService(users) {
|
|
let successCount = 0;
|
|
let failCount = 0;
|
|
let totalTransferred = 0;
|
|
const logs = [];
|
|
|
|
users.forEach(u => {
|
|
if (u.simulateError) {
|
|
failCount++;
|
|
return;
|
|
}
|
|
const comm = parseFloat(u.commission);
|
|
if (comm <= 0) {
|
|
return; // 跳过
|
|
}
|
|
const money = parseFloat(u.money);
|
|
// 执行原子互转
|
|
u.commission = '0.00';
|
|
u.money = (money + comm).toFixed(2);
|
|
totalTransferred += comm;
|
|
successCount++;
|
|
|
|
logs.push({
|
|
user_id: u.id,
|
|
type: 'commission',
|
|
amount: -comm,
|
|
event: 'transfer_to_money',
|
|
});
|
|
logs.push({
|
|
user_id: u.id,
|
|
type: 'money',
|
|
amount: comm,
|
|
event: 'transfer_by_commission',
|
|
});
|
|
});
|
|
|
|
return {
|
|
successCount,
|
|
failCount,
|
|
totalTransferred: totalTransferred.toFixed(2),
|
|
logs,
|
|
};
|
|
}
|
|
|
|
// 测试数据集
|
|
const initialUsers = [
|
|
{ id: 1, nickname: '张三', money: '100.00', commission: '250.50' },
|
|
{ id: 2, nickname: '李四', money: '0.00', commission: '398.00' },
|
|
{ id: 3, nickname: '王五', money: '50.00', commission: '0.00' }, // 佣金为0
|
|
{ id: 4, nickname: '赵六', money: '80.00', commission: '120.00', simulateError: true }, // 模拟异常
|
|
];
|
|
|
|
// 计算初始总资金
|
|
const initialTotalAssets = initialUsers.reduce((sum, u) => {
|
|
return sum + parseFloat(u.money) + parseFloat(u.commission);
|
|
}, 0);
|
|
|
|
const result = mockTransferService(initialUsers);
|
|
|
|
assert(result.successCount === 2, '成功结转 2 位有佣金的会员');
|
|
assert(result.failCount === 1, '异常会员被安全隔离统计为 1 笔失败');
|
|
assert(result.totalTransferred === '648.50', '结转总金额精确匹配 250.50 + 398.00 = 648.50');
|
|
|
|
// 校验各个用户结转后的状态
|
|
assert(initialUsers[0].commission === '0.00', '张三佣金归零');
|
|
assert(initialUsers[0].money === '350.50', '张三钱包余额从 100 增至 350.50');
|
|
assert(initialUsers[1].commission === '0.00', '李四佣金归零');
|
|
assert(initialUsers[1].money === '398.00', '李四钱包余额从 0 增至 398.00');
|
|
assert(initialUsers[2].commission === '0.00' && initialUsers[2].money === '50.00', '王五佣金本为0,保持不变');
|
|
assert(initialUsers[3].commission === '120.00' && initialUsers[3].money === '80.00', '赵六发生异常,资金状态未被污染');
|
|
|
|
// 资金守恒定理断言:结转前后总资产必须完全相等
|
|
const finalTotalAssets = initialUsers.reduce((sum, u) => {
|
|
return sum + parseFloat(u.money) + parseFloat(u.commission);
|
|
}, 0);
|
|
assert(Math.abs(initialTotalAssets - finalTotalAssets) < 0.0001, '资金守恒断言:结转前后系统总资产完全守恒');
|
|
|
|
// 幂等性测试断言:再次运行
|
|
const result2 = mockTransferService(initialUsers.filter(u => !u.simulateError));
|
|
assert(result2.successCount === 0, '幂等性断言:再次运行成功数为 0');
|
|
assert(result2.totalTransferred === '0.00', '幂等性断言:再次运行结转金额为 0.00');
|
|
|
|
console.log('\n========================================================');
|
|
console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
|
|
console.log('========================================================');
|
|
|
|
if (failCount > 0) {
|
|
process.exit(1);
|
|
}
|