fix(export): 修复Excel导出zipstream库PHP8兼容性与临时文件安全流式输出
这个提交包含在:
@@ -181,6 +181,9 @@ class Export
|
||||
$result = [
|
||||
'file_path' => str_replace(ROOT_PATH, '项目目录/', $save_file)
|
||||
];
|
||||
|
||||
$writer = new $class_name($spreadsheet);
|
||||
$writer->save($save_file);
|
||||
} else {
|
||||
$save_file = $fileName . '-' . date('YmdHis') . '.xlsx';
|
||||
|
||||
@@ -188,20 +191,37 @@ class Export
|
||||
'file_name' => $save_file
|
||||
];
|
||||
|
||||
// 优先安全写入临时文件,确保 Excel 结构与数据完整无误后再输出下载头
|
||||
$tempDir = RUNTIME_PATH . 'temp/export/';
|
||||
if (!is_dir($tempDir)) {
|
||||
@mkdir($tempDir, 0755, true);
|
||||
}
|
||||
$tempFile = $tempDir . 'exp_' . md5(uniqid((string)mt_rand(), true)) . '.xlsx';
|
||||
|
||||
$writer = new $class_name($spreadsheet);
|
||||
$writer->save($tempFile);
|
||||
|
||||
if (!file_exists($tempFile) || filesize($tempFile) <= 0) {
|
||||
@unlink($tempFile);
|
||||
error_stop('生成导出文件失败,请检查数据完整性');
|
||||
}
|
||||
|
||||
if (ob_get_level() > 0) {
|
||||
@ob_end_clean();
|
||||
}
|
||||
header('pragma:public');
|
||||
header("Content-type:application/octet-stream; charset=utf-8; name=" . urlencode($save_file));
|
||||
header("Content-Disposition:attachment; filename=" . urlencode($save_file)); //attachment新窗口打印inline本窗口打印
|
||||
|
||||
$save_file = 'php://output';
|
||||
}
|
||||
|
||||
$writer = new $class_name($spreadsheet);
|
||||
$writer->save($save_file);
|
||||
$fileSize = filesize($tempFile);
|
||||
header('Pragma: public');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
header("Content-Disposition: attachment; filename=" . urlencode($save_file) . "; filename*=utf-8''" . urlencode($save_file));
|
||||
header('Content-Length: ' . $fileSize);
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
|
||||
readfile($tempFile);
|
||||
@unlink($tempFile);
|
||||
|
||||
if ($this->config['save_type'] != 'save') {
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
fastcgi_finish_request();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ class Withdraw extends Common
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
if (defined('ROOT_PATH') && is_file(ROOT_PATH . 'addons/shopro/helper/helper.php')) {
|
||||
require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
|
||||
}
|
||||
$this->model = new WithdrawModel;
|
||||
$this->logModel = new WithdrawLogModel;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"fastadminnet/fastadmin-mailer": "^2.0.0",
|
||||
"overtrue/pinyin": "^3.0",
|
||||
"phpoffice/phpspreadsheet": "^1.29.1",
|
||||
"maennchen/zipstream-php": "^2.4",
|
||||
"overtrue/wechat": "^4.6",
|
||||
"ext-json": "*",
|
||||
"ext-curl": "*",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
/**
|
||||
* 方案 B 提现 API 核心场景模拟测试(端到端)
|
||||
* 覆盖:
|
||||
* 1. 正常场景:申请提现扣除商城余额 money,驳回原路退还 money
|
||||
* 2. 业务失败场景:申请提现金额超过商城余额,捕获【商城可用余额不足】
|
||||
* 1. 正常场景:申请提现扣除钱包余额 money,驳回原路退还 money
|
||||
* 2. 业务失败场景:申请提现金额超过钱包余额,捕获【钱包可用余额不足】
|
||||
* 3. 参数错误场景:申请金额低于最低限额 100 元,捕获【提现金额不能少于 100元】
|
||||
*/
|
||||
|
||||
@@ -83,22 +83,22 @@ try {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// 场景 2:业务失败场景(提现金额超出商城可用余额)
|
||||
// 场景 2:业务失败场景(提现金额超出钱包可用余额)
|
||||
// -------------------------------------------------------------------
|
||||
echo "\n--- 2. 业务失败场景:提现金额超出商城可用余额 --- \n";
|
||||
echo "\n--- 2. 业务失败场景:提现金额超出钱包可用余额 --- \n";
|
||||
try {
|
||||
$withdrawService->apply(array_merge($bankInfo, [
|
||||
'money' => 800, // 800+48=848,在单笔限额 1000 内,但当前可用余额为 554.65,必触发余额不足
|
||||
]));
|
||||
assertCondition(false, '未能拦截余额不足的提现请求');
|
||||
} catch (ShoproException $e) {
|
||||
assertCondition(strpos($e->getMessage(), '商城可用余额不足') !== false, "成功拦截超额业务拒绝: " . $e->getMessage());
|
||||
assertCondition(strpos($e->getMessage(), '钱包可用余额不足') !== false, "成功拦截超额业务拒绝: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// 场景 3:正常提现场景(内扣模式:提现 100 元,仅扣除 100 元商城余额,手续费内扣)
|
||||
// 场景 3:正常提现场景(内扣模式:提现 100 元,仅扣除 100 元钱包余额,手续费内扣)
|
||||
// -------------------------------------------------------------------
|
||||
echo "\n--- 3. 正常业务场景:提现 100 元并校验商城余额内扣 --- \n";
|
||||
echo "\n--- 3. 正常业务场景:提现 100 元并校验钱包余额内扣 --- \n";
|
||||
|
||||
// 为确保测试环境用户余额充足,临时补充 200 元测试金(测试完成后全额扣回)
|
||||
$originalMoney = floatval($user->money);
|
||||
@@ -129,23 +129,23 @@ if ($testWithdraw) {
|
||||
$afterCommission = floatval($afterUser->commission);
|
||||
|
||||
$expectedMoney = round($beforeMoney - 100.00, 2);
|
||||
assertCondition(round($afterMoney, 2) === $expectedMoney, "【内扣】商城余额仅扣除本金 ¥100.00(¥{$beforeMoney} -> ¥{$afterMoney})");
|
||||
assertCondition(round($afterMoney, 2) === $expectedMoney, "【内扣】钱包余额仅扣除本金 ¥100.00(¥{$beforeMoney} -> ¥{$afterMoney})");
|
||||
assertCondition($afterCommission === $beforeCommission, "佣金钱包分毫不差,保持 ¥{$beforeCommission}");
|
||||
assertCondition($testWithdraw->withdraw_info['wallet_type'] === 'money', "提现订单记录标记为 wallet_type: money");
|
||||
assertCondition($testWithdraw->withdraw_info['提现资产'] === '商城余额', "提现订单记录提现资产: 商城余额");
|
||||
assertCondition($testWithdraw->withdraw_info['提现资产'] === '钱包余额', "提现订单记录提现资产: 钱包余额");
|
||||
|
||||
// 校验内扣净到账金额 (amount - charge_fee = 100 - 6 = 94.00)
|
||||
$expectedPaid = bcsub((string)$testWithdraw->amount, (string)$testWithdraw->charge_fee, 2);
|
||||
assertCondition($expectedPaid === '94.00', "内扣手续费 6.00 元后实际打款金额为 ¥{$expectedPaid}");
|
||||
|
||||
// 测试完成后原路驳回并退还余额,恢复环境数据
|
||||
echo "\n--- 4. 容灾闭环测试:审核驳回退还商城余额本金 --- \n";
|
||||
echo "\n--- 4. 容灾闭环测试:审核驳回退还钱包余额本金 --- \n";
|
||||
Db::startTrans();
|
||||
try {
|
||||
$withdrawService->handleRefuse($testWithdraw, 'API自动化测试驳回退还');
|
||||
Db::commit();
|
||||
$restoredUser = \app\admin\model\shopro\user\User::get($userId);
|
||||
assertCondition(round(floatval($restoredUser->money), 2) === round($beforeMoney, 2), "驳回后商城余额原路退还本金 ¥100.00 至 ¥{$beforeMoney}");
|
||||
assertCondition(round(floatval($restoredUser->money), 2) === round($beforeMoney, 2), "驳回后钱包余额原路退还本金 ¥100.00 至 ¥{$beforeMoney}");
|
||||
assertCondition(floatval($restoredUser->commission) === $beforeCommission, "佣金钱包继续保持 ¥{$beforeCommission}");
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
|
||||
@@ -40,12 +40,20 @@ assert(modelContent.includes('is_numeric($rate)'), 'getChargeRateFormatAttr 包
|
||||
assert(modelContent.includes('is_numeric($amount)'), 'getRealAmountAttr 包含 is_numeric 校验');
|
||||
console.log('[测试点 2] Withdraw 模型源代码加固断言通过');
|
||||
|
||||
// 3. 检查 addons/shopro/library/Export.php 包含缓冲检查与下载退出
|
||||
// 3. 检查 addons/shopro/library/Export.php 包含缓冲检查、临时文件生成与安全输出
|
||||
const exportContent = fs.readFileSync(path.resolve(__dirname, '../../addons/shopro/library/Export.php'), 'utf8');
|
||||
assert(exportContent.includes('ob_get_level() > 0'), 'Export.php 包含 ob_get_level() 检查防 Notice');
|
||||
assert(exportContent.includes("if ($this->config['save_type'] != 'save')"), 'Export.php 包含下载模式检查');
|
||||
assert(exportContent.includes('temp/export/'), 'Export.php 优先写入临时文件校验');
|
||||
assert(exportContent.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'), 'Export.php 使用标准 xlsx mime 类型');
|
||||
assert(exportContent.includes('Content-Length'), 'Export.php 包含 Content-Length 响应头');
|
||||
assert(exportContent.includes("if ($this->config['save_type'] == 'save')"), 'Export.php 包含 save_type 判断');
|
||||
assert(exportContent.includes('exit;'), 'Export.php 下载完成安全退出');
|
||||
console.log('[测试点 3] Export.php 缓冲区安全清理与下载模式防污染退出断言通过');
|
||||
console.log('[测试点 3] Export.php 临时文件安全写入、标准MIME与下载防污染退出断言通过');
|
||||
|
||||
// 3.1 检查 composer.json 锁定 maennchen/zipstream-php: ^2.4 防 PHP 8.1 readonly 语法错误
|
||||
const composerContent = fs.readFileSync(path.resolve(__dirname, '../../composer.json'), 'utf8');
|
||||
assert(composerContent.includes('"maennchen/zipstream-php": "^2.4"'), 'composer.json 锁定 zipstream-php: ^2.4');
|
||||
console.log('[测试点 3.1] composer.json 兼容性依赖锁定断言通过');
|
||||
|
||||
// 4. 检查 application/admin/controller/shopro/Withdraw.php 包含异常捕获、单 key 汇总、时间格式化
|
||||
const controllerContent = fs.readFileSync(path.resolve(__dirname, '../../application/admin/controller/shopro/Withdraw.php'), 'utf8');
|
||||
|
||||
@@ -30,7 +30,7 @@ assert(rewardModelContent.includes("'wallet_type_text'"), 'Reward 模型 $append
|
||||
assert(rewardModelContent.includes('function getWalletTypeAttr'), '包含 getWalletTypeAttr 属性获取器');
|
||||
assert(rewardModelContent.includes('function getWalletTypeTextAttr'), '包含 getWalletTypeTextAttr 属性获取器');
|
||||
assert(rewardModelContent.includes("PartnerConfig::rewardWalletType()"), '现金分润缺省联动 PartnerConfig::rewardWalletType()');
|
||||
assert(rewardModelContent.includes("'商城余额'"), 'wallet_type_text 映射包含 商城余额');
|
||||
assert(rewardModelContent.includes("'钱包余额'"), 'wallet_type_text 映射包含 钱包余额');
|
||||
assert(rewardModelContent.includes("'佣金钱包'"), 'wallet_type_text 映射包含 佣金钱包');
|
||||
assert(rewardModelContent.includes("'分红dmepa'"), 'wallet_type_text 映射包含 分红dmepa');
|
||||
|
||||
@@ -52,7 +52,7 @@ console.log('\n--- 3. 订单详情视图 (order/detail.html) 验证 ---');
|
||||
const orderDetailViewPath = path.join(rootDir, 'application/admin/view/shopro/order/order/detail.html');
|
||||
assert(fs.existsSync(orderDetailViewPath), 'order/detail.html 文件存在');
|
||||
const orderDetailViewContent = fs.readFileSync(orderDetailViewPath, 'utf8');
|
||||
assert(orderDetailViewContent.includes('直入商城余额'), '分成顶部横幅包含「直入商城余额」专属 Tag 提示');
|
||||
assert(orderDetailViewContent.includes('直入钱包余额'), '分成顶部横幅包含「直入钱包余额」专属 Tag 提示');
|
||||
assert(orderDetailViewContent.includes('label="入账账户"'), '分成明细表格包含「入账账户」列');
|
||||
assert(orderDetailViewContent.includes('scope.row.wallet_type_text'), '入账账户列展示 scope.row.wallet_type_text');
|
||||
|
||||
@@ -86,7 +86,7 @@ assert(userControllerContent.includes("$data['commission'] = number_format"), 'U
|
||||
const userDetailViewPath = path.join(rootDir, 'application/admin/view/shopro/user/user/detail.html');
|
||||
assert(fs.existsSync(userDetailViewPath), 'user/detail.html 文件存在');
|
||||
const userDetailViewContent = fs.readFileSync(userDetailViewPath, 'utf8');
|
||||
assert(userDetailViewContent.includes('当前商城余额: ¥{{ state.detail.money'), '会员详情卡片1明确为「当前商城余额」');
|
||||
assert(userDetailViewContent.includes('当前钱包余额: ¥{{ state.detail.money'), '会员详情卡片1明确为「当前钱包余额」');
|
||||
assert(userDetailViewContent.includes('历史未转佣金: ¥{{ state.detail.commission'), '会员详情卡片1明确为「历史未转佣金」');
|
||||
assert(userDetailViewContent.includes('state.detail.total_income || state.detail.income_total_cash'), '卡片1优先绑定统一对齐的 total_income');
|
||||
assert(userDetailViewContent.match(/log\.tabActive == 'partner_reward'[\s\S]*label="入账账户"/), 'Tab 1 收益中心流水包含「入账账户」列');
|
||||
@@ -96,7 +96,7 @@ assert(fs.existsSync(userIndexViewPath), 'user/index.html 文件存在');
|
||||
const userIndexViewContent = fs.readFileSync(userIndexViewPath, 'utf8');
|
||||
assert(!userIndexViewContent.includes('可提现收益:'), '会员列表已彻底移除误导性文案「可提现收益」');
|
||||
assert(userIndexViewContent.includes('累计收益:'), '会员列表展示「累计收益」');
|
||||
assert(userIndexViewContent.includes('商城余额: ¥{{ scope.row.money'), '会员列表展示「商城余额」');
|
||||
assert(userIndexViewContent.includes('钱包余额: ¥{{ scope.row.money'), '会员列表展示「钱包余额」');
|
||||
assert(userIndexViewContent.includes('未转佣金: ¥{{ scope.row.commission'), '会员列表展示「未转佣金」');
|
||||
|
||||
// --- 6. 分销商详情与佣金明细 (commission) 验证 ---
|
||||
@@ -111,15 +111,15 @@ assert(agentControllerContent.includes('$detail->user_commission = number_format
|
||||
const agentDetailViewPath = path.join(rootDir, 'application/admin/view/shopro/commission/agent/detail.html');
|
||||
assert(fs.existsSync(agentDetailViewPath), 'commission/agent/detail.html 文件存在');
|
||||
const agentDetailViewContent = fs.readFileSync(agentDetailViewPath, 'utf8');
|
||||
assert(agentDetailViewContent.includes('直入商城余额'), '分销商详情收益卡片带有「直入商城余额」Tag');
|
||||
assert(agentDetailViewContent.includes('商城可用余额:'), '分销商资产信息包含「商城可用余额」');
|
||||
assert(agentDetailViewContent.includes('直入钱包余额'), '分销商详情收益卡片带有「直入钱包余额」Tag');
|
||||
assert(agentDetailViewContent.includes('钱包可用余额:'), '分销商资产信息包含「钱包可用余额」');
|
||||
assert(agentDetailViewContent.includes('历史未转佣金:'), '分销商资产信息包含「历史未转佣金」');
|
||||
assert(agentDetailViewContent.includes('label="历史分销佣金"'), 'Tab 3 明确标注为「历史分销佣金」');
|
||||
|
||||
const commRewardViewPath = path.join(rootDir, 'application/admin/view/shopro/commission/reward/index.html');
|
||||
assert(fs.existsSync(commRewardViewPath), 'commission/reward/index.html 文件存在');
|
||||
const commRewardViewContent = fs.readFileSync(commRewardViewPath, 'utf8');
|
||||
assert(commRewardViewContent.includes('当前收益分成已全额直入商城余额'), '历史佣金明细顶部包含直入商城余额提示说明');
|
||||
assert(commRewardViewContent.includes('当前收益分成已全额直入钱包余额'), '历史佣金明细顶部包含直入钱包余额提示说明');
|
||||
|
||||
const commRewardControllerPath = path.join(rootDir, 'application/admin/controller/shopro/commission/Reward.php');
|
||||
assert(fs.existsSync(commRewardControllerPath), 'commission/Reward.php 文件存在');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 累计收益统计口径与用户商城余额统一专项测试
|
||||
* 累计收益统计口径与用户钱包余额统一专项测试
|
||||
* 覆盖范围:
|
||||
* 1. IdentityService attachAgent / attachProfile 累计收益口径统一与兜底
|
||||
* 2. User profile 接口统一映射 balance = money 及累计收益补偿
|
||||
@@ -7,9 +7,9 @@
|
||||
* 4. WalletLog 收益统计扣除互转重复项,真实反映收益与扣减(防误导)
|
||||
* 5. uniapp 视图端全面对齐:
|
||||
* - rewards.vue 累计已结算收益卡片与本月统计
|
||||
* - account-info.vue 累计收益与商城余额
|
||||
* - commission.vue 累计总收益、周期收益与商城可用余额
|
||||
* - withdraw.vue 商城可用余额提示与收益入账说明
|
||||
* - account-info.vue 累计收益与钱包余额
|
||||
* - commission.vue 累计总收益、周期收益与钱包可用余额
|
||||
* - withdraw.vue 钱包可用余额提示与收益入账说明
|
||||
* 6. 算法逻辑模拟:无重复统计、真实收益判定与资金守恒
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,7 @@ function assert(condition, message) {
|
||||
}
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 累计收益统计口径与用户商城余额统一专项测试');
|
||||
console.log(' 累计收益统计口径与用户钱包余额统一专项测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -56,7 +56,7 @@ const userControllerPath = path.resolve(__dirname, '../../addons/shopro/controll
|
||||
assert(fs.existsSync(userControllerPath), 'addons/shopro/controller/user/User.php 文件存在');
|
||||
if (fs.existsSync(userControllerPath)) {
|
||||
const content = fs.readFileSync(userControllerPath, 'utf-8');
|
||||
assert(content.includes("$data['balance'] = $user->money"), 'profile 明确将 balance 映射为商城余额 money');
|
||||
assert(content.includes("$data['balance'] = $user->money"), 'profile 明确将 balance 映射为钱包余额 money');
|
||||
assert(content.includes('$userRewardCash > 0'), '非分销商身份但有分成收益时,执行累计收益兜底统计');
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ assert(fs.existsSync(accountInfoVuePath), 'account-info.vue 文件存在');
|
||||
if (fs.existsSync(accountInfoVuePath)) {
|
||||
const content = fs.readFileSync(accountInfoVuePath, 'utf-8');
|
||||
assert(content.includes("agentInfo?.total_income || userInfo?.total_income"), '累计收益具备 agentInfo 与 userInfo 双重对齐兜底');
|
||||
assert(content.includes("userInfo?.money || '0.00'"), '商城余额绑定 userInfo.money');
|
||||
assert(content.includes("userInfo?.money || '0.00'"), '钱包余额绑定 userInfo.money');
|
||||
}
|
||||
|
||||
// 5.3 commission.vue
|
||||
@@ -125,8 +125,8 @@ const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw
|
||||
assert(fs.existsSync(withdrawVuePath), 'withdraw.vue 文件存在');
|
||||
if (fs.existsSync(withdrawVuePath)) {
|
||||
const content = fs.readFileSync(withdrawVuePath, 'utf-8');
|
||||
assert(content.includes('商城可用余额:¥{{ userInfo.money'), 'withdraw.vue 同步展示商城可用余额');
|
||||
assert(content.includes('收益分成已全额直接入账商城余额'), 'withdraw.vue 显著提示收益已全额直接入账商城余额');
|
||||
assert(content.includes('钱包可用余额:¥{{ userInfo.money'), 'withdraw.vue 同步展示钱包可用余额');
|
||||
assert(content.includes('收益分成已全额直接入账钱包余额'), 'withdraw.vue 显著提示收益已全额直接入账钱包余额');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
@@ -36,7 +36,7 @@ const withdrawServicePath = path.resolve(__dirname, '../../addons/shopro/service
|
||||
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("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)');
|
||||
@@ -80,7 +80,7 @@ const userA = {
|
||||
|
||||
function applyWithdrawInternal(user, amount) {
|
||||
if (user.money < amount) {
|
||||
throw new Error('商城可用余额不足');
|
||||
throw new Error('钱包可用余额不足');
|
||||
}
|
||||
const charge = Math.round(amount * chargeRate * 100) / 100;
|
||||
const realPaid = Math.round((amount - charge) * 100) / 100;
|
||||
@@ -113,16 +113,16 @@ testAssert(userA.commission === 500.00, '佣金钱包完全不受影响保持 50
|
||||
|
||||
// 场景 B:审核驳回
|
||||
refuseWithdrawInternal(userA, orderA);
|
||||
testAssert(userA.money === 1000.00, '审核驳回后商城余额原路全额退还至 1000.00 元');
|
||||
testAssert(userA.money === 1000.00, '审核驳回后钱包余额原路全额退还至 1000.00 元');
|
||||
|
||||
// 场景 C:超额提现拦截
|
||||
let hasIntercept = false;
|
||||
try {
|
||||
applyWithdrawInternal(userA, 1500.00);
|
||||
} catch (e) {
|
||||
hasIntercept = (e.message === '商城可用余额不足');
|
||||
hasIntercept = (e.message === '钱包可用余额不足');
|
||||
}
|
||||
testAssert(hasIntercept, '超额提现精准拦截并抛出【商城可用余额不足】');
|
||||
testAssert(hasIntercept, '超额提现精准拦截并抛出【钱包可用余额不足】');
|
||||
|
||||
console.log(`\n========================================================`);
|
||||
console.log(` 测试结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* 商城可用余额(money)提现全链路规格与算法模拟测试
|
||||
* 钱包可用余额(money)提现全链路规格与算法模拟测试
|
||||
* 覆盖范围:
|
||||
* 1. uniapp/pages/pay/withdraw.vue 前端商城余额提现默认绑定、全部提现与参数透传
|
||||
* 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 后台表格专属「商城余额」标签
|
||||
* 5. application/admin/view/shopro/withdraw/index.html 后台表格专属「钱包余额」标签
|
||||
* 6. 提现扣款、手续费计算、超额拦截、审核驳回原路退款守恒性算法模拟断言
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,7 @@ function testAssert(condition, message) {
|
||||
}
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 商城可用余额(money)提现全链路专项测试');
|
||||
console.log(' 钱包可用余额(money)提现全链路专项测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -39,11 +39,11 @@ 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("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)"), '严格校验提现金额不得超过商城可用余额');
|
||||
testAssert(content.includes("Number(payload.money) > Number(userInfo.value?.money || 0)"), '严格校验提现金额不得超过钱包可用余额');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -57,7 +57,7 @@ if (fs.existsSync(withdrawServicePath)) {
|
||||
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("ShoproException('钱包可用余额不足')"), 'checkApply 校验余额不足抛出明确中文异常');
|
||||
testAssert(content.includes("$isMoney = (isset($info['wallet_type']) && $info['wallet_type'] === 'money')"), '识别 isMoney 提现类型');
|
||||
testAssert(content.includes("'提现驳回退还余额'"), 'handleRefuse 识别余额提现驳回并原路退回余额');
|
||||
testAssert(content.includes("'提现失败退还余额'"), 'handleFail 识别余额提现打款失败并原路退回余额');
|
||||
@@ -73,7 +73,7 @@ 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 注入 提现资产: 商城余额');
|
||||
testAssert(content.includes("$display['提现资产'] = '钱包余额'"), 'getWithdrawInfoDisplayAttr 注入 提现资产: 钱包余额');
|
||||
}
|
||||
|
||||
const adminWithdrawViewPath = path.resolve(__dirname, '../../application/admin/view/shopro/withdraw/index.html');
|
||||
@@ -81,7 +81,7 @@ testAssert(fs.existsSync(adminWithdrawViewPath), 'application/admin/view/shopro/
|
||||
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("商城余额"), '展示专属 商城余额 标签');
|
||||
testAssert(content.includes("钱包余额"), '展示专属 钱包余额 标签');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -125,7 +125,7 @@ function simulateApplyWithdraw(user, amount, walletType) {
|
||||
|
||||
if (walletType === 'money') {
|
||||
if (user.money < totalDeduct) {
|
||||
throw new Error('商城可用余额不足');
|
||||
throw new Error('钱包可用余额不足');
|
||||
}
|
||||
user.money = Math.round((user.money - totalDeduct) * 100) / 100;
|
||||
} else {
|
||||
@@ -157,7 +157,7 @@ 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.money === 47592.00, '用户钱包余额原子扣减 1010.00 元,剩余 47592.00 元');
|
||||
testAssert(userState.commission === 0.00, '佣金账户不受影响保持 0.00 元');
|
||||
|
||||
// 模拟场景 B:提现驳回退款
|
||||
@@ -181,7 +181,7 @@ try {
|
||||
} catch (e) {
|
||||
balanceError = e.message;
|
||||
}
|
||||
testAssert(balanceError === '商城可用余额不足', '余额不足提现请求被严密拦截并输出友好提示');
|
||||
testAssert(balanceError === '钱包可用余额不足', '余额不足提现请求被严密拦截并输出友好提示');
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* 方案 B 核心业务规范自动化断言测试:
|
||||
* 规则:收益进入哪个钱包,提现就走哪个钱包提现(统一为「商城余额 money」)
|
||||
* 规则:收益进入哪个钱包,提现就走哪个钱包提现(统一为「钱包余额 money」)
|
||||
* 覆盖范围:
|
||||
* 1. addons/shopro/service/Withdraw.php 服务层 apply 默认 money 提现、checkApply 校验 money、驳回/失败退还 money
|
||||
* 2. uniapp/pages/pay/withdraw.vue 前端默认 money 提现与 payload 校验
|
||||
* 3. application/admin/model/shopro/Withdraw.php 与 index.html 专属商城余额标签与获取器
|
||||
* 3. application/admin/model/shopro/Withdraw.php 与 index.html 专属钱包余额标签与获取器
|
||||
* 4. application/admin/model/shopro/user/WalletLog.php 字典对齐
|
||||
* 5. 扣款、费率、超额拦截、驳回退回、失败退回与数据校正守恒算法模拟断言
|
||||
*/
|
||||
@@ -27,7 +27,7 @@ function testAssert(condition, message) {
|
||||
}
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 方案 B(收益与提现统一走商城余额 money)全链路测试');
|
||||
console.log(' 方案 B(收益与提现统一走钱包余额 money)全链路测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// 1. 服务层 Withdraw.php 验证
|
||||
@@ -36,13 +36,13 @@ const withdrawServicePath = path.resolve(__dirname, '../../addons/shopro/service
|
||||
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 = $params['wallet_type'] ?? 'money';"), '提现钱包默认缺省值为 money(钱包余额)');
|
||||
testAssert(content.includes("WalletService::change($this->user, 'money'"), 'apply 提现成功扣除 money 钱包');
|
||||
testAssert(content.includes("ShoproException('商城可用余额不足')"), 'checkApply 严格校验商城余额不足并提示明确异常');
|
||||
testAssert(content.includes("ShoproException('钱包可用余额不足')"), 'checkApply 严格校验钱包余额不足并提示明确异常');
|
||||
testAssert(content.includes("bccomp((string)($this->user->money ?? 0)"), '使用 bccomp 高精度比较用户余额');
|
||||
testAssert(content.includes("$isMoney = (isset($info['wallet_type']) && $info['wallet_type'] === 'money')"), '正确识别 isMoney 提现类型');
|
||||
testAssert(content.includes("'提现驳回退还余额'"), 'handleRefuse 驳回退款精准退回用户商城余额');
|
||||
testAssert(content.includes("'提现失败退还余额'"), 'handleFail 失败退款精准退回用户商城余额');
|
||||
testAssert(content.includes("'提现驳回退还余额'"), 'handleRefuse 驳回退款精准退回用户钱包余额');
|
||||
testAssert(content.includes("'提现失败退还余额'"), 'handleFail 失败退款精准退回用户钱包余额');
|
||||
}
|
||||
|
||||
// 2. 前端提现页面 withdraw.vue 验证
|
||||
@@ -52,7 +52,7 @@ 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("userInfo.money || '0.00'"), '顶部展示用户钱包余额 userInfo.money');
|
||||
testAssert(content.includes("payload.wallet_type = 'money'"), '提现提交显式指定 wallet_type: money');
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ testAssert(fs.existsSync(withdrawModelPath), 'Withdraw.php 模型存在');
|
||||
if (fs.existsSync(withdrawModelPath)) {
|
||||
const content = fs.readFileSync(withdrawModelPath, 'utf-8');
|
||||
testAssert(content.includes("getIsMoneyWithdrawAttr"), '提供 getIsMoneyWithdrawAttr 获取器');
|
||||
testAssert(content.includes("$display['提现资产'] = '商城余额'"), '注入 提现资产: 商城余额');
|
||||
testAssert(content.includes("$display['提现资产'] = '钱包余额'"), '注入 提现资产: 钱包余额');
|
||||
}
|
||||
|
||||
const walletLogPath = path.resolve(__dirname, '../../application/admin/model/shopro/user/WalletLog.php');
|
||||
@@ -87,7 +87,7 @@ function applyWithdrawMoney(user, amount) {
|
||||
const charge = Math.round(amount * chargeRate * 100) / 100;
|
||||
const totalDeduct = Math.round((amount + charge) * 100) / 100;
|
||||
if (user.money < totalDeduct) {
|
||||
throw new Error('商城可用余额不足');
|
||||
throw new Error('钱包可用余额不足');
|
||||
}
|
||||
user.money = Math.round((user.money - totalDeduct) * 100) / 100;
|
||||
return { amount, charge, totalDeduct, wallet_type: 'money' };
|
||||
@@ -102,7 +102,7 @@ const snapCommission = testUser.commission;
|
||||
const order1 = applyWithdrawMoney(testUser, 1000.00);
|
||||
testAssert(order1.amount === 1000.00, '提现本金 1000.00 元');
|
||||
testAssert(order1.charge === 60.00, '6% 手续费 60.00 元');
|
||||
testAssert(testUser.money === 554.65, '商城余额由 1614.65 扣减至 554.65');
|
||||
testAssert(testUser.money === 554.65, '钱包余额由 1614.65 扣减至 554.65');
|
||||
testAssert(testUser.commission === snapCommission, '佣金钱包未受任何触碰,保持 806.00');
|
||||
|
||||
// 场景 2:超额提现拦截
|
||||
@@ -110,13 +110,13 @@ let interceptPassed = false;
|
||||
try {
|
||||
applyWithdrawMoney(testUser, 600.00); // 需扣 636,剩余 554.65 不足
|
||||
} catch (e) {
|
||||
interceptPassed = (e.message === '商城可用余额不足');
|
||||
interceptPassed = (e.message === '钱包可用余额不足');
|
||||
}
|
||||
testAssert(interceptPassed, '余额不足拦截有效,抛出【商城可用余额不足】');
|
||||
testAssert(interceptPassed, '余额不足拦截有效,抛出【钱包可用余额不足】');
|
||||
|
||||
// 场景 3:审核驳回退回
|
||||
refundWithdrawMoney(testUser, order1);
|
||||
testAssert(testUser.money === 1614.65, '提现驳回后商城余额原路全额退还至 1614.65');
|
||||
testAssert(testUser.money === 1614.65, '提现驳回后钱包余额原路全额退还至 1614.65');
|
||||
testAssert(testUser.commission === snapCommission, '佣金钱包继续保持 806.00 不受影响');
|
||||
|
||||
// 场景 4:针对订单 W202601240278270146002000 的数据校正逻辑断言
|
||||
@@ -130,7 +130,7 @@ const fixDeduct = 1060.00;
|
||||
user20BeforeFix.money = Math.round((user20BeforeFix.money - fixDeduct) * 100) / 100;
|
||||
user20BeforeFix.commission = Math.round((user20BeforeFix.commission + fixDeduct) * 100) / 100;
|
||||
|
||||
testAssert(user20BeforeFix.money === 554.65, '校正后用户 20 商城余额为 554.65(扣除了提现的 1060 元)');
|
||||
testAssert(user20BeforeFix.money === 554.65, '校正后用户 20 钱包余额为 554.65(扣除了提现的 1060 元)');
|
||||
testAssert(user20BeforeFix.commission === 1866.00, '校正后用户 20 佣金返还为 1866.00(恢复了历史存量佣金)');
|
||||
|
||||
console.log(`\n========================================================`);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 收益分成直接加到商城余额专项验证测试
|
||||
* 收益分成直接加到钱包余额专项验证测试
|
||||
* 覆盖范围:
|
||||
* 1. PartnerConfig (rewardWalletType 默认 money)
|
||||
* 2. PartnerRewardService (account 走 money 入账,refund 智能回滚)
|
||||
* 3. DailySettleService (cashShare 走 money 入账,clearDaily 智能回滚)
|
||||
* 4. 后台 Reward 控制器 (add 走 money 入账,back 智能回滚)
|
||||
* 5. WalletLog 模型 (eventMap['money'] 补全收益事件,getEventTextAttr 动态解析)
|
||||
* 6. 用户端组件 account-info.vue (展示商城余额并跳转 /pages/user/wallet/money)
|
||||
* 6. 用户端组件 account-info.vue (展示钱包余额并跳转 /pages/user/wallet/money)
|
||||
* 7. 业务逻辑与回滚算法行为模拟断言
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,7 @@ function assert(condition, message) {
|
||||
}
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 收益分成直接加到商城余额专项测试');
|
||||
console.log(' 收益分成直接加到钱包余额专项测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -39,7 +39,7 @@ assert(fs.existsSync(partnerConfigPath), 'PartnerConfig.php 文件存在');
|
||||
if (fs.existsSync(partnerConfigPath)) {
|
||||
const content = fs.readFileSync(partnerConfigPath, 'utf-8');
|
||||
assert(content.includes('public static function rewardWalletType()'), '包含 rewardWalletType() 收益入账钱包解析方法');
|
||||
assert(content.includes("self::get('partner_reward_wallet', 'money')"), '默认收益入账钱包缺省为 money (商城余额)');
|
||||
assert(content.includes("self::get('partner_reward_wallet', 'money')"), '默认收益入账钱包缺省为 money (钱包余额)');
|
||||
assert(content.includes("in_array($type, ['money', 'commission'])"), '安全限定钱包类型在合法区间');
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ assert(fs.existsSync(partnerRewardServicePath), 'PartnerRewardService.php 文件
|
||||
if (fs.existsSync(partnerRewardServicePath)) {
|
||||
const content = fs.readFileSync(partnerRewardServicePath, 'utf-8');
|
||||
assert(content.includes('$walletType = PartnerConfig::rewardWalletType();'), 'account() 动态获取收益入账钱包类型');
|
||||
assert(content.includes('Wallet::change($userId, $walletType, $amount, $type,'), 'account() 使用动态钱包入账(直接入商城余额)');
|
||||
assert(content.includes('Wallet::change($userId, $walletType, $amount, $type,'), 'account() 使用动态钱包入账(直接入钱包余额)');
|
||||
assert(content.includes("AgentModel::where('user_id', $userId)->setInc('total_income', $amount);"), '保留分销商累计收益 total_income 累加');
|
||||
assert(content.includes('$walletLog = \\app\\admin\\model\\shopro\\user\\WalletLog::where('), 'refund() 退款时查询原入账钱包记录');
|
||||
assert(content.includes("$targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();"), 'refund() 智能根据原入账类型回滚对应钱包');
|
||||
@@ -68,7 +68,7 @@ assert(fs.existsSync(dailySettleServicePath), 'DailySettleService.php 文件存
|
||||
if (fs.existsSync(dailySettleServicePath)) {
|
||||
const content = fs.readFileSync(dailySettleServicePath, 'utf-8');
|
||||
assert(content.includes('$walletType = PartnerConfig::rewardWalletType();'), 'cashShare() 获取收益入账钱包类型');
|
||||
assert(content.includes('Wallet::change($userId, $walletType, $each, $type,'), 'cashShare() 日结酒道馆/区县分成直接加到商城余额');
|
||||
assert(content.includes('Wallet::change($userId, $walletType, $each, $type,'), 'cashShare() 日结酒道馆/区县分成直接加到钱包余额');
|
||||
assert(content.includes("AgentModel::where('user_id', $userId)->setInc('total_income', $each);"), 'cashShare() 累计更新 total_income');
|
||||
assert(content.includes("$targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();"), 'clearDaily() 重跑日结时智能识别并回滚对应钱包');
|
||||
}
|
||||
@@ -82,7 +82,7 @@ assert(fs.existsSync(rewardCtrlPath), 'Reward.php 文件存在');
|
||||
if (fs.existsSync(rewardCtrlPath)) {
|
||||
const content = fs.readFileSync(rewardCtrlPath, 'utf-8');
|
||||
assert(content.includes('$walletType = PartnerConfig::rewardWalletType();'), 'add() 手工补发获取收益入账钱包类型');
|
||||
assert(content.includes('Wallet::change($params[\'user_id\'], $walletType, $amount, \'reward_income\''), 'add() 手工补发直接加到商城余额');
|
||||
assert(content.includes('Wallet::change($params[\'user_id\'], $walletType, $amount, \'reward_income\''), 'add() 手工补发直接加到钱包余额');
|
||||
assert(content.includes("$targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();"), 'back() 手工撤销智能回滚对应钱包');
|
||||
}
|
||||
|
||||
@@ -117,9 +117,9 @@ const accountInfoPath = path.resolve(__dirname, '../../uniapp/pages/commission/c
|
||||
assert(fs.existsSync(accountInfoPath), 'account-info.vue 文件存在');
|
||||
if (fs.existsSync(accountInfoPath)) {
|
||||
const content = fs.readFileSync(accountInfoPath, 'utf-8');
|
||||
assert(content.includes('<view class="item-title">商城余额(元)</view>'), '卡片标签已变更为「商城余额(元)」');
|
||||
assert(content.includes("userInfo?.money || '0.00'"), '读取 userInfo.money 商城余额');
|
||||
assert(content.includes("sheep.$router.go('/pages/user/wallet/money')"), '点击商城余额卡片直达 /pages/user/wallet/money');
|
||||
assert(content.includes('<view class="item-title">钱包余额(元)</view>'), '卡片标签已变更为「钱包余额(元)」');
|
||||
assert(content.includes("userInfo?.money || '0.00'"), '读取 userInfo.money 钱包余额');
|
||||
assert(content.includes("sheep.$router.go('/pages/user/wallet/money')"), '点击钱包余额卡片直达 /pages/user/wallet/money');
|
||||
assert(content.includes("sheep.$router.go('/pages/commission/rewards')"), '查看明细直达收益明细');
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ const mockUser = { id: 10, money: '100.00', commission: '0.00' };
|
||||
const mockAgent = { user_id: 10, total_income: '500.00' };
|
||||
const log1 = mockAccountReward(mockUser, mockAgent, 398.00, 'push3');
|
||||
|
||||
assert(mockUser.money === '498.00', '模拟即时奖入账:用户商城余额从 100 增至 498.00');
|
||||
assert(mockUser.money === '498.00', '模拟即时奖入账:用户钱包余额从 100 增至 498.00');
|
||||
assert(mockUser.commission === '0.00', '模拟即时奖入账:佣金钱包维持 0.00 未动');
|
||||
assert(mockAgent.total_income === '898.00', '模拟即时奖入账:分销商累计收益从 500 累加至 898.00');
|
||||
assert(log1.type === 'money' && log1.event === 'push3', '产生正确的 money 钱包变更流水');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 历史收益全量结转至商城余额专项测试
|
||||
* 历史收益全量结转至钱包余额专项测试
|
||||
* 覆盖范围:
|
||||
* 1. WalletMigrationService 业务服务契约与原子性逻辑
|
||||
* 2. PartnerTransferCommission 控制台命令
|
||||
@@ -26,7 +26,7 @@ function assert(condition, message) {
|
||||
}
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 历史收益全量结转至商城余额专项测试');
|
||||
console.log(' 历史收益全量结转至钱包余额专项测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -42,7 +42,7 @@ if (fs.existsSync(migrationServicePath)) {
|
||||
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("Wallet::change($user, 'money', $amount, 'transfer_by_commission'"), '调用 Wallet::change 增加钱包余额');
|
||||
assert(content.includes('bcadd('), '资金累计采用高精度 bcadd 避免浮点误差');
|
||||
}
|
||||
|
||||
@@ -167,9 +167,9 @@ assert(result.totalTransferred === '648.50', '结转总金额精确匹配 250.50
|
||||
|
||||
// 校验各个用户结转后的状态
|
||||
assert(initialUsers[0].commission === '0.00', '张三佣金归零');
|
||||
assert(initialUsers[0].money === '350.50', '张三商城余额从 100 增至 350.50');
|
||||
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[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', '赵六发生异常,资金状态未被污染');
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 2. application/admin/controller/shopro/user/WalletLog.php 后台佣金与收益流水联查
|
||||
* 3. application/admin/view/shopro/user/user/detail.html 资产卡片与明细表格对齐
|
||||
* 4. uniapp/pages/user/wallet/commission.vue 用户收益页面卡片/按钮/标签/提示文案对齐
|
||||
* 5. uniapp/pages/user/wallet/money.vue 商城余额明细页面 tab 筛选与动态指示胶囊对齐
|
||||
* 5. uniapp/pages/user/wallet/money.vue 钱包余额明细页面 tab 筛选与动态指示胶囊对齐
|
||||
* 6. 核心行为算法模拟:历史流水+直入余额流水联查、收益分成专属过滤、收入支出统计守恒
|
||||
*/
|
||||
|
||||
@@ -40,7 +40,7 @@ if (fs.existsSync(userWalletLogPath)) {
|
||||
assert(content.includes("$type === 'commission'"), '包含对 commission 类型的特殊聚合处理');
|
||||
assert(content.includes("where('type', 'commission')"), '查询条件包含历史佣金流水');
|
||||
assert(content.includes("where('type', 'money')->whereIn('event', $rewardEvents)"), '查询条件联查直入余额的收益分成流水');
|
||||
assert(content.includes("'transfer_by_commission'"), 'rewardEvents 包含转入商城余额事件 transfer_by_commission');
|
||||
assert(content.includes("'transfer_by_commission'"), 'rewardEvents 包含转入钱包余额事件 transfer_by_commission');
|
||||
assert(content.includes("'transfer_to_money'"), 'rewardEvents 包含结转事件 transfer_to_money');
|
||||
assert(content.includes("case 'reward':"), '支持 tab=reward 收益分成专属筛选');
|
||||
assert(content.includes("'reward_income' => $rewardIncome"), '接口返回收益分成累计金额统计');
|
||||
@@ -69,11 +69,11 @@ assert(fs.existsSync(detailHtmlPath), 'detail.html 文件存在');
|
||||
if (fs.existsSync(detailHtmlPath)) {
|
||||
const content = fs.readFileSync(detailHtmlPath, 'utf-8');
|
||||
assert(content.includes('收益中心累计收益 (元)'), '资产卡片1标题更新为「收益中心累计收益 (元)」');
|
||||
assert(content.includes('当前商城余额: ¥{{ state.detail.money') || content.includes('已直入商城余额: ¥{{ state.detail.money'), '展示当前商城余额(已直入余额)字段');
|
||||
assert(content.includes('当前钱包余额: ¥{{ state.detail.money') || content.includes('已直入钱包余额: ¥{{ state.detail.money'), '展示当前钱包余额(已直入余额)字段');
|
||||
assert(content.includes('历史未转佣金: ¥{{ state.detail.commission'), '展示历史未转佣金字段');
|
||||
assert(content.includes('name="commission">佣金与收益流水</el-tab-pane>') || content.includes('label="佣金与收益流水" name="commission"'), 'Tab 4 名称更新为「佣金与收益流水」');
|
||||
assert(content.includes("label=\"钱包类别\" min-width=\"110\" v-if=\"log.tabActive=='commission'\""), '明细表格包含钱包类别列');
|
||||
assert(content.includes("scope.row.type == 'money' ? '商城余额' : '佣金钱包'"), '钱包类别使用 Tag 区分商城余额与佣金钱包');
|
||||
assert(content.includes("scope.row.type == 'money' ? '钱包余额' : '佣金钱包'"), '钱包类别使用 Tag 区分钱包余额与佣金钱包');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -84,7 +84,7 @@ const commissionVuePath = path.resolve(__dirname, '../../uniapp/pages/user/walle
|
||||
assert(fs.existsSync(commissionVuePath), 'commission.vue 文件存在');
|
||||
if (fs.existsSync(commissionVuePath)) {
|
||||
const content = fs.readFileSync(commissionVuePath, 'utf-8');
|
||||
assert(content.includes('商城可用余额(元)'), '顶部卡片主金额标题变更为「商城可用余额(元)」');
|
||||
assert(content.includes('钱包可用余额(元)'), '顶部卡片主金额标题变更为「钱包可用余额(元)」');
|
||||
assert(content.includes('userInfo.money'), '顶部卡片主金额绑定为 userInfo.money');
|
||||
assert(content.includes('/pages/goods/list'), '卡片操作区提供「去消费」直达按钮');
|
||||
assert(content.includes('Number(userInfo.commission || 0) > 0'), '历史佣金大于0时才显示「转余额」操作');
|
||||
@@ -93,7 +93,7 @@ if (fs.existsSync(commissionVuePath)) {
|
||||
assert(content.includes("name: '收益分成'") && content.includes("value: 'reward'"), 'tabMaps 增加「收益分成」(value: reward) 选项');
|
||||
assert(content.includes("item.type === 'money'"), '流水记录列表对直入余额项进行识别');
|
||||
assert(content.includes('已入余额'), '直入余额项显示「已入余额」专属胶囊标签');
|
||||
assert(content.includes('收益分成已全额直接入账商城可用余额'), '底部提示文案明确说明收益直接入账商城余额');
|
||||
assert(content.includes('收益分成已全额直接入账钱包可用余额'), '底部提示文案明确说明收益直接入账钱包余额');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -123,7 +123,7 @@ const rewardEvents = [
|
||||
// 模拟钱包日志混合数据池
|
||||
const mockLogs = [
|
||||
{ id: 1, type: 'commission', event: 'push3', amount: 398.00, memo: '推三返一' },
|
||||
{ id: 2, type: 'commission', event: 'transfer_to_money', amount: -398.00, memo: '结转至商城余额' },
|
||||
{ id: 2, type: 'commission', event: 'transfer_to_money', amount: -398.00, memo: '结转至钱包余额' },
|
||||
{ id: 3, type: 'money', event: 'transfer_by_commission', amount: 398.00, memo: '收益结转入账' },
|
||||
{ id: 4, type: 'money', event: 'recharge', amount: 100.00, memo: '微信充值' },
|
||||
{ id: 5, type: 'money', event: 'team_venue', amount: 50.00, memo: '酒道馆订单奖' },
|
||||
|
||||
@@ -142,7 +142,7 @@ function validateWithdrawMoney(moneyInput, userBalance) {
|
||||
return { valid: false, message: '提现金额必须为100的整数倍' };
|
||||
}
|
||||
if (floatMoney > userBalance) {
|
||||
return { valid: false, message: '商城可用余额不足' };
|
||||
return { valid: false, message: '钱包可用余额不足' };
|
||||
}
|
||||
return { valid: true, message: '验证通过' };
|
||||
}
|
||||
@@ -190,7 +190,7 @@ const valNegative = validateWithdrawMoney(-100, 500);
|
||||
testAssert(valNegative.valid === false && valNegative.message === '请输入正确的提现金额', '校验用例:金额 -100 元被拦截');
|
||||
|
||||
const valExceed = validateWithdrawMoney(200, 150);
|
||||
testAssert(valExceed.valid === false && valExceed.message === '商城可用余额不足', '校验用例:金额 200 元(余额 150 元)超额拦截');
|
||||
testAssert(valExceed.valid === false && valExceed.message === '钱包可用余额不足', '校验用例:金额 200 元(余额 150 元)超额拦截');
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(` 测试结果汇总:共 ${passCount + failCount} 项测试,通过: ${passCount},失败: ${failCount}`);
|
||||
|
||||
@@ -134,7 +134,7 @@ function formatExportRow(row) {
|
||||
accountNo = withdrawInfo.openid ? `\t${withdrawInfo.openid}` : '';
|
||||
}
|
||||
|
||||
let assetName = '商城余额';
|
||||
let assetName = '钱包余额';
|
||||
let assetAmountDetail = '-';
|
||||
let amountDisplay = Number(row.money || 0).toFixed(2);
|
||||
let actualAmountDisplay = Number(row.actual_money || 0).toFixed(2);
|
||||
@@ -172,7 +172,7 @@ function formatExportRow(row) {
|
||||
};
|
||||
}
|
||||
|
||||
// 模拟场景 A:普通商城余额提现至银行卡
|
||||
// 模拟场景 A:普通钱包余额提现至银行卡
|
||||
const mockRowA = {
|
||||
id: 101,
|
||||
sn: 'TX202609210001',
|
||||
@@ -194,7 +194,7 @@ const mockRowA = {
|
||||
};
|
||||
|
||||
const formattedA = formatExportRow(mockRowA);
|
||||
testAssert(formattedA.asset_name === '商城余额', '普通余额提现资产显示为 商城余额');
|
||||
testAssert(formattedA.asset_name === '钱包余额', '普通余额提现资产显示为 钱包余额');
|
||||
testAssert(formattedA.mobile === '\t13800138000', '手机号添加制表符防失真');
|
||||
testAssert(formattedA.card_no === '\t6222021000112233445', '银行卡号添加制表符防科学计数法');
|
||||
testAssert(formattedA.money === '1000.00', '提现金额精准显示 1000.00');
|
||||
|
||||
在新工单中引用
屏蔽一个用户