feat: 收益全额直入商城余额、存量结转与奖项名称全局规范化对齐
这个提交包含在:
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 自购与直推订单双向保活(收益有效期)专项测试
|
||||
* 覆盖场景:
|
||||
* 1. 静态代码结构检查:PartnerRewardService::touchOrderTimes, ActiveService::hasQualifiedOrder & syncActiveTimes, Partner.php 监听调用
|
||||
* 2. 逻辑模拟:自购订单激活本人保活
|
||||
* 3. 逻辑模拟:直推订单激活直属上级保活(二级及以上间接团队隔离,不激活)
|
||||
* 4. 逻辑模拟:防时间倒退(较旧订单不覆盖较新订单时间戳)
|
||||
* 5. 状态转换模拟:直推订单使 30 天未自购的上级从 inactive 转为 active
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
function it(desc, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` [PASS] ${desc}`);
|
||||
passCount++;
|
||||
} catch (err) {
|
||||
console.error(` [FAIL] ${desc}`);
|
||||
console.error(` Error: ${err.message}`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 自购与直推订单双向保活(收益有效期)专项测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
const rewardServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerRewardService.php');
|
||||
const activeServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/ActiveService.php');
|
||||
const listenerPath = path.resolve(__dirname, '../../addons/shopro/listener/Partner.php');
|
||||
|
||||
const rewardServiceContent = fs.readFileSync(rewardServicePath, 'utf8');
|
||||
const activeServiceContent = fs.readFileSync(activeServicePath, 'utf8');
|
||||
const listenerContent = fs.readFileSync(listenerPath, 'utf8');
|
||||
|
||||
// 1. 代码规范与方法存在性检查
|
||||
it('PartnerRewardService 包含 touchOrderTimes 方法且更新买家本人与一级推荐人', () => {
|
||||
assert(rewardServiceContent.includes('function touchOrderTimes'), '缺少 touchOrderTimes 方法');
|
||||
assert(rewardServiceContent.includes('parent_user_id'), 'touchOrderTimes 未查询 parent_user_id');
|
||||
assert(rewardServiceContent.includes('$this->updateAgentOrderTime($parentUserId'), '未更新直推上级订单时间');
|
||||
});
|
||||
|
||||
it('PartnerRewardService 防时间倒退逻辑(新时间戳不被旧时间戳覆盖)', () => {
|
||||
assert(rewardServiceContent.includes('$timestamp >= $current'), 'updateAgentOrderTime 缺少防时间倒退判断');
|
||||
});
|
||||
|
||||
it('ActiveService::hasQualifiedOrder 支持自购与直推订单双向判定', () => {
|
||||
assert(activeServiceContent.includes('parent_user_id'), 'hasQualifiedOrder 未关联直推下级 parent_user_id');
|
||||
assert(activeServiceContent.includes('$candidateUserIds = array_merge([$userId], $directChildIds)'), '缺少买家本人与直推下级候选集合并');
|
||||
});
|
||||
|
||||
it('ActiveService 包含 syncActiveTimes 全量校准同步方法', () => {
|
||||
assert(activeServiceContent.includes('function syncActiveTimes'), '缺少 syncActiveTimes 方法');
|
||||
assert(activeServiceContent.includes('max($selfTime, $directTime)'), 'syncActiveTimes 缺少自购与直推最大值聚合');
|
||||
});
|
||||
|
||||
it('Partner 监听器 orderPaidAfter 调用 touchOrderTimes 触发双向保活', () => {
|
||||
assert(listenerContent.includes('touchOrderTimes'), 'Partner.php 未调用 touchOrderTimes');
|
||||
});
|
||||
|
||||
// 2. 模拟订单与层级保活流转模拟器
|
||||
class MockAgentSystem {
|
||||
constructor() {
|
||||
this.users = {}; // id -> { id, parent_user_id }
|
||||
this.agents = {}; // user_id -> { last_self_order_time, level }
|
||||
}
|
||||
|
||||
addUser(id, parentUserId, level = 1) {
|
||||
this.users[id] = { id, parent_user_id: parentUserId };
|
||||
this.agents[id] = { last_self_order_time: 0, level };
|
||||
}
|
||||
|
||||
updateAgentOrderTime(userId, timestamp) {
|
||||
const agent = this.agents[userId];
|
||||
if (agent) {
|
||||
if (timestamp >= agent.last_self_order_time) {
|
||||
agent.last_self_order_time = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
touchOrderTimes(buyerId, time) {
|
||||
const timestamp = time || Math.floor(Date.now() / 1000);
|
||||
// 1. 买家本人(自购)
|
||||
this.updateAgentOrderTime(buyerId, timestamp);
|
||||
|
||||
// 2. 直推一级上级(直推)
|
||||
const user = this.users[buyerId];
|
||||
if (user && user.parent_user_id && user.parent_user_id !== buyerId) {
|
||||
this.updateAgentOrderTime(user.parent_user_id, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
isActive(userId, nowSec, windowDays = 30) {
|
||||
const agent = this.agents[userId];
|
||||
if (!agent || !agent.last_self_order_time) return false;
|
||||
const windowStart = nowSec - windowDays * 86400;
|
||||
return agent.last_self_order_time >= windowStart;
|
||||
}
|
||||
}
|
||||
|
||||
// 场景一:买家 A 自购,激活本人 30 天保活
|
||||
it('场景一:买家 A 自购 -> 激活本人保活,状态为 active', () => {
|
||||
const sys = new MockAgentSystem();
|
||||
sys.addUser(101, 0); // 无上级
|
||||
const orderTime = 1700000000;
|
||||
|
||||
sys.touchOrderTimes(101, orderTime);
|
||||
assert.strictEqual(sys.agents[101].last_self_order_time, orderTime, '买家 A 订单时间未更新');
|
||||
assert.strictEqual(sys.isActive(101, orderTime + 86400), true, '自购后第 1 天应为 active');
|
||||
assert.strictEqual(sys.isActive(101, orderTime + 31 * 86400), false, '自购后第 31 天应为 inactive');
|
||||
});
|
||||
|
||||
// 场景二:直推下级 B 下单 -> 激活直推上级 A 保活
|
||||
it('场景二:直推下级 B 下单 -> 同时激活直推上级 A,A 即使无自购也变为 active', () => {
|
||||
const sys = new MockAgentSystem();
|
||||
sys.addUser(100, 0); // 上级 A(从未自购)
|
||||
sys.addUser(200, 100); // 直推下级 B(推荐人是 A)
|
||||
|
||||
const orderTime = 1700000000;
|
||||
sys.touchOrderTimes(200, orderTime);
|
||||
|
||||
// B 本人自购更新
|
||||
assert.strictEqual(sys.agents[200].last_self_order_time, orderTime, '买家 B 未更新');
|
||||
// A 直推获得保活更新
|
||||
assert.strictEqual(sys.agents[100].last_self_order_time, orderTime, '直推上级 A 未被直推订单激活');
|
||||
assert.strictEqual(sys.isActive(100, orderTime + 10 * 86400), true, '直推上级 A 获得 30 天有效分红资格');
|
||||
});
|
||||
|
||||
// 场景三:间接下级 C(二级)下单 -> 仅激活 B,不激活上上级 A(严格限定直推与自购)
|
||||
it('场景三:二级下级 C 下单 -> 仅激活一级直推人 B,上上级 A 不受影响(层级隔离)', () => {
|
||||
const sys = new MockAgentSystem();
|
||||
sys.addUser(10, 0); // 上上级 A
|
||||
sys.addUser(20, 10); // 上级 B
|
||||
sys.addUser(30, 20); // 下级 C(买家)
|
||||
|
||||
const orderTime = 1700000000;
|
||||
sys.touchOrderTimes(30, orderTime);
|
||||
|
||||
assert.strictEqual(sys.agents[30].last_self_order_time, orderTime, '买家 C 应更新');
|
||||
assert.strictEqual(sys.agents[20].last_self_order_time, orderTime, '直属上级 B 应更新');
|
||||
assert.strictEqual(sys.agents[10].last_self_order_time, 0, '上上级 A 绝不应被二级订单更新(严格限定自购与直推)');
|
||||
});
|
||||
|
||||
// 场景四:防时间倒退机制(乱序异步事件不能覆盖更新的时间戳)
|
||||
it('场景四:防时间倒退 -> 乱序处理时,较旧的订单不会覆盖较新的保活时间戳', () => {
|
||||
const sys = new MockAgentSystem();
|
||||
sys.addUser(1, 0);
|
||||
sys.addUser(2, 1);
|
||||
|
||||
const tNew = 1700050000;
|
||||
const tOld = 1700010000;
|
||||
|
||||
sys.touchOrderTimes(2, tNew);
|
||||
assert.strictEqual(sys.agents[1].last_self_order_time, tNew);
|
||||
|
||||
// 此时来了一单较早的补偿结算
|
||||
sys.touchOrderTimes(2, tOld);
|
||||
assert.strictEqual(sys.agents[1].last_self_order_time, tNew, '较旧的时间戳被错误覆盖了');
|
||||
});
|
||||
|
||||
console.log(`\n--------------------------------------------------------`);
|
||||
console.log(`测试结果统计: 通过 ${passCount} 项, 失败 ${failCount} 项`);
|
||||
console.log(`========================================================\n`);
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 管理后台全模块查询与展示对齐防误导专项测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
function assert(condition, message) {
|
||||
if (condition) {
|
||||
console.log(` [\x1b[32mPASS\x1b[0m] ${message}`);
|
||||
passCount++;
|
||||
} else {
|
||||
console.error(` [\x1b[31mFAIL\x1b[0m] ${message}`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
|
||||
// --- 1. Reward.php 模型追加属性与解析验证 ---
|
||||
console.log('--- 1. Reward.php 模型追加属性与入账账户解析验证 ---');
|
||||
const rewardModelPath = path.join(rootDir, 'application/admin/model/shopro/partner/Reward.php');
|
||||
assert(fs.existsSync(rewardModelPath), 'application/admin/model/shopro/partner/Reward.php 文件存在');
|
||||
const rewardModelContent = fs.readFileSync(rewardModelPath, 'utf8');
|
||||
assert(rewardModelContent.includes("'wallet_type'"), 'Reward 模型 $append 追加 wallet_type');
|
||||
assert(rewardModelContent.includes("'wallet_type_text'"), 'Reward 模型 $append 追加 wallet_type_text');
|
||||
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 映射包含 分红马豆');
|
||||
|
||||
// --- 2. Order.php 订单分成构建与入账账户对齐验证 ---
|
||||
console.log('\n--- 2. Order.php 订单分成构建与入账账户对齐验证 ---');
|
||||
const orderControllerPath = path.join(rootDir, 'application/admin/controller/shopro/order/Order.php');
|
||||
assert(fs.existsSync(orderControllerPath), 'Order.php 文件存在');
|
||||
const orderControllerContent = fs.readFileSync(orderControllerPath, 'utf8');
|
||||
assert(orderControllerContent.includes("'wallet_type' => (string) ($row->wallet_type"), '即时奖励数据行注入 wallet_type');
|
||||
assert(orderControllerContent.includes("'wallet_type_text' => (string) ($row->wallet_type_text"), '即时奖励数据行注入 wallet_type_text');
|
||||
assert(orderControllerContent.includes("'wallet_type' => 'commission'"), '传统分销数据行标识为 commission');
|
||||
assert(orderControllerContent.includes("'wallet_type_text' => '佣金钱包'"), '传统分销数据行标识为 佣金钱包');
|
||||
assert(orderControllerContent.includes("'wallet_type' => 'system'"), '平台留存数据行标识为 system');
|
||||
assert(orderControllerContent.includes("'wallet_type_text' => '平台留存'"), '平台留存数据行标识为 平台留存');
|
||||
assert(orderControllerContent.includes("PartnerConfig::rewardWalletType()"), '日结均分奖池注入动态入账钱包');
|
||||
|
||||
// --- 3. 订单详情视图 (order/detail.html) 验证 ---
|
||||
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('label="入账账户"'), '分成明细表格包含「入账账户」列');
|
||||
assert(orderDetailViewContent.includes('scope.row.wallet_type_text'), '入账账户列展示 scope.row.wallet_type_text');
|
||||
|
||||
// --- 4. 收益明细控制器与视图 (partner/reward) 验证 ---
|
||||
console.log('\n--- 4. 收益明细控制器与视图 (partner/reward) 验证 ---');
|
||||
const partnerRewardControllerPath = path.join(rootDir, 'application/admin/controller/shopro/partner/Reward.php');
|
||||
assert(fs.existsSync(partnerRewardControllerPath), 'partner/Reward.php 文件存在');
|
||||
const partnerRewardControllerContent = fs.readFileSync(partnerRewardControllerPath, 'utf8');
|
||||
assert(partnerRewardControllerContent.includes("'wallet_type_text' => '入账账户'"), '导出表头新增「入账账户」列');
|
||||
assert(partnerRewardControllerContent.includes("'wallet_type' => $r->wallet_type"), 'orderTree 订单透视树注入 wallet_type');
|
||||
assert(partnerRewardControllerContent.includes("'wallet_type_text' => $r->wallet_type_text"), 'orderTree 订单透视树注入 wallet_type_text');
|
||||
|
||||
const partnerRewardViewPath = path.join(rootDir, 'application/admin/view/shopro/partner/reward/index.html');
|
||||
assert(fs.existsSync(partnerRewardViewPath), 'partner/reward/index.html 文件存在');
|
||||
const partnerRewardViewContent = fs.readFileSync(partnerRewardViewPath, 'utf8');
|
||||
assert(partnerRewardViewContent.includes('label="入账账户"'), '收益流水主列表包含「入账账户」列');
|
||||
assert(partnerRewardViewContent.includes('orderTreeDialog'), '包含订单透视弹窗');
|
||||
assert(partnerRewardViewContent.match(/orderTreeDialog[\s\S]*label="入账账户"/), '订单透视明细树表格包含「入账账户」列');
|
||||
|
||||
// --- 5. 会员详情与会员列表 (user/user) 验证 ---
|
||||
console.log('\n--- 5. 会员详情与会员列表 (user/user) 验证 ---');
|
||||
const userControllerPath = path.join(rootDir, 'application/admin/controller/shopro/user/User.php');
|
||||
assert(fs.existsSync(userControllerPath), 'User.php 文件存在');
|
||||
const userControllerContent = fs.readFileSync(userControllerPath, 'utf8');
|
||||
assert(userControllerContent.includes('alignedTotalIncome = max($agentTotalIncome, (float) $totalCash)'), 'User.php 累计收益取分销与二开收益中心最大值对齐');
|
||||
assert(userControllerContent.includes("$data['total_income'] = number_format($alignedTotalIncome"), 'User.php 输出格式化 total_income');
|
||||
assert(userControllerContent.includes("$data['income_total_cash'] = number_format($alignedTotalIncome"), 'User.php 输出格式化 income_total_cash');
|
||||
assert(userControllerContent.includes("$data['money'] = number_format"), 'User.php 输出格式化 money');
|
||||
assert(userControllerContent.includes("$data['commission'] = number_format"), 'User.php 输出格式化 commission');
|
||||
|
||||
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.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 收益中心流水包含「入账账户」列');
|
||||
|
||||
const userIndexViewPath = path.join(rootDir, 'application/admin/view/shopro/user/user/index.html');
|
||||
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.commission'), '会员列表展示「未转佣金」');
|
||||
|
||||
// --- 6. 分销商详情与佣金明细 (commission) 验证 ---
|
||||
console.log('\n--- 6. 分销商详情与佣金明细 (commission) 验证 ---');
|
||||
const agentControllerPath = path.join(rootDir, 'application/admin/controller/shopro/commission/Agent.php');
|
||||
assert(fs.existsSync(agentControllerPath), 'commission/Agent.php 文件存在');
|
||||
const agentControllerContent = fs.readFileSync(agentControllerPath, 'utf8');
|
||||
assert(agentControllerContent.includes('alignedIncome = number_format(max(floatval($detail->total_income ?? 0), (float) $totalCash)'), 'Agent.php detail 累计收益严格对齐收益中心');
|
||||
assert(agentControllerContent.includes('$detail->user_money = number_format'), 'Agent.php 注入 user_money');
|
||||
assert(agentControllerContent.includes('$detail->user_commission = number_format'), 'Agent.php 注入 user_commission');
|
||||
|
||||
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('历史未转佣金:'), '分销商资产信息包含「历史未转佣金」');
|
||||
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('当前收益分成已全额直入商城余额'), '历史佣金明细顶部包含直入商城余额提示说明');
|
||||
|
||||
const commRewardControllerPath = path.join(rootDir, 'application/admin/controller/shopro/commission/Reward.php');
|
||||
assert(fs.existsSync(commRewardControllerPath), 'commission/Reward.php 文件存在');
|
||||
const commRewardControllerContent = fs.readFileSync(commRewardControllerPath, 'utf8');
|
||||
assert(commRewardControllerContent.includes('历史分销佣金列表'), '导出文件名称更新为「历史分销佣金列表」');
|
||||
|
||||
// --- 7. 底层分润服务 ext.wallet_type 与日志兜底防误导验证 ---
|
||||
console.log('\n--- 7. 底层分润服务 ext.wallet_type 与日志兜底防误导验证 ---');
|
||||
const partnerRewardServicePath = path.join(rootDir, 'addons/shopro/service/partner/PartnerRewardService.php');
|
||||
assert(fs.existsSync(partnerRewardServicePath), 'PartnerRewardService.php 文件存在');
|
||||
const partnerRewardServiceContent = fs.readFileSync(partnerRewardServicePath, 'utf8');
|
||||
assert(partnerRewardServiceContent.includes("$ext['wallet_type'] = $walletType;"), 'PartnerRewardService::account 显式持久化 ext.wallet_type 确保自说明性');
|
||||
|
||||
const dailySettleServicePath = path.join(rootDir, 'addons/shopro/service/partner/DailySettleService.php');
|
||||
assert(fs.existsSync(dailySettleServicePath), 'DailySettleService.php 文件存在');
|
||||
const dailySettleServiceContent = fs.readFileSync(dailySettleServicePath, 'utf8');
|
||||
assert(dailySettleServiceContent.includes("'wallet_type' => $walletType,"), 'DailySettleService 均分分润显式持久化 ext.wallet_type');
|
||||
assert(dailySettleServiceContent.includes("'wallet_type' => 'system',"), 'DailySettleService 平台尾差与分母为0显式持久化 ext.wallet_type 为 system');
|
||||
|
||||
const walletLogModelPath = path.join(rootDir, 'application/admin/model/shopro/user/WalletLog.php');
|
||||
assert(fs.existsSync(walletLogModelPath), 'WalletLog.php 文件存在');
|
||||
const walletLogModelContent = fs.readFileSync(walletLogModelPath, 'utf8');
|
||||
assert(walletLogModelContent.includes("$eventMap['money'][$event] ?? ($eventMap['commission'][$event] ?? ($event ?: ''))"), 'WalletLog::getEventTextAttr 包含跨钱包与事件编码三级非空兜底保护');
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
|
||||
console.log('========================================================');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace tests\partner;
|
||||
|
||||
define('APP_PATH', __DIR__ . '/../../application/');
|
||||
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
|
||||
require_once __DIR__ . '/../../thinkphp/base.php';
|
||||
require_once __DIR__ . '/../../thinkphp/helper.php';
|
||||
require_once __DIR__ . '/../../application/common.php';
|
||||
|
||||
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
|
||||
\think\Config::set(include APP_PATH . 'database.php', 'database');
|
||||
\think\Loader::addNamespace('app', APP_PATH);
|
||||
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
|
||||
|
||||
use addons\shopro\service\commission\Goods as GoodsCommissionService;
|
||||
use addons\shopro\service\commission\Agent as AgentService;
|
||||
use app\admin\model\shopro\commission\Level as LevelModel;
|
||||
|
||||
class VerifyAgentUpgradePhp8Spec
|
||||
{
|
||||
private $passed = 0;
|
||||
private $failed = 0;
|
||||
|
||||
public function run()
|
||||
{
|
||||
echo "========================================================\n";
|
||||
echo " 创客升级与 PHP 8.0 兼容性专项回归测试 \n";
|
||||
echo "========================================================\n\n";
|
||||
|
||||
$this->testGoodsCommissionConfigNullSafe();
|
||||
$this->testUserUpgradeLevelMatch();
|
||||
|
||||
echo "\n--------------------------------------------------------\n";
|
||||
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
|
||||
echo "========================================================\n";
|
||||
return $this->failed === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
private function assert($condition, $testName, $detail = '')
|
||||
{
|
||||
if ($condition) {
|
||||
$this->passed++;
|
||||
echo " [✓ PASS] {$testName}\n";
|
||||
} else {
|
||||
$this->failed++;
|
||||
echo " [✗ FAIL] {$testName} - {$detail}\n";
|
||||
}
|
||||
}
|
||||
|
||||
private function testGoodsCommissionConfigNullSafe()
|
||||
{
|
||||
try {
|
||||
// 模拟一个 commission_config 为 null 的商品对象
|
||||
$fakeGoods = new \stdClass();
|
||||
$fakeGoods->commission_config = null;
|
||||
$fakeGoods->self_rules = 0;
|
||||
$fakeGoods->commission_rules = null;
|
||||
|
||||
$goodsService = new GoodsCommissionService($fakeGoods, 0);
|
||||
$this->assert($goodsService !== null, 'GoodsCommissionService 空配置对象构造兼容测试');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assert(false, 'GoodsCommissionService 空配置对象构造兼容测试', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function testUserUpgradeLevelMatch()
|
||||
{
|
||||
try {
|
||||
$levels = LevelModel::where('level', '>=', 2)->select();
|
||||
$allStrict = true;
|
||||
foreach ($levels as $l) {
|
||||
if ($l->upgrade_type !== 1) {
|
||||
$allStrict = false;
|
||||
}
|
||||
}
|
||||
$this->assert($allStrict, '创客等级全部条件升级模式校验 (upgrade_type === 1)');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assert(false, '创客等级全部条件升级模式校验', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
$tester = new VerifyAgentUpgradePhp8Spec();
|
||||
exit($tester->run());
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
define('APP_PATH', __DIR__ . '/../../application/');
|
||||
define('ROOT_PATH', __DIR__ . '/../../');
|
||||
require_once ROOT_PATH . 'thinkphp/base.php';
|
||||
require_once ROOT_PATH . 'thinkphp/helper.php';
|
||||
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
|
||||
\think\Config::set(include APP_PATH . 'database.php', 'database');
|
||||
\think\Loader::addNamespace('app', APP_PATH);
|
||||
\think\Loader::addNamespace('addons\\shopro', ROOT_PATH . 'addons/shopro/');
|
||||
require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
|
||||
|
||||
$auth = \app\common\library\Auth::instance();
|
||||
$auth->direct(10);
|
||||
echo "User 10 logged in, id: " . $auth->id . PHP_EOL;
|
||||
|
||||
$agent = \app\admin\model\shopro\commission\Agent::where('user_id', 10)->find();
|
||||
$res = \addons\shopro\service\partner\IdentityService::attachAgent($agent);
|
||||
echo "show_madow_rate in attachAgent: " . ($res['show_madow_rate'] ?? 'NOT_FOUND') . PHP_EOL;
|
||||
|
||||
$prof = \addons\shopro\service\partner\IdentityService::attachProfile($agent);
|
||||
echo "show_madow_rate in attachProfile: " . ($prof['show_madow_rate'] ?? 'NOT_FOUND') . PHP_EOL;
|
||||
|
||||
$rate = \addons\shopro\service\partner\PartnerConfig::showMadowRate();
|
||||
echo "PartnerConfig::showMadowRate: " . $rate . PHP_EOL;
|
||||
echo "ALL OK!" . PHP_EOL;
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 累计收益统计口径与用户商城余额统一专项测试
|
||||
* 覆盖范围:
|
||||
* 1. IdentityService attachAgent / attachProfile 累计收益口径统一与兜底
|
||||
* 2. User profile 接口统一映射 balance = money 及累计收益补偿
|
||||
* 3. partner/Index index / rewards 接口统一返回 total_income 与 month_income
|
||||
* 4. WalletLog 收益统计扣除互转重复项,真实反映收益与扣减(防误导)
|
||||
* 5. uniapp 视图端全面对齐:
|
||||
* - rewards.vue 累计已结算收益卡片与本月统计
|
||||
* - account-info.vue 累计收益与商城余额
|
||||
* - commission.vue 累计总收益、周期收益与商城可用余额
|
||||
* - withdraw.vue 商城可用余额提示与收益入账说明
|
||||
* 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. IdentityService 累计收益与资产聚合验证
|
||||
// ----------------------------------------------------
|
||||
console.log('--- 1. IdentityService 累计收益口径与资产聚合验证 ---');
|
||||
const identityServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/IdentityService.php');
|
||||
assert(fs.existsSync(identityServicePath), 'IdentityService.php 文件存在');
|
||||
if (fs.existsSync(identityServicePath)) {
|
||||
const content = fs.readFileSync(identityServicePath, 'utf-8');
|
||||
assert(content.includes('max($agentTotalIncome, floatval($rewardCash))'), 'attachAgent 取分销商总收益与收益中心奖励最大值对齐');
|
||||
assert(content.includes("'total_income' => self::formatMoney($unifiedTotalIncome)"), 'attachAgent 返回格式化 total_income');
|
||||
assert(content.includes("'income_total_cash' => self::formatMoney($unifiedTotalIncome)"), 'attachAgent 返回格式化 income_total_cash');
|
||||
assert(content.includes("'total_income' => $full['total_income'] ?? '0.00'"), 'attachProfile 包含 total_income');
|
||||
assert(content.includes("'income_total_cash' => $full['income_total_cash'] ?? '0.00'"), 'attachProfile 包含 income_total_cash');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 2. User profile 接口验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 2. User.php 个人详情接口 balance 与收益对齐验证 ---');
|
||||
const userControllerPath = path.resolve(__dirname, '../../addons/shopro/controller/user/User.php');
|
||||
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('$userRewardCash > 0'), '非分销商身份但有分成收益时,执行累计收益兜底统计');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 3. partner/Index 接口对齐验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 3. partner/Index 概览与明细接口 total_income 对齐验证 ---');
|
||||
const partnerIndexPath = path.resolve(__dirname, '../../addons/shopro/controller/partner/Index.php');
|
||||
assert(fs.existsSync(partnerIndexPath), 'partner/Index.php 文件存在');
|
||||
if (fs.existsSync(partnerIndexPath)) {
|
||||
const content = fs.readFileSync(partnerIndexPath, 'utf-8');
|
||||
assert(content.includes("'total_income' => $totalIncome"), 'index 概览接口返回 total_income');
|
||||
assert(content.includes("'month_income' => IdentityService::formatMoney($monthIncome)"), 'index 概览接口返回本月收益 month_income');
|
||||
assert(content.includes("'total_income' => $totalIncome"), 'rewards 明细接口返回 total_income');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 4. WalletLog 收益统计防误导口径验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 4. WalletLog 收入与支出统计口径防误导验证 ---');
|
||||
const walletLogPath = path.resolve(__dirname, '../../addons/shopro/controller/user/WalletLog.php');
|
||||
assert(fs.existsSync(walletLogPath), 'WalletLog.php 文件存在');
|
||||
if (fs.existsSync(walletLogPath)) {
|
||||
const content = fs.readFileSync(walletLogPath, 'utf-8');
|
||||
assert(content.includes("whereNotIn('event', ['transfer_by_commission'])"), 'commission 收入统计排除了转余额入账 transfer_by_commission (防重复计算)');
|
||||
assert(content.includes("whereNotIn('event', ['transfer_to_money', 'commission_transfer'])"), 'commission 支出统计排除了结转至余额 transfer_to_money (防伪支出误导)');
|
||||
assert(content.includes('$actualRewardEvents'), '定义了真实业务奖励事件清单 actualRewardEvents');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 5. uniapp 页面端到端对齐验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 5. uniapp 视图端全面对齐验证 ---');
|
||||
|
||||
// 5.1 rewards.vue
|
||||
const rewardsVuePath = path.resolve(__dirname, '../../uniapp/pages/commission/rewards.vue');
|
||||
assert(fs.existsSync(rewardsVuePath), 'rewards.vue 文件存在');
|
||||
if (fs.existsSync(rewardsVuePath)) {
|
||||
const content = fs.readFileSync(rewardsVuePath, 'utf-8');
|
||||
assert(content.includes('累计已结算收益(元)'), 'rewards.vue 卡片主标题变更为「累计已结算收益(元)」');
|
||||
assert(content.includes('displayTotalIncome'), 'rewards.vue 卡片主金额使用 displayTotalIncome 绑定');
|
||||
assert(content.includes('本月已结算: ¥{{ state.overview.month_income'), '副指标清晰标注本月已结算');
|
||||
assert(content.includes('state.overview.total_income'), 'overview 状态机与接口对齐包含 total_income');
|
||||
}
|
||||
|
||||
// 5.2 account-info.vue
|
||||
const accountInfoVuePath = path.resolve(__dirname, '../../uniapp/pages/commission/components/account-info.vue');
|
||||
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');
|
||||
}
|
||||
|
||||
// 5.3 commission.vue
|
||||
const commissionVuePath = path.resolve(__dirname, '../../uniapp/pages/user/wallet/commission.vue');
|
||||
assert(fs.existsSync(commissionVuePath), 'commission.vue 文件存在');
|
||||
if (fs.existsSync(commissionVuePath)) {
|
||||
const content = fs.readFileSync(commissionVuePath, 'utf-8');
|
||||
assert(content.includes("agentInfo.total_income || userInfo.total_income"), '累计总收益具备双重兜底');
|
||||
assert(content.includes('周期收益¥'), '日期区间统计描述对齐为「周期收益」,明确收益口径');
|
||||
}
|
||||
|
||||
// 5.4 withdraw.vue
|
||||
const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw.vue');
|
||||
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 显著提示收益已全额直接入账商城余额');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 6. 统计口径算法精准性断言
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 6. 统计口径算法精准性模拟断言 ---');
|
||||
|
||||
const actualRewardEvents = [
|
||||
'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
|
||||
'team_venue', 'team_county', 'venue_peer', 'county_peer',
|
||||
'venue_share', 'county_share', 'reward_income'
|
||||
];
|
||||
|
||||
// 模拟典型用户流水:
|
||||
// 1) 历史获得推三返一:+398 (commission)
|
||||
// 2) 结转转出至余额:-398 (commission, event=transfer_to_money)
|
||||
// 3) 结转入账至余额:+398 (money, event=transfer_by_commission)
|
||||
// 4) 产生新订单直入余额奖:+50 (money, event=team_venue)
|
||||
// 5) 新订单发生售后退回:-50 (money, event=team_venue_back)
|
||||
const userTestLogs = [
|
||||
{ type: 'commission', event: 'push3', amount: 398.00 },
|
||||
{ type: 'commission', event: 'transfer_to_money', amount: -398.00 },
|
||||
{ type: 'money', event: 'transfer_by_commission', amount: 398.00 },
|
||||
{ type: 'money', event: 'team_venue', amount: 50.00 },
|
||||
{ type: 'money', event: 'team_venue_back', amount: -50.00 },
|
||||
];
|
||||
|
||||
function calculateAlignedIncomeAndExpense(logs) {
|
||||
let income = 0;
|
||||
let expense = 0;
|
||||
|
||||
for (const log of logs) {
|
||||
if (log.amount > 0) {
|
||||
// 排除互转入账,只统计真正创造的收益
|
||||
if (log.type === 'money' && actualRewardEvents.includes(log.event)) {
|
||||
income += log.amount;
|
||||
} else if (log.type === 'commission' && log.event !== 'transfer_by_commission') {
|
||||
income += log.amount;
|
||||
}
|
||||
} else if (log.amount < 0) {
|
||||
// 排除结转内部转出,只统计真实的业务扣减与退款
|
||||
if (!['transfer_to_money', 'commission_transfer'].includes(log.event)) {
|
||||
expense += log.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { income, expense };
|
||||
}
|
||||
|
||||
const calculated = calculateAlignedIncomeAndExpense(userTestLogs);
|
||||
assert(calculated.income === (398.00 + 50.00), `总收益收入精准为 ¥${calculated.income} (398+50=448,未被 398 结转重复统计为 846)`);
|
||||
assert(calculated.expense === -50.00, `总支出精准为 ¥${calculated.expense} (仅扣款-50,未被 -398 结转误统计为 -448)`);
|
||||
|
||||
const netGain = calculated.income + calculated.expense;
|
||||
assert(netGain === 398.00, `实际净收益精准守恒为 ¥${netGain}`);
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(`测试完成!通过: \x1b[32m${passCount}\x1b[0m, 失败: \x1b[31m${failCount}\x1b[0m`);
|
||||
console.log('========================================================');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* tests/partner/verify_madow_switch_spec.js
|
||||
*
|
||||
* 验证“我的马豆”页面马币市值指示胶囊后台开关:
|
||||
* 1. 后端配置读取与默认值向后兼容逻辑验证
|
||||
* 2. 移动端前台显隐条件判定逻辑(开启、关闭、未配置)验证
|
||||
* 3. 接口数据契约与统一返回结构校验
|
||||
* 4. 关键文件代码变更完整性静态检查
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
console.log('====================================================');
|
||||
console.log(' 开始验证:“我的马豆”马币市值指示胶囊后台显隐开关');
|
||||
console.log('====================================================\n');
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
function runTest(title, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`[PASS] ${title}`);
|
||||
passCount++;
|
||||
} catch (err) {
|
||||
console.error(`[FAIL] ${title}`);
|
||||
console.error(` 错误原因: ${err.message}`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 1. 测试后端 showMadowRate 配置降级与逻辑映射
|
||||
// ----------------------------------------------------
|
||||
function mockShowMadowRate(configMap) {
|
||||
const get = (key, defaultVal) => (configMap[key] !== undefined ? configMap[key] : defaultVal);
|
||||
const show = get('partner_show_madow_rate', 1);
|
||||
if (show === null || show === '') {
|
||||
return 1;
|
||||
}
|
||||
return parseInt(show, 10) ? 1 : 0;
|
||||
}
|
||||
|
||||
runTest('配置未设置或为 null/空字符时,向后兼容默认返回 1 (显示)', () => {
|
||||
assert.strictEqual(mockShowMadowRate({}), 1);
|
||||
assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: null }), 1);
|
||||
assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: '' }), 1);
|
||||
});
|
||||
|
||||
runTest('配置显式设置为 0 时,返回 0 (隐藏)', () => {
|
||||
assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: 0 }), 0);
|
||||
assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: '0' }), 0);
|
||||
});
|
||||
|
||||
runTest('配置显式设置为 1 时,返回 1 (显示)', () => {
|
||||
assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: 1 }), 1);
|
||||
assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: '1' }), 1);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 2. 测试移动端 madow.vue 显隐条件判定
|
||||
// ----------------------------------------------------
|
||||
function evalRateCapsuleVisible(showMadowRate) {
|
||||
return showMadowRate !== false && showMadowRate !== 0 && showMadowRate !== '0';
|
||||
}
|
||||
|
||||
runTest('移动端当 show_madow_rate 为 1 / "1" / true 时判定展示', () => {
|
||||
assert.strictEqual(evalRateCapsuleVisible(1), true);
|
||||
assert.strictEqual(evalRateCapsuleVisible('1'), true);
|
||||
assert.strictEqual(evalRateCapsuleVisible(true), true);
|
||||
});
|
||||
|
||||
runTest('移动端当 show_madow_rate 为 0 / "0" / false 时判定隐藏', () => {
|
||||
assert.strictEqual(evalRateCapsuleVisible(0), false);
|
||||
assert.strictEqual(evalRateCapsuleVisible('0'), false);
|
||||
assert.strictEqual(evalRateCapsuleVisible(false), false);
|
||||
});
|
||||
|
||||
runTest('移动端当 show_madow_rate 为 undefined / null 时保底默认展示', () => {
|
||||
assert.strictEqual(evalRateCapsuleVisible(undefined), true);
|
||||
assert.strictEqual(evalRateCapsuleVisible(null), true);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 3. 接口数据返回结构校验
|
||||
// ----------------------------------------------------
|
||||
runTest('madow 接口返回 payload 需包含 show_madow_rate 字段', () => {
|
||||
const mockApiResponse = {
|
||||
code: 1,
|
||||
msg: 'ok',
|
||||
data: {
|
||||
madow: '100.0000',
|
||||
madow_rate: '0.000007865',
|
||||
madow_value: '0.00',
|
||||
show_madow_rate: 1,
|
||||
list: []
|
||||
}
|
||||
};
|
||||
assert.strictEqual(mockApiResponse.code, 1);
|
||||
assert.strictEqual(typeof mockApiResponse.data.show_madow_rate, 'number');
|
||||
assert.strictEqual(mockApiResponse.data.show_madow_rate, 1);
|
||||
assert.strictEqual(evalRateCapsuleVisible(mockApiResponse.data.show_madow_rate), true);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 4. 源码静态变更完整性检查
|
||||
// ----------------------------------------------------
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
|
||||
runTest('检查 application/admin/controller/shopro/Config.php 包含 partner_show_madow_rate', () => {
|
||||
const file = path.join(rootDir, 'application/admin/controller/shopro/Config.php');
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
assert.ok(content.includes('partner_show_madow_rate'), 'Config.php 必须包含 partner_show_madow_rate 字段');
|
||||
assert.ok(content.includes("ShoproConfig::where('code', 'shop.commission.partner_show_madow_rate')"), 'Config.php 必须包含自愈检查逻辑');
|
||||
});
|
||||
|
||||
runTest('检查 application/admin/view/shopro/config/index.html 包含开关组件', () => {
|
||||
const file = path.join(rootDir, 'application/admin/view/shopro/config/index.html');
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
assert.ok(content.includes('form.model.partner_show_madow_rate'), 'index.html 必须绑定 partner_show_madow_rate');
|
||||
assert.ok(content.includes('显示马币市值'), 'index.html 必须包含表单中文标签');
|
||||
});
|
||||
|
||||
runTest('检查 public/assets/js/backend/shopro/config.js 包含初始化', () => {
|
||||
const file = path.join(rootDir, 'public/assets/js/backend/shopro/config.js');
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
assert.ok(content.includes('form.model.partner_show_madow_rate'), 'config.js 必须包含 partner_show_madow_rate 初始化');
|
||||
});
|
||||
|
||||
runTest('检查 addons/shopro/service/partner/PartnerConfig.php 包含 showMadowRate 方法', () => {
|
||||
const file = path.join(rootDir, 'addons/shopro/service/partner/PartnerConfig.php');
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
assert.ok(content.includes('public static function showMadowRate()'), 'PartnerConfig 必须包含 showMadowRate 方法');
|
||||
});
|
||||
|
||||
runTest('检查 addons/shopro/controller/partner/Index.php 包含 show_madow_rate 输出', () => {
|
||||
const file = path.join(rootDir, 'addons/shopro/controller/partner/Index.php');
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
assert.ok(content.includes("'show_madow_rate' => PartnerConfig::showMadowRate()"), 'Index.php 必须在 madow 中返回 show_madow_rate');
|
||||
});
|
||||
|
||||
runTest('检查 uniapp/pages/commission/madow.vue 包含条件渲染与状态绑定', () => {
|
||||
const file = path.join(rootDir, 'uniapp/pages/commission/madow.vue');
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
assert.ok(content.includes('state.info.show_madow_rate'), 'madow.vue 必须使用 state.info.show_madow_rate');
|
||||
assert.ok(content.includes('v-if="state.info.show_madow_rate'), 'madow.vue 的 rate-capsule 必须绑定 v-if');
|
||||
});
|
||||
|
||||
runTest('检查 addons/shopro/upgrade/partner_v2.sql 包含 SQL 种子', () => {
|
||||
const file = path.join(rootDir, 'addons/shopro/upgrade/partner_v2.sql');
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
assert.ok(content.includes('shop.commission.partner_show_madow_rate'), 'partner_v2.sql 必须包含配置初始行');
|
||||
});
|
||||
|
||||
console.log('\n====================================================');
|
||||
console.log(`测试完成: 成功 ${passCount} 项, 失败 ${failCount} 项`);
|
||||
console.log('====================================================');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 收益分成直接加到商城余额专项验证测试
|
||||
* 覆盖范围:
|
||||
* 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)
|
||||
* 7. 业务逻辑与回滚算法行为模拟断言
|
||||
*/
|
||||
|
||||
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. PartnerConfig.php 验证
|
||||
// ----------------------------------------------------
|
||||
console.log('--- 1. PartnerConfig.php 规范验证 ---');
|
||||
const partnerConfigPath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerConfig.php');
|
||||
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("in_array($type, ['money', 'commission'])"), '安全限定钱包类型在合法区间');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 2. PartnerRewardService.php 验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 2. PartnerRewardService.php 调度验证 ---');
|
||||
const partnerRewardServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerRewardService.php');
|
||||
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("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() 智能根据原入账类型回滚对应钱包');
|
||||
assert(content.includes("Wallet::change($reward->user_id, $targetWallet, -$reward->amount, 'reward_back'"), 'refund() 精准回滚目标钱包');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 3. DailySettleService.php 验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 3. DailySettleService.php 日结加权分润验证 ---');
|
||||
const dailySettleServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/DailySettleService.php');
|
||||
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("AgentModel::where('user_id', $userId)->setInc('total_income', $each);"), 'cashShare() 累计更新 total_income');
|
||||
assert(content.includes("$targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();"), 'clearDaily() 重跑日结时智能识别并回滚对应钱包');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 4. 后台 Reward 控制器验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 4. 后台 Reward.php 控制器验证 ---');
|
||||
const rewardCtrlPath = path.resolve(__dirname, '../../application/admin/controller/shopro/partner/Reward.php');
|
||||
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("$targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();"), 'back() 手工撤销智能回滚对应钱包');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 5. WalletLog.php 模型验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 5. WalletLog.php 模型事件映射验证 ---');
|
||||
const walletLogPath = path.resolve(__dirname, '../../application/admin/model/shopro/user/WalletLog.php');
|
||||
assert(fs.existsSync(walletLogPath), 'WalletLog.php 文件存在');
|
||||
if (fs.existsSync(walletLogPath)) {
|
||||
const content = fs.readFileSync(walletLogPath, 'utf-8');
|
||||
assert(content.includes("'push3' => '推荐提成'"), 'money 事件映射包含 push3 (推荐提成)');
|
||||
assert(content.includes("'maker_diff' => '创客进阶奖'"), 'money 事件映射包含 maker_diff (创客进阶奖)');
|
||||
assert(content.includes("'maker_peer' => '创客协同奖'"), 'money 事件映射包含 maker_peer (创客协同奖)');
|
||||
assert(content.includes("'maker_overtaken' => '创客卓越奖'"), 'money 事件映射包含 maker_overtaken (创客卓越奖)');
|
||||
assert(content.includes("'team_venue' => '酒道馆管理奖'"), 'money 事件映射包含 team_venue (酒道馆管理奖)');
|
||||
assert(content.includes("'team_county' => '区县管理奖'"), 'money 事件映射包含 team_county (区县管理奖)');
|
||||
assert(content.includes("'venue_peer' => '酒道馆平级奖'"), 'money 事件映射包含 venue_peer (酒道馆平级奖)');
|
||||
assert(content.includes("'county_peer' => '区县代平级奖'"), 'money 事件映射包含 county_peer (区县代平级奖)');
|
||||
assert(content.includes("'venue_share' => '酒道馆加权奖'"), 'money 事件映射包含 venue_share (酒道馆加权奖)');
|
||||
assert(content.includes("'county_share' => '区县加权奖'"), 'money 事件映射包含 county_share (区县加权奖)');
|
||||
assert(content.includes("'reward_income' => '收益分成-入账'"), 'money 事件映射包含 reward_income (收益分成-入账)');
|
||||
assert(content.includes("'reward_back' => '收益分成-退还'"), 'money 事件映射包含 reward_back (收益分成-退还)');
|
||||
assert(content.includes('PartnerConfig::rewardTypeMap()'), 'WalletLog 支持联动 PartnerConfig 动态解析文案');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 6. 前端视图 account-info.vue 验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 6. 前端视图 account-info.vue 验证 ---');
|
||||
const accountInfoPath = path.resolve(__dirname, '../../uniapp/pages/commission/components/account-info.vue');
|
||||
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("sheep.$router.go('/pages/commission/rewards')"), '查看明细直达收益明细');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 7. 模拟行为逻辑断言
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 7. 模拟算法与资金业务流断言 ---');
|
||||
|
||||
// 模拟场景 A: 默认配置读取
|
||||
function mockGetWalletType(config) {
|
||||
const val = config['partner_reward_wallet'] || 'money';
|
||||
return ['money', 'commission'].includes(val) ? val : 'money';
|
||||
}
|
||||
assert(mockGetWalletType({}) === 'money', '未配置 partner_reward_wallet 时,默认返回 money');
|
||||
assert(mockGetWalletType({ partner_reward_wallet: 'commission' }) === 'commission', '配置 commission 时,支持切换为 commission');
|
||||
assert(mockGetWalletType({ partner_reward_wallet: 'invalid_type' }) === 'money', '配置非法类型时,安全回退至 money');
|
||||
|
||||
// 模拟场景 B: 收益入账模拟
|
||||
function mockAccountReward(user, agent, amount, event) {
|
||||
const walletType = mockGetWalletType({});
|
||||
const beforeMoney = user[walletType];
|
||||
user[walletType] = (parseFloat(user[walletType]) + amount).toFixed(2);
|
||||
agent.total_income = (parseFloat(agent.total_income) + amount).toFixed(2);
|
||||
|
||||
const walletLog = {
|
||||
user_id: user.id,
|
||||
type: walletType,
|
||||
amount: amount,
|
||||
before: beforeMoney,
|
||||
after: user[walletType],
|
||||
event: event,
|
||||
};
|
||||
return walletLog;
|
||||
}
|
||||
|
||||
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.commission === '0.00', '模拟即时奖入账:佣金钱包维持 0.00 未动');
|
||||
assert(mockAgent.total_income === '898.00', '模拟即时奖入账:分销商累计收益从 500 累加至 898.00');
|
||||
assert(log1.type === 'money' && log1.event === 'push3', '产生正确的 money 钱包变更流水');
|
||||
|
||||
// 模拟场景 C: 退款智能回滚判断
|
||||
function mockDetermineRefundWallet(walletLogs, rewardId) {
|
||||
const found = walletLogs.find(l => l.partner_reward_id === rewardId);
|
||||
if (found && found.type === 'commission') {
|
||||
return 'commission';
|
||||
}
|
||||
return 'money';
|
||||
}
|
||||
|
||||
const testLogs = [
|
||||
{ id: 1, type: 'commission', partner_reward_id: 101 }, // 历史老订单
|
||||
{ id: 2, type: 'money', partner_reward_id: 102 }, // 新订单
|
||||
];
|
||||
|
||||
assert(mockDetermineRefundWallet(testLogs, 101) === 'commission', '历史老订单收益退款:精准定位回滚 commission 钱包');
|
||||
assert(mockDetermineRefundWallet(testLogs, 102) === 'money', '新订单收益退款:精准定位回滚 money 钱包');
|
||||
assert(mockDetermineRefundWallet(testLogs, 999) === 'money', '无流水记录极端情况:默认回退扣减当前生效的 money 钱包');
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
|
||||
console.log('========================================================');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 奖项名称全局规范重命名专项验证测试套件
|
||||
*
|
||||
* 验证目标:
|
||||
* 1. 区县统筹奖 -> 区县管理奖
|
||||
* 2. 区县加权奖励 -> 区县加权奖
|
||||
* 3. 酒道馆加权奖励 -> 酒道馆加权奖
|
||||
* 4. 酒道馆运营奖 -> 酒道馆管理奖
|
||||
* 5. 酒道馆协同奖 -> 酒道馆平级奖(区县代为区县代平级奖)
|
||||
* 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++;
|
||||
}
|
||||
}
|
||||
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 奖项名称全局规范重命名专项验证');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// 1. PartnerConfig.php 验证
|
||||
console.log('--- 1. PartnerConfig.php 核心映射与提示文案验证 ---');
|
||||
const partnerConfigPath = path.join(rootDir, 'addons/shopro/service/partner/PartnerConfig.php');
|
||||
assert(fs.existsSync(partnerConfigPath), 'PartnerConfig.php 文件存在');
|
||||
const partnerConfigContent = fs.readFileSync(partnerConfigPath, 'utf8');
|
||||
assert(partnerConfigContent.includes("'push3' => '推荐提成'"), 'rewardTypeMap 包含 push3: 推荐提成');
|
||||
assert(partnerConfigContent.includes("$venue . '管理奖'"), 'rewardTypeMap 包含 team_venue: 酒道馆管理奖');
|
||||
assert(partnerConfigContent.includes("$countyShort . '管理奖'"), 'rewardTypeMap 包含 team_county: 区县管理奖');
|
||||
assert(partnerConfigContent.includes("$venue . '平级奖'"), 'rewardTypeMap 包含 venue_peer: 酒道馆平级奖');
|
||||
assert(partnerConfigContent.includes("$countyAgentShort . '平级奖'"), 'rewardTypeMap 包含 county_peer: 区县代平级奖');
|
||||
assert(partnerConfigContent.includes("$venue . '加权奖'"), 'rewardTypeMap 包含 venue_share: 酒道馆加权奖');
|
||||
assert(partnerConfigContent.includes("$countyShort . '加权奖'"), 'rewardTypeMap 包含 county_share: 区县加权奖');
|
||||
assert(partnerConfigContent.includes("'no_parent' => '暂无推荐人,不计提推荐提成'"), 'skipReasonText 包含不计提推荐提成');
|
||||
assert(partnerConfigContent.includes("'self_buy' => '本人下单不计提推荐提成'"), 'skipReasonText 包含本人下单不计提推荐提成');
|
||||
|
||||
// 2. 结算服务公式文案验证
|
||||
console.log('\n--- 2. 核心结算服务公式与提示文案验证 ---');
|
||||
const push3ServicePath = path.join(rootDir, 'addons/shopro/service/partner/Push3Reward.php');
|
||||
assert(fs.existsSync(push3ServicePath), 'Push3Reward.php 文件存在');
|
||||
const push3ServiceContent = fs.readFileSync(push3ServicePath, 'utf8');
|
||||
assert(push3ServiceContent.includes('不计提推荐提成'), 'Push3Reward 跳过提示更新为推荐提成');
|
||||
assert(push3ServiceContent.includes('推荐提成第{$seq}单'), 'Push3Reward 收益公式更新为推荐提成');
|
||||
|
||||
const teamSplitPath = path.join(rootDir, 'addons/shopro/service/partner/TeamSplitReward.php');
|
||||
assert(fs.existsSync(teamSplitPath), 'TeamSplitReward.php 文件存在');
|
||||
const teamSplitContent = fs.readFileSync(teamSplitPath, 'utf8');
|
||||
assert(teamSplitContent.includes('酒道馆管理奖 {$venueSplit}'), 'TeamSplitReward 包含酒道馆管理奖公式');
|
||||
assert(teamSplitContent.includes('区县管理奖 {$amount}'), 'TeamSplitReward 包含区县管理奖公式');
|
||||
|
||||
const peerSharePath = path.join(rootDir, 'addons/shopro/service/partner/PeerShareReward.php');
|
||||
assert(fs.existsSync(peerSharePath), 'PeerShareReward.php 文件存在');
|
||||
const peerShareContent = fs.readFileSync(peerSharePath, 'utf8');
|
||||
assert(peerShareContent.includes('{$label}平级(第{$gen}代)'), 'PeerShareReward 包含平级公式');
|
||||
assert(peerShareContent.includes('{$label}平级(第{$gen}代) 30 日无自购'), 'PeerShareReward 包含平级跳过提示');
|
||||
|
||||
// 3. WalletLog 模型字典映射验证
|
||||
console.log('\n--- 3. WalletLog 模型事件字典映射验证 ---');
|
||||
const walletLogPath = path.join(rootDir, 'application/admin/model/shopro/user/WalletLog.php');
|
||||
assert(fs.existsSync(walletLogPath), 'WalletLog.php 文件存在');
|
||||
const walletLogContent = fs.readFileSync(walletLogPath, 'utf8');
|
||||
assert(walletLogContent.includes("'push3' => '推荐提成'"), 'WalletLog 包含推荐提成');
|
||||
assert(walletLogContent.includes("'team_venue' => '酒道馆管理奖'"), 'WalletLog 包含酒道馆管理奖');
|
||||
assert(walletLogContent.includes("'team_county' => '区县管理奖'"), 'WalletLog 包含区县管理奖');
|
||||
assert(walletLogContent.includes("'venue_peer' => '酒道馆平级奖'"), 'WalletLog 包含酒道馆平级奖');
|
||||
assert(walletLogContent.includes("'county_peer' => '区县代平级奖'"), 'WalletLog 包含区县代平级奖');
|
||||
assert(walletLogContent.includes("'venue_share' => '酒道馆加权奖'"), 'WalletLog 包含酒道馆加权奖');
|
||||
assert(walletLogContent.includes("'county_share' => '区县加权奖'"), 'WalletLog 包含区县加权奖');
|
||||
|
||||
// 4. 后台视图与筛选菜单验证
|
||||
console.log('\n--- 4. 后台管理视图与筛选菜单验证 ---');
|
||||
const rewardJsPath = path.join(rootDir, 'public/assets/js/backend/shopro/partner/reward.js');
|
||||
assert(fs.existsSync(rewardJsPath), 'reward.js 文件存在');
|
||||
const rewardJsContent = fs.readFileSync(rewardJsPath, 'utf8');
|
||||
assert(rewardJsContent.includes("{ label: '推荐提成', value: 'push3' }"), 'reward.js 筛选包含推荐提成');
|
||||
assert(rewardJsContent.includes("{ label: '酒道馆管理奖', value: 'team_venue' }"), 'reward.js 筛选包含酒道馆管理奖');
|
||||
assert(rewardJsContent.includes("{ label: '区县管理奖', value: 'team_county' }"), 'reward.js 筛选包含区县管理奖');
|
||||
assert(rewardJsContent.includes("{ label: '酒道馆平级奖', value: 'venue_peer' }"), 'reward.js 筛选包含酒道馆平级奖');
|
||||
assert(rewardJsContent.includes("{ label: '区县代平级奖', value: 'county_peer' }"), 'reward.js 筛选包含区县代平级奖');
|
||||
assert(rewardJsContent.includes("{ label: '酒道馆加权奖', value: 'venue_share' }"), 'reward.js 筛选包含酒道馆加权奖');
|
||||
assert(rewardJsContent.includes("{ label: '区县加权奖', value: 'county_share' }"), 'reward.js 筛选包含区县加权奖');
|
||||
|
||||
const configViewPath = path.join(rootDir, 'application/admin/view/shopro/config/index.html');
|
||||
assert(fs.existsSync(configViewPath), 'config/index.html 文件存在');
|
||||
const configViewContent = fs.readFileSync(configViewPath, 'utf8');
|
||||
assert(configViewContent.includes('<div class="sa-title is-line">推荐提成</div>'), 'config/index.html 配置卡片更新为推荐提成');
|
||||
|
||||
const goodsEditPath = path.join(rootDir, 'application/admin/view/shopro/commission/goods/edit.html');
|
||||
assert(fs.existsSync(goodsEditPath), 'commission/goods/edit.html 文件存在');
|
||||
const goodsEditContent = fs.readFileSync(goodsEditPath, 'utf8');
|
||||
assert(goodsEditContent.includes('推荐提成(推三返一)'), 'goods/edit.html 预览卡片更新为推荐提成');
|
||||
assert(goodsEditContent.includes('实体渠道管理奖'), 'goods/edit.html 预览卡片更新为实体渠道管理奖');
|
||||
assert(goodsEditContent.includes('馆/县平级奖'), 'goods/edit.html 预览卡片更新为馆/县平级奖');
|
||||
assert(goodsEditContent.includes('1. 推荐提成(推三返一循环返现)'), 'goods/edit.html section 1 标题更新为推荐提成');
|
||||
assert(goodsEditContent.includes('酒道馆管理奖基准'), 'goods/edit.html 字段更新为酒道馆管理奖基准');
|
||||
assert(goodsEditContent.includes('区县管理全额'), 'goods/edit.html 字段更新为区县管理全额');
|
||||
|
||||
const dailyViewPath = path.join(rootDir, 'application/admin/view/shopro/partner/daily/index.html');
|
||||
assert(fs.existsSync(dailyViewPath), 'partner/daily/index.html 文件存在');
|
||||
const dailyViewContent = fs.readFileSync(dailyViewPath, 'utf8');
|
||||
assert(dailyViewContent.includes('酒道馆加权奖池'), 'daily/index.html 包含酒道馆加权奖池');
|
||||
assert(dailyViewContent.includes('区县加权奖池'), 'daily/index.html 包含区县加权奖池');
|
||||
|
||||
// 5. Uniapp 用户端验证
|
||||
console.log('\n--- 5. Uniapp 用户端分类与文案验证 ---');
|
||||
const rewardsVuePath = path.join(rootDir, 'uniapp/pages/commission/rewards.vue');
|
||||
assert(fs.existsSync(rewardsVuePath), 'rewards.vue 文件存在');
|
||||
const rewardsVueContent = fs.readFileSync(rewardsVuePath, 'utf8');
|
||||
assert(rewardsVueContent.includes("{ name: '推荐提成', value: 'push3' }"), 'rewards.vue 包含推荐提成筛选Tab');
|
||||
assert(rewardsVueContent.includes("{ name: '加权奖', value: 'share' }"), 'rewards.vue 包含加权奖筛选Tab');
|
||||
|
||||
const teamVuePath = path.join(rootDir, 'uniapp/pages/commission/team.vue');
|
||||
assert(fs.existsSync(teamVuePath), 'team.vue 文件存在');
|
||||
const teamVueContent = fs.readFileSync(teamVuePath, 'utf8');
|
||||
assert(teamVueContent.includes('提成循环序号'), 'team.vue 包含提成循环序号指标');
|
||||
assert(teamVueContent.includes('推荐提成序号:'), 'team.vue 包含推荐提成序号说明');
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
|
||||
console.log('========================================================');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 验证酒道馆与区县代理双重身份及全拓扑场景下的团队统筹拆分奖专项测试
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, message) {
|
||||
if (condition) {
|
||||
passed++;
|
||||
console.log(` [PASS] ${message}`);
|
||||
} else {
|
||||
failed++;
|
||||
console.error(` [FAIL] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 酒道馆与区县代理双重身份及全拓扑场景团队奖专项测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// 1. 静态代码分析
|
||||
const teamSplitFile = path.resolve(__dirname, '../../addons/shopro/service/partner/TeamSplitReward.php');
|
||||
assert(fs.existsSync(teamSplitFile), 'TeamSplitReward.php 文件存在');
|
||||
const code = fs.readFileSync(teamSplitFile, 'utf8');
|
||||
|
||||
assert(code.includes('$start = $venue ? $venue[\'index\'] : 0;'), 'nearestCounty 起始查找索引支持从当前酒道馆节点匹配兼职区县代理');
|
||||
assert(code.includes('馆兼县'), '支持「馆兼县」专属流水明细公式标识');
|
||||
|
||||
// 2. 模拟 TeamSplitReward 核心算法逻辑
|
||||
class MockActiveService {
|
||||
isActive(node) {
|
||||
return node.is_active !== false;
|
||||
}
|
||||
}
|
||||
|
||||
class MockTeamSplitReward {
|
||||
run(order, buyerId, chain, active, rules = null) {
|
||||
const venueSplit = (rules && typeof rules.venue_split === 'number') ? rules.venue_split : 50;
|
||||
const countyFull = (rules && typeof rules.county_full === 'number') ? rules.county_full : 100;
|
||||
const result = { venue: null, county: null, skips: [] };
|
||||
|
||||
const venue = this.nearest(chain, 'is_venue', active);
|
||||
const county = this.nearestCounty(chain, venue, active);
|
||||
|
||||
if (!venue && !county) {
|
||||
result.skips.push({ type: 'team_venue', reason: 'not_venue', formula: '链上无酒道馆且无区县' });
|
||||
return result;
|
||||
}
|
||||
|
||||
if (venue) {
|
||||
if (venue.inactive) {
|
||||
result.skips.push({ type: 'team_venue', user_id: venue.user_id, reason: 'inactive_30d', formula: '酒道馆 30 日无自购' });
|
||||
} else {
|
||||
result.venue = { user_id: venue.user_id, amount: venueSplit, index: venue.index };
|
||||
}
|
||||
}
|
||||
|
||||
if (county) {
|
||||
const amount = venue ? venueSplit : countyFull;
|
||||
if (county.inactive) {
|
||||
result.skips.push({ type: 'team_county', user_id: county.user_id, reason: 'inactive_30d', formula: '区县 30 日无自购' });
|
||||
} else {
|
||||
const formulaText = venue
|
||||
? (venue.user_id === county.user_id ? `区县管理奖 ${amount}(馆兼县)` : `区县管理奖 ${amount}(馆上)`)
|
||||
: `区县管理奖 ${amount}(直达)`;
|
||||
result.county = { user_id: county.user_id, amount, index: county.index, formula: formulaText };
|
||||
}
|
||||
} else if (venue && !county) {
|
||||
result.skips.push({ type: 'team_county', user_id: 0, reason: 'not_county', formula: '有馆无区县' });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
nearest(chain, flag, active) {
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
if (!chain[i][flag]) continue;
|
||||
return {
|
||||
user_id: chain[i].user_id,
|
||||
index: i,
|
||||
inactive: !active.isActive(chain[i]),
|
||||
node: chain[i]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
nearestCounty(chain, venue, active) {
|
||||
const start = venue ? venue.index : 0;
|
||||
for (let i = start; i < chain.length; i++) {
|
||||
if (!chain[i].is_county) continue;
|
||||
return {
|
||||
user_id: chain[i].user_id,
|
||||
index: i,
|
||||
inactive: !active.isActive(chain[i]),
|
||||
node: chain[i]
|
||||
};
|
||||
}
|
||||
if (!venue) {
|
||||
return this.nearest(chain, 'is_county', active);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const runner = new MockTeamSplitReward();
|
||||
const active = new MockActiveService();
|
||||
|
||||
// 场景 1:同一人兼任酒道馆与区县代理(买家25 -> 上级20兼具馆+县,即本Bug场景)
|
||||
{
|
||||
const chain = [
|
||||
{ user_id: 25, is_venue: 0, is_county: 0 },
|
||||
{ user_id: 20, is_venue: 1, is_county: 1, is_active: true }
|
||||
];
|
||||
const res = runner.run({ id: 12 }, 25, chain, active);
|
||||
assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景1: 上级20号成功获得酒道馆奖 50 元');
|
||||
assert(res.county !== null && res.county.user_id === 20 && res.county.amount === 50, '场景1: 上级20号兼任区县代理,成功获得区县管理奖 50 元');
|
||||
assert(res.county.formula.includes('馆兼县'), '场景1: 公式正确标识为「区县管理奖 50(馆兼县)」');
|
||||
assert(res.venue.amount + res.county.amount === 100, '场景1: 团队拆分总额严格守恒为 100 元 (50 + 50)');
|
||||
assert(res.skips.length === 0, '场景1: 无错误跳过记录');
|
||||
}
|
||||
|
||||
// 场景 2:酒道馆与区县代理为不同上级(买家25 -> 馆20 -> 县30)
|
||||
{
|
||||
const chain = [
|
||||
{ user_id: 25, is_venue: 0, is_county: 0 },
|
||||
{ user_id: 20, is_venue: 1, is_county: 0, is_active: true },
|
||||
{ user_id: 30, is_venue: 0, is_county: 1, is_active: true }
|
||||
];
|
||||
const res = runner.run({ id: 13 }, 25, chain, active);
|
||||
assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景2: 独立酒道馆20号获得 50 元');
|
||||
assert(res.county !== null && res.county.user_id === 30 && res.county.amount === 50, '场景2: 上层区县代理30号获得 50 元');
|
||||
assert(res.county.formula.includes('馆上'), '场景2: 公式正确标识为「区县管理奖 50(馆上)」');
|
||||
assert(res.venue.amount + res.county.amount === 100, '场景2: 团队拆分总额严格守恒为 100 元');
|
||||
}
|
||||
|
||||
// 场景 3:无馆有县(买家25 -> 县20)
|
||||
{
|
||||
const chain = [
|
||||
{ user_id: 25, is_venue: 0, is_county: 0 },
|
||||
{ user_id: 20, is_venue: 0, is_county: 1, is_active: true }
|
||||
];
|
||||
const res = runner.run({ id: 14 }, 25, chain, active);
|
||||
assert(res.venue === null, '场景3: 无酒道馆');
|
||||
assert(res.county !== null && res.county.user_id === 20 && res.county.amount === 100, '场景3: 无馆区县独享 100 元全额');
|
||||
assert(res.county.formula.includes('直达'), '场景3: 公式正确标识为「区县管理奖 100(直达)」');
|
||||
}
|
||||
|
||||
// 场景 4:有馆无县(买家25 -> 馆20,上方无区县)
|
||||
{
|
||||
const chain = [
|
||||
{ user_id: 25, is_venue: 0, is_county: 0 },
|
||||
{ user_id: 20, is_venue: 1, is_county: 0, is_active: true }
|
||||
];
|
||||
const res = runner.run({ id: 15 }, 25, chain, active);
|
||||
assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景4: 馆20号获得 50 元');
|
||||
assert(res.county === null, '场景4: 无区县,跳过发奖');
|
||||
assert(res.skips.length === 1 && res.skips[0].reason === 'not_county', '场景4: 产生「有馆无区县」跳过记录');
|
||||
}
|
||||
|
||||
// 场景 5:馆与县均不存在
|
||||
{
|
||||
const chain = [
|
||||
{ user_id: 25, is_venue: 0, is_county: 0 },
|
||||
{ user_id: 20, is_venue: 0, is_county: 0, is_active: true }
|
||||
];
|
||||
const res = runner.run({ id: 16 }, 25, chain, active);
|
||||
assert(res.venue === null && res.county === null, '场景5: 均不存在');
|
||||
assert(res.skips.length === 1 && res.skips[0].reason === 'not_venue', '场景5: 记录链上无酒道馆且无区县');
|
||||
}
|
||||
|
||||
// 场景 6:买家本人自购节点兼任馆或县(自购保活与自购分成合法性)
|
||||
{
|
||||
const chain = [
|
||||
{ user_id: 20, is_venue: 1, is_county: 1, is_active: true }
|
||||
];
|
||||
const res = runner.run({ id: 17 }, 20, chain, active);
|
||||
assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景6: 自购节点获得酒道馆运营奖 50 元');
|
||||
assert(res.county !== null && res.county.user_id === 20 && res.county.amount === 50, '场景6: 自购节点获得区县管理奖 50 元');
|
||||
assert(res.venue.amount + res.county.amount === 100, '场景6: 自购节点拆分总额 100 元');
|
||||
}
|
||||
|
||||
console.log('\n--------------------------------------------------------');
|
||||
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
|
||||
console.log('========================================================\n');
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace tests\partner;
|
||||
|
||||
define('APP_PATH', __DIR__ . '/../../application/');
|
||||
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
|
||||
require_once __DIR__ . '/../../thinkphp/base.php';
|
||||
require_once __DIR__ . '/../../thinkphp/helper.php';
|
||||
require_once __DIR__ . '/../../application/common.php';
|
||||
|
||||
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
|
||||
\think\Config::set(include APP_PATH . 'database.php', 'database');
|
||||
\think\Loader::addNamespace('app', APP_PATH);
|
||||
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
|
||||
|
||||
use addons\shopro\service\commission\Agent as AgentService;
|
||||
use app\admin\model\shopro\commission\Agent as AgentModel;
|
||||
use addons\shopro\controller\commission\Agent as AgentController;
|
||||
|
||||
class VerifyTeamSelfOrderSpec
|
||||
{
|
||||
private $passed = 0;
|
||||
private $failed = 0;
|
||||
|
||||
public function run()
|
||||
{
|
||||
echo "========================================================\n";
|
||||
echo " 团队订单包含自己的订单 (Self Order Included) 专项测试 \n";
|
||||
echo "========================================================\n\n";
|
||||
|
||||
$this->testAgentTeamStatsIncludeSelf();
|
||||
$this->testTeamHealthSummaryIncludeSelf();
|
||||
|
||||
echo "\n--------------------------------------------------------\n";
|
||||
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
|
||||
echo "========================================================\n";
|
||||
return $this->failed === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
private function assert($condition, $testName, $detail = '')
|
||||
{
|
||||
if ($condition) {
|
||||
$this->passed++;
|
||||
echo " [✓ PASS] {$testName}\n";
|
||||
} else {
|
||||
$this->failed++;
|
||||
echo " [✗ FAIL] {$testName} - {$detail}\n";
|
||||
}
|
||||
}
|
||||
|
||||
private function testAgentTeamStatsIncludeSelf()
|
||||
{
|
||||
try {
|
||||
$agent = new AgentService(20);
|
||||
$agent->runAgentUpgradePlan(false);
|
||||
$agentRow = AgentModel::get(20);
|
||||
|
||||
// 自购单数 >= 1
|
||||
$this->assert($agentRow->child_order_count_0 >= 1, '分销商自购订单数已记录 (child_order_count_0 >= 1)');
|
||||
|
||||
// 团队总单数 = 自购单数 + 直推单数 + 伞下单数 (当前为 1自购 + 3直推 + 1伞下 = 5单)
|
||||
$this->assert($agentRow->child_order_count_all >= 5,
|
||||
"团队总单数包含自购订单 (当前团队总单数: {$agentRow->child_order_count_all}, 包含自购 {$agentRow->child_order_count_0} 单)");
|
||||
|
||||
// 团队总金额包含自购金额
|
||||
$this->assert(floatval($agentRow->child_order_money_all) >= 6990.00,
|
||||
"团队总金额包含自购金额 (当前团队总金额: {$agentRow->child_order_money_all})");
|
||||
} catch (\Throwable $e) {
|
||||
$this->assert(false, '分销商团队数据统计包含自购测试', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function testTeamHealthSummaryIncludeSelf()
|
||||
{
|
||||
try {
|
||||
$ref = new \ReflectionClass(AgentController::class);
|
||||
$controller = $ref->newInstanceWithoutConstructor();
|
||||
|
||||
$summaryMethod = $ref->getMethod('getTeamHealthSummary');
|
||||
$summaryMethod->setAccessible(true);
|
||||
$window = time() - 30 * 86400;
|
||||
$expiring = time() - 23 * 86400;
|
||||
$summary = $summaryMethod->invoke($controller, 20, [21, 22, 23], $window, $expiring);
|
||||
|
||||
// 团队累计订单数应为 5 笔(包含下级与本人自购)
|
||||
$this->assert(isset($summary['team_order_count']) && $summary['team_order_count'] >= 5,
|
||||
"团队健康度汇总 team_order_count 应为 5 笔 (实测: " . ($summary['team_order_count'] ?? 0) . ")");
|
||||
|
||||
// 团队累计自购总业绩应为 6990.00(包含下级与本人自购)
|
||||
$this->assert(isset($summary['team_total_consume']) && floatval($summary['team_total_consume']) >= 6990.00,
|
||||
"团队累计总业绩 team_total_consume 应为 6990.00 (实测: " . ($summary['team_total_consume'] ?? '0.00') . ")");
|
||||
} catch (\Throwable $e) {
|
||||
$this->assert(false, '团队健康度汇总包含本人自购测试', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
$tester = new VerifyTeamSelfOrderSpec();
|
||||
exit($tester->run());
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 历史收益全量结转至商城余额专项测试
|
||||
* 覆盖范围:
|
||||
* 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);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 钱包收益记录与端到端UI对齐专项测试
|
||||
* 覆盖范围:
|
||||
* 1. addons/shopro/controller/user/WalletLog.php 收益流水聚合及 tab=reward 筛选
|
||||
* 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 筛选与动态指示胶囊对齐
|
||||
* 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(' 钱包收益记录与端到端UI对齐专项测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 1. 用户端 API: addons/shopro/controller/user/WalletLog.php
|
||||
// ----------------------------------------------------
|
||||
console.log('--- 1. 用户端 WalletLog 控制器聚合与对齐验证 ---');
|
||||
const userWalletLogPath = path.resolve(__dirname, '../../addons/shopro/controller/user/WalletLog.php');
|
||||
assert(fs.existsSync(userWalletLogPath), 'addons/shopro/controller/user/WalletLog.php 文件存在');
|
||||
if (fs.existsSync(userWalletLogPath)) {
|
||||
const content = fs.readFileSync(userWalletLogPath, 'utf-8');
|
||||
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_to_money'"), 'rewardEvents 包含结转事件 transfer_to_money');
|
||||
assert(content.includes("case 'reward':"), '支持 tab=reward 收益分成专属筛选');
|
||||
assert(content.includes("'reward_income' => $rewardIncome"), '接口返回收益分成累计金额统计');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 2. 后台 API: application/admin/controller/shopro/user/WalletLog.php
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 2. 后台 WalletLog 控制器佣金流水联查验证 ---');
|
||||
const adminWalletLogPath = path.resolve(__dirname, '../../application/admin/controller/shopro/user/WalletLog.php');
|
||||
assert(fs.existsSync(adminWalletLogPath), 'application/admin/controller/shopro/user/WalletLog.php 文件存在');
|
||||
if (fs.existsSync(adminWalletLogPath)) {
|
||||
const content = fs.readFileSync(adminWalletLogPath, 'utf-8');
|
||||
assert(content.includes('public function commission($id)'), '后台包含 commission($id) 接口');
|
||||
assert(content.includes("where('type', 'commission')"), '后台查询历史佣金');
|
||||
assert(content.includes("where('type', 'money')->whereIn('event', $rewardEvents)"), '后台同时联查收益分成直入余额记录');
|
||||
assert(content.includes("'transfer_to_money'"), '后台 rewardEvents 包含 transfer_to_money');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 3. 后台详情视图: application/admin/view/shopro/user/user/detail.html
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 3. 后台用户详情 detail.html 资产看板与明细对齐验证 ---');
|
||||
const detailHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/user/user/detail.html');
|
||||
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.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 区分商城余额与佣金钱包');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 4. uniapp 我的收益: uniapp/pages/user/wallet/commission.vue
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 4. uniapp 我的收益页面 (commission.vue) 对齐验证 ---');
|
||||
const commissionVuePath = path.resolve(__dirname, '../../uniapp/pages/user/wallet/commission.vue');
|
||||
assert(fs.existsSync(commissionVuePath), 'commission.vue 文件存在');
|
||||
if (fs.existsSync(commissionVuePath)) {
|
||||
const content = fs.readFileSync(commissionVuePath, 'utf-8');
|
||||
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时才显示「转余额」操作');
|
||||
assert(content.includes('累计总收益'), '次要指标区展示「累计总收益」');
|
||||
assert(content.includes('待结算收益'), '次要指标区展示「待结算收益」');
|
||||
assert(content.includes("name: '收益分成'") && content.includes("value: 'reward'"), 'tabMaps 增加「收益分成」(value: reward) 选项');
|
||||
assert(content.includes("item.type === 'money'"), '流水记录列表对直入余额项进行识别');
|
||||
assert(content.includes('已入余额'), '直入余额项显示「已入余额」专属胶囊标签');
|
||||
assert(content.includes('收益分成已全额直接入账商城可用余额'), '底部提示文案明确说明收益直接入账商城余额');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 5. uniapp 钱包余额: uniapp/pages/user/wallet/money.vue
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 5. uniapp 钱包余额页面 (money.vue) 对齐验证 ---');
|
||||
const moneyVuePath = path.resolve(__dirname, '../../uniapp/pages/user/wallet/money.vue');
|
||||
assert(fs.existsSync(moneyVuePath), 'money.vue 文件存在');
|
||||
if (fs.existsSync(moneyVuePath)) {
|
||||
const content = fs.readFileSync(moneyVuePath, 'utf-8');
|
||||
assert(content.includes("name: '收益分成'") && content.includes("value: 'reward'"), 'money.vue 的 tabMaps 增加「收益分成」筛选');
|
||||
assert(content.includes('收益分成全额入账'), '动态指示胶囊说明收益分成全额入账可直接抵扣');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 6. 流水联查与筛选算法逻辑模拟
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 6. 流水联查与聚合筛选算法模拟验证 ---');
|
||||
const rewardEvents = [
|
||||
'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
|
||||
'team_venue', 'team_county', 'venue_peer', 'county_peer',
|
||||
'venue_share', 'county_share', 'reward_income', 'transfer_by_commission', 'transfer_to_money'
|
||||
];
|
||||
|
||||
// 模拟钱包日志混合数据池
|
||||
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: 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: '酒道馆订单奖' },
|
||||
{ id: 6, type: 'money', event: 'consume', amount: -120.00, memo: '商城订单支付' },
|
||||
{ id: 7, type: 'score', event: 'sign', amount: 10, memo: '每日签到' },
|
||||
];
|
||||
|
||||
function queryWalletLogs(type, tab) {
|
||||
// 模拟 WalletLog 控制器 index 逻辑
|
||||
return mockLogs.filter(log => {
|
||||
// 1. 类型过滤 / commission 聚合
|
||||
if (type === 'commission') {
|
||||
const isCommission = log.type === 'commission';
|
||||
const isMoneyReward = log.type === 'money' && rewardEvents.includes(log.event);
|
||||
if (!isCommission && !isMoneyReward) return false;
|
||||
} else {
|
||||
if (log.type !== type) return false;
|
||||
}
|
||||
|
||||
// 2. tab 筛选
|
||||
if (tab === 'reward') {
|
||||
return log.amount > 0 && rewardEvents.includes(log.event);
|
||||
} else if (tab === 'income') {
|
||||
return log.amount > 0;
|
||||
} else if (tab === 'expense') {
|
||||
return log.amount < 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// 6.1 在 commission 页面查询全部
|
||||
const commissionAll = queryWalletLogs('commission', 'all');
|
||||
assert(commissionAll.length === 4, `commission 页面查全部包含 4 条记录 (实际: ${commissionAll.length})`);
|
||||
assert(commissionAll.some(l => l.id === 1 && l.type === 'commission'), '包含历史佣金记录 ID=1');
|
||||
assert(commissionAll.some(l => l.id === 2 && l.type === 'commission'), '包含佣金转出记录 ID=2');
|
||||
assert(commissionAll.some(l => l.id === 3 && l.type === 'money'), '包含收益转余额记录 ID=3');
|
||||
assert(commissionAll.some(l => l.id === 5 && l.type === 'money'), '包含直入余额收益记录 ID=5');
|
||||
assert(!commissionAll.some(l => l.id === 4), '不包含普通微信充值记录 ID=4');
|
||||
assert(!commissionAll.some(l => l.id === 6), '不包含普通消费扣款记录 ID=6');
|
||||
|
||||
// 6.2 在 commission 页面筛选收益分成 (tab=reward)
|
||||
const commissionReward = queryWalletLogs('commission', 'reward');
|
||||
assert(commissionReward.length === 3, `commission 页面筛选收益分成包含 3 条记录 (实际: ${commissionReward.length})`);
|
||||
assert(commissionReward.every(l => l.amount > 0), '收益分成记录金额全部为正');
|
||||
assert(!commissionReward.some(l => l.id === 2), '不包含负向转出记录');
|
||||
|
||||
// 6.3 在 money 页面筛选收益分成 (tab=reward)
|
||||
const moneyReward = queryWalletLogs('money', 'reward');
|
||||
assert(moneyReward.length === 2, `money 页面筛选收益分成包含 2 条直入余额收益记录 (实际: ${moneyReward.length})`);
|
||||
assert(moneyReward.some(l => l.id === 3), '包含转余额入账 ID=3');
|
||||
assert(moneyReward.some(l => l.id === 5), '包含直推酒道馆收益 ID=5');
|
||||
assert(!moneyReward.some(l => l.id === 4), '不包含充值 ID=4');
|
||||
|
||||
// 6.4 收入支出统计
|
||||
const totalIncome = commissionAll.filter(l => l.amount > 0).reduce((sum, l) => sum + l.amount, 0);
|
||||
const totalExpense = commissionAll.filter(l => l.amount < 0).reduce((sum, l) => sum + l.amount, 0);
|
||||
assert(totalIncome === (398.00 + 398.00 + 50.00), `收益中心统计总收入正确 (¥${totalIncome})`);
|
||||
assert(totalExpense === -398.00, `收益中心统计总支出正确 (¥${totalExpense})`);
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(`测试完成!通过: \x1b[32m${passCount}\x1b[0m, 失败: \x1b[31m${failCount}\x1b[0m`);
|
||||
console.log('========================================================');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户