feat(commission): 支持计奖商品参与设置、独立分成与多规格规则,完善团队中心与微信快捷登录
这个提交包含在:
@@ -0,0 +1,164 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail = '') {
|
||||
if (condition) {
|
||||
console.log(` [PASS] ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.error(` [FAIL] ${testName} - ${detail}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("========================================================");
|
||||
console.log(" 分销商成员档案 URL 路由 & 参数回显自动化规范测试");
|
||||
console.log("========================================================\n");
|
||||
|
||||
// 1. 验证 dashboard.js 中 onOpenAgent 修复
|
||||
const dashboardJsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/dashboard.js');
|
||||
assert(fs.existsSync(dashboardJsPath), 'dashboard.js 文件存在');
|
||||
const dashboardContent = fs.readFileSync(dashboardJsPath, 'utf-8');
|
||||
|
||||
assert(!dashboardContent.includes('`shopro/commission/agent/index` + (level ? `&level=${level}` : \'\')'),
|
||||
'dashboard.js 中已彻底移除错误的 &level= 拼接语法');
|
||||
|
||||
assert(dashboardContent.includes('`shopro/commission/agent/index` + (level !== undefined && level !== null && level !== \'\' ? `?level=${level}` : \'\')'),
|
||||
'dashboard.js 中使用健壮的 ?level= 拼接参数');
|
||||
|
||||
// 模拟测试 onOpenAgent 各种参数行为
|
||||
function mockOnOpenAgent(level) {
|
||||
let captured = {};
|
||||
function onOpenTab(url, title) {
|
||||
captured = { url, title };
|
||||
}
|
||||
let url = `shopro/commission/agent/index` + (level !== undefined && level !== null && level !== '' ? `?level=${level}` : '');
|
||||
let title = (level == 1) ? "品酒师会员" : "成员档案";
|
||||
onOpenTab(url, title);
|
||||
return captured;
|
||||
}
|
||||
|
||||
const res1 = mockOnOpenAgent(1);
|
||||
assert(res1.url === 'shopro/commission/agent/index?level=1' && res1.title === '品酒师会员',
|
||||
'正常场景: level=1 生成正确的 query string ?level=1 且标题为品酒师会员');
|
||||
|
||||
const resEmpty = mockOnOpenAgent('');
|
||||
assert(resEmpty.url === 'shopro/commission/agent/index' && resEmpty.title === '成员档案',
|
||||
'边界场景: level="" 生成基础无参数 URL');
|
||||
|
||||
const resNull = mockOnOpenAgent(null);
|
||||
assert(resNull.url === 'shopro/commission/agent/index' && resNull.title === '成员档案',
|
||||
'边界场景: level=null 生成基础无参数 URL');
|
||||
|
||||
const resUndef = mockOnOpenAgent(undefined);
|
||||
assert(resUndef.url === 'shopro/commission/agent/index' && resUndef.title === '成员档案',
|
||||
'边界场景: level=undefined 生成基础无参数 URL');
|
||||
|
||||
// 2. 验证 agent.js URL 参数解析与初始化
|
||||
const agentJsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/commission/agent.js');
|
||||
assert(fs.existsSync(agentJsPath), 'agent.js 文件存在');
|
||||
const agentContent = fs.readFileSync(agentJsPath, 'utf-8');
|
||||
|
||||
assert(agentContent.includes('is_partner: \'\''), 'agent.js filter.data 包含 is_partner');
|
||||
assert(agentContent.includes('is_partner: {') && agentContent.includes('label: \'合伙股东\''), 'agent.js filter.tools 包含 is_partner 筛选工具');
|
||||
assert(agentContent.includes('{ value: \'partner\', label: \'合伙股东\' }'), 'agent.js filter.tools.identity 选项中包含合伙股东');
|
||||
assert(agentContent.includes('new URLSearchParams(location.search)'), 'agent.js onMounted 中使用 URLSearchParams 解析参数');
|
||||
assert(agentContent.includes('params.get(\'level\')'), 'agent.js onMounted 读取 level 参数');
|
||||
assert(agentContent.includes('params.get(\'is_venue\')'), 'agent.js onMounted 读取 is_venue 参数');
|
||||
assert(agentContent.includes('params.get(\'is_county\')'), 'agent.js onMounted 读取 is_county 参数');
|
||||
assert(agentContent.includes('params.get(\'is_partner\')'), 'agent.js onMounted 读取 is_partner 参数');
|
||||
assert(agentContent.includes('params.get(\'identity\')'), 'agent.js onMounted 读取 identity 参数');
|
||||
assert(agentContent.includes('params.get(\'user_id\')'), 'agent.js onMounted 读取 user_id 参数');
|
||||
|
||||
// 模拟测试 agent.js onMounted 参数解析与赋值
|
||||
function mockAgentMount(searchString) {
|
||||
const params = new URLSearchParams(searchString);
|
||||
let state = {
|
||||
filter: {
|
||||
data: { level: '', is_venue: '', is_county: '', is_partner: '', identity: '', user: { field: 'user_id', value: '' } },
|
||||
tools: {
|
||||
level: { value: '' },
|
||||
is_venue: { value: '' },
|
||||
is_county: { value: '' },
|
||||
is_partner: { value: '' },
|
||||
identity: { value: '' },
|
||||
user: { value: { field: 'user_id', value: '' } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const levelParam = params.get('level');
|
||||
if (levelParam !== null && levelParam !== '') {
|
||||
const parsedLevel = !isNaN(Number(levelParam)) ? Number(levelParam) : levelParam;
|
||||
state.filter.data.level = parsedLevel;
|
||||
if (state.filter.tools && state.filter.tools.level) {
|
||||
state.filter.tools.level.value = parsedLevel;
|
||||
}
|
||||
}
|
||||
const isVenueParam = params.get('is_venue');
|
||||
if (isVenueParam !== null && isVenueParam !== '') {
|
||||
state.filter.data.is_venue = isVenueParam;
|
||||
if (state.filter.tools && state.filter.tools.is_venue) {
|
||||
state.filter.tools.is_venue.value = isVenueParam;
|
||||
}
|
||||
}
|
||||
const isCountyParam = params.get('is_county');
|
||||
if (isCountyParam !== null && isCountyParam !== '') {
|
||||
state.filter.data.is_county = isCountyParam;
|
||||
if (state.filter.tools && state.filter.tools.is_county) {
|
||||
state.filter.tools.is_county.value = isCountyParam;
|
||||
}
|
||||
}
|
||||
const isPartnerParam = params.get('is_partner');
|
||||
if (isPartnerParam !== null && isPartnerParam !== '') {
|
||||
state.filter.data.is_partner = isPartnerParam;
|
||||
if (state.filter.tools && state.filter.tools.is_partner) {
|
||||
state.filter.tools.is_partner.value = isPartnerParam;
|
||||
}
|
||||
}
|
||||
const identityParam = params.get('identity');
|
||||
if (identityParam !== null && identityParam !== '') {
|
||||
state.filter.data.identity = identityParam;
|
||||
if (state.filter.tools && state.filter.tools.identity) {
|
||||
state.filter.tools.identity.value = identityParam;
|
||||
}
|
||||
}
|
||||
const userIdParam = params.get('user_id');
|
||||
if (userIdParam !== null && userIdParam !== '') {
|
||||
state.filter.data.user = { field: 'user_id', value: userIdParam };
|
||||
if (state.filter.tools && state.filter.tools.user) {
|
||||
state.filter.tools.user.value = { field: 'user_id', value: userIdParam };
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
const mockState = mockAgentMount('?level=1&ref=addtabs');
|
||||
assert(mockState.filter.data.level === 1 && mockState.filter.tools.level.value === 1,
|
||||
'agent.js 正确从 ?level=1 解析数字等级 1 并同步至 filter.data 与 filter.tools');
|
||||
|
||||
const mockPartnerState = mockAgentMount('?is_partner=1');
|
||||
assert(mockPartnerState.filter.data.is_partner === '1' && mockPartnerState.filter.tools.is_partner.value === '1',
|
||||
'agent.js 正确从 ?is_partner=1 解析合伙股东标记');
|
||||
|
||||
// 3. 验证 AgentFilter.php 过滤能力
|
||||
const filterPhpPath = path.resolve(__dirname, '../addons/shopro/filter/commission/AgentFilter.php');
|
||||
assert(fs.existsSync(filterPhpPath), 'AgentFilter.php 文件存在');
|
||||
const filterPhpContent = fs.readFileSync(filterPhpPath, 'utf-8');
|
||||
|
||||
assert(filterPhpContent.includes('public function level($query, $data)'), 'AgentFilter 显式定义 level 过滤方法');
|
||||
assert(filterPhpContent.includes('public function isPartner($query, $data)'), 'AgentFilter 显式定义 isPartner 过滤方法');
|
||||
assert(filterPhpContent.includes('public function isVenue($query, $data)'), 'AgentFilter 显式定义 isVenue 过滤方法');
|
||||
assert(filterPhpContent.includes('public function isCounty($query, $data)'), 'AgentFilter 显式定义 isCounty 过滤方法');
|
||||
assert(filterPhpContent.includes('if ($value === \'partner\') {') && filterPhpContent.includes('where(\'is_partner\', 1)'),
|
||||
'AgentFilter identity 方法支持合伙股东 (partner)');
|
||||
|
||||
console.log("\n--------------------------------------------------------");
|
||||
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
|
||||
console.log("========================================================");
|
||||
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* 计奖商品独立分成与多规格规则专项验证测试
|
||||
* 覆盖范围:
|
||||
* 1. PartnerConfig (defaultRules, getEffectiveRules, getOrderEffectiveRules)
|
||||
* 2. PartnerRewardService (settleOrder 动态规则提取与分发)
|
||||
* 3. 奖项子服务 (Push3Reward, MakerDiffReward, MakerPeerReward, TeamSplitReward, PeerShareReward, VoucherService)
|
||||
* 4. 后台控制器 Goods.php (detail, edit)
|
||||
* 5. 后台模板 index.html & edit.html
|
||||
* 6. 后台前端脚本 goods.js (Vue 表单交互、多规格规则映射、默认值同步)
|
||||
* 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 defaultRules()'), '包含 defaultRules() 矩阵方法');
|
||||
assert(content.includes("'push3_amounts' => self::push3Amounts()"), 'defaultRules 包含 push3_amounts');
|
||||
assert(content.includes("'maker_bonus' => self::makerBonus()"), 'defaultRules 包含 maker_bonus');
|
||||
assert(content.includes("'maker_peer_rate' => (float) self::get('partner_maker_peer_rate'"), 'defaultRules 包含 maker_peer_rate');
|
||||
assert(content.includes("'maker_overtaken_rate' => (float) self::get('partner_maker_overtaken_rate'"), 'defaultRules 包含 maker_overtaken_rate');
|
||||
assert(content.includes("'venue_split' => (float) self::get('partner_venue_split'"), 'defaultRules 包含 venue_split');
|
||||
assert(content.includes("'county_full' => (float) self::get('partner_county_full'"), 'defaultRules 包含 county_full');
|
||||
assert(content.includes("'voucher_times' => (float) self::get('partner_voucher_times'"), 'defaultRules 包含 voucher_times');
|
||||
assert(content.includes('public static function getEffectiveRules($goodsId, $skuPriceId = 0)'), '包含 getEffectiveRules($goodsId, $skuPriceId) 方法');
|
||||
assert(content.includes('$selfRules !== 1 && $selfRules !== 2'), 'getEffectiveRules 识别 self_rules 规则类型 (0默认/1独立/2批量)');
|
||||
assert(content.includes("isset($custom[$skuKey])"), 'getEffectiveRules 支持根据 skuPriceId 匹配特定规格规则');
|
||||
assert(content.includes("isset($custom['sku_' . $skuKey])"), 'getEffectiveRules 兼容 sku_ 前缀格式');
|
||||
assert(content.includes('public static function getOrderEffectiveRules($order)'), '包含 getOrderEffectiveRules($order) 订单规则解析方法');
|
||||
assert(content.includes("!empty($rules['is_custom'])"), 'getOrderEffectiveRules 优先提取商品专属独立规则');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 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('$orderRules = PartnerConfig::getOrderEffectiveRules($order)'), 'settleOrder 调用 getOrderEffectiveRules 提取订单专属规则');
|
||||
assert(content.includes('(new Push3Reward())->run($order, $buyerId, $parent, $active, $orderRules)'), 'Push3Reward 透传 $orderRules');
|
||||
assert(content.includes('(new MakerDiffReward())->run($order, $buyerId, $parentChain, $active, $orderRules)'), 'MakerDiffReward 透传 $orderRules');
|
||||
assert(content.includes('(new MakerPeerReward())->run($order, $buyerId, $parentChain, $diff, $active, $orderRules)'), 'MakerPeerReward 透传 $orderRules');
|
||||
assert(content.includes('(new TeamSplitReward())->run($order, $buyerId, $selfChain, $active, $orderRules)'), 'TeamSplitReward 透传 $orderRules');
|
||||
assert(content.includes('(new PeerShareReward())->run($order, $buyerId, $selfChain, $team, $active, $orderRules)'), 'PeerShareReward 透传 $orderRules');
|
||||
assert(content.includes('(new VoucherService())->grant($order, $buyerId, $orderRules)'), 'VoucherService grant 透传 $orderRules');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 3. 各奖励子服务入参验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 3. 奖项子服务参数消费验证 ---');
|
||||
const push3Path = path.resolve(__dirname, '../addons/shopro/service/partner/Push3Reward.php');
|
||||
if (fs.existsSync(push3Path)) {
|
||||
const c = fs.readFileSync(push3Path, 'utf-8');
|
||||
assert(c.includes('$rules = null') || c.includes('$rules = []'), 'Push3Reward::run 支持接收 $rules');
|
||||
assert(c.includes("$rules['push3_amounts']"), 'Push3Reward 优先使用 $rules 中的推三返一循环金额');
|
||||
}
|
||||
|
||||
const makerDiffPath = path.resolve(__dirname, '../addons/shopro/service/partner/MakerDiffReward.php');
|
||||
if (fs.existsSync(makerDiffPath)) {
|
||||
const c = fs.readFileSync(makerDiffPath, 'utf-8');
|
||||
assert(c.includes('$rules = null') || c.includes('$rules = []'), 'MakerDiffReward::run 支持接收 $rules');
|
||||
assert(c.includes("$rules['maker_bonus']"), 'MakerDiffReward 优先使用 $rules 中的各级极差基数');
|
||||
}
|
||||
|
||||
const makerPeerPath = path.resolve(__dirname, '../addons/shopro/service/partner/MakerPeerReward.php');
|
||||
if (fs.existsSync(makerPeerPath)) {
|
||||
const c = fs.readFileSync(makerPeerPath, 'utf-8');
|
||||
assert(c.includes('$rules = null') || c.includes('$rules = []'), 'MakerPeerReward::run 支持接收 $rules');
|
||||
assert(c.includes("$rules['maker_peer_rate']"), 'MakerPeerReward 优先使用 $rules 中的创客平级协同比例');
|
||||
assert(c.includes("$rules['maker_overtaken_rate']"), 'MakerPeerReward 优先使用 $rules 中的创客超越卓越比例');
|
||||
}
|
||||
|
||||
const teamSplitPath = path.resolve(__dirname, '../addons/shopro/service/partner/TeamSplitReward.php');
|
||||
if (fs.existsSync(teamSplitPath)) {
|
||||
const c = fs.readFileSync(teamSplitPath, 'utf-8');
|
||||
assert(c.includes('$rules = null') || c.includes('$rules = []'), 'TeamSplitReward::run 支持接收 $rules');
|
||||
assert(c.includes("$rules['venue_split']"), 'TeamSplitReward 优先使用 $rules 中的酒道馆运营分成金额');
|
||||
assert(c.includes("$rules['county_full']"), 'TeamSplitReward 优先使用 $rules 中的区县统筹分成金额');
|
||||
}
|
||||
|
||||
const peerSharePath = path.resolve(__dirname, '../addons/shopro/service/partner/PeerShareReward.php');
|
||||
if (fs.existsSync(peerSharePath)) {
|
||||
const c = fs.readFileSync(peerSharePath, 'utf-8');
|
||||
assert(c.includes('$rules = null') || c.includes('$rules = []'), 'PeerShareReward::run 支持接收 $rules');
|
||||
assert(c.includes("$rules['venue_peer_rate']"), 'PeerShareReward 优先使用 $rules 中的酒道馆协同比例');
|
||||
assert(c.includes("$rules['county_peer_rate']"), 'PeerShareReward 优先使用 $rules 中的区县协同比例');
|
||||
}
|
||||
|
||||
const voucherPath = path.resolve(__dirname, '../addons/shopro/service/partner/VoucherService.php');
|
||||
if (fs.existsSync(voucherPath)) {
|
||||
const c = fs.readFileSync(voucherPath, 'utf-8');
|
||||
assert(c.includes('$rules = null') || c.includes('$rules = []'), 'VoucherService::grant 支持接收 $rules');
|
||||
assert(c.includes("$rules['voucher_times']"), 'VoucherService 优先使用 $rules 中的消费券倍数');
|
||||
assert(c.includes("$rules['voucher_days']"), 'VoucherService 优先使用 $rules 中的消费券释放周期天数');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 4. 后台控制器 Goods.php 验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 4. 后台控制器 Goods.php 验证 ---');
|
||||
const goodsControllerPath = path.resolve(__dirname, '../application/admin/controller/shopro/commission/Goods.php');
|
||||
assert(fs.existsSync(goodsControllerPath), 'Goods.php 控制器存在');
|
||||
if (fs.existsSync(goodsControllerPath)) {
|
||||
const c = fs.readFileSync(goodsControllerPath, 'utf-8');
|
||||
assert(c.includes('PartnerConfig::defaultRules()'), 'detail() 返回系统全局 default_partner_rules');
|
||||
assert(c.includes('PartnerConfig::roleNames()'), 'detail() 返回定制化 role_names');
|
||||
assert(c.includes('Level::order'), 'detail() 返回创客等级 levels 列表');
|
||||
assert(c.includes("$params = $this->request->only(['status', 'self_rules', 'commission_order_status'"), 'edit() 严格获取指定参数');
|
||||
assert(c.includes("intval($params['status']) === 1 ? 1 : 0"), 'edit() 规范化 status 为 0/1');
|
||||
assert(c.includes("intval($params['commission_order_status']) === 0 ? 0 : 1"), 'edit() 规范化 commission_order_status 为 0/1');
|
||||
assert(c.includes("json_decode($params['commission_rules'], true)"), 'edit() 对 commission_rules 进行 JSON 防御校验');
|
||||
assert(c.includes('Db::transaction'), 'edit() 使用数据库事务保证批量或单条保存原子性');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 5. 后台视图与前端脚本验证
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 5. 后台视图与前端脚本验证 ---');
|
||||
const indexPath = path.resolve(__dirname, '../application/admin/view/shopro/commission/goods/index.html');
|
||||
assert(fs.existsSync(indexPath), 'index.html 存在');
|
||||
if (fs.existsSync(indexPath)) {
|
||||
const c = fs.readFileSync(indexPath, 'utf-8');
|
||||
assert(c.includes('label="分成规则"'), '列表包含“分成规则”列');
|
||||
assert(c.includes('默认规则'), '列表包含“默认规则” Tag 展现');
|
||||
assert(c.includes('独立规则'), '列表包含“独立规则” Tag 展现');
|
||||
assert(c.includes('不参与计奖'), '列表包含“不参与计奖” Tag 展现');
|
||||
assert(c.includes('设置分成'), '操作区包含“设置分成”按钮');
|
||||
}
|
||||
|
||||
const editPath = path.resolve(__dirname, '../application/admin/view/shopro/commission/goods/edit.html');
|
||||
assert(fs.existsSync(editPath), 'edit.html 存在');
|
||||
if (fs.existsSync(editPath)) {
|
||||
const c = fs.readFileSync(editPath, 'utf-8');
|
||||
assert(c.includes('v-model="state.commission_goods.status"'), '包含参与计奖开关状态');
|
||||
assert(c.includes('v-model="state.commission_goods.self_rules"'), '包含分成规则单选 (0默认/1独立/2批量)');
|
||||
assert(c.includes('推三返一循环返现'), '表单包含“推三返一循环返现”卡片');
|
||||
assert(c.includes('极差基数'), '表单包含“各级极差基数”卡片');
|
||||
assert(c.includes('创客下级反超卓越比例') || c.includes('协同奖'), '表单包含“协同与超越卓越奖”卡片');
|
||||
assert(c.includes('酒道馆运营奖基准') || c.includes('实体渠道与区域运营奖'), '表单包含“实体渠道与区域运营分成”卡片');
|
||||
assert(c.includes('消费补贴兑换券计划'), '表单包含“消费补贴兑换券计划”卡片');
|
||||
assert(c.includes('全平台日结均分奖池注入'), '表单包含“全平台日结均分奖池注入”卡片');
|
||||
assert(c.includes('@click="onFillDefaults'), '包含快速填入系统默认规则按钮');
|
||||
assert(c.includes('@click="onSyncToAllSkus"'), '包含同步至全部规格按钮');
|
||||
}
|
||||
|
||||
const goodsJsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/commission/goods.js');
|
||||
assert(fs.existsSync(goodsJsPath), 'goods.js 存在');
|
||||
if (fs.existsSync(goodsJsPath)) {
|
||||
const c = fs.readFileSync(goodsJsPath, 'utf-8');
|
||||
assert(c.includes("'commission_goods.self_rules'"), '包含分成规则筛选器');
|
||||
assert(c.includes('function makeFormFromDefaults'), '包含 makeFormFromDefaults 工具函数');
|
||||
assert(c.includes('function rulesToForm'), '包含 rulesToForm 工具函数');
|
||||
assert(c.includes('function formToRules'), '包含 formToRules 工具函数');
|
||||
assert(c.includes('state.skuMode === \'sku\''), '支持按规格分别设置与统一设置判断');
|
||||
assert(c.includes('state.skuRules[sku.id]'), '保存时正确提取各规格独立规则映射');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 6. 算法行为与业务逻辑模拟
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--- 6. 算法行为与业务逻辑模拟断言 ---');
|
||||
|
||||
const mockSystemDefaults = {
|
||||
push3_amounts: [398, 400, 600],
|
||||
maker_bonus: { 2: 20, 3: 40, 4: 60, 5: 80, 6: 100, 7: 120 },
|
||||
maker_peer_rate: 0.10,
|
||||
maker_overtaken_rate: 0.05,
|
||||
venue_split: 50,
|
||||
county_full: 100,
|
||||
venue_peer_rate: 0.10,
|
||||
county_peer_rate: 0.10,
|
||||
voucher_times: 1.5,
|
||||
voucher_days: 365,
|
||||
venue_share: 100,
|
||||
county_share: 50,
|
||||
is_custom: false,
|
||||
};
|
||||
|
||||
function mockGetEffectiveRules(goodsRecord, skuPriceId = 0) {
|
||||
const rules = JSON.parse(JSON.stringify(mockSystemDefaults));
|
||||
if (!goodsRecord || Number(goodsRecord.status) !== 1) {
|
||||
rules.status = 0;
|
||||
return rules;
|
||||
}
|
||||
rules.status = 1;
|
||||
rules.commission_order_status = Number(goodsRecord.commission_order_status ?? 1);
|
||||
const selfRules = Number(goodsRecord.self_rules ?? 0);
|
||||
rules.self_rules = selfRules;
|
||||
|
||||
if (selfRules !== 1 && selfRules !== 2) {
|
||||
return rules; // 跟随默认规则
|
||||
}
|
||||
|
||||
let custom = goodsRecord.commission_rules;
|
||||
if (typeof custom === 'string') {
|
||||
try {
|
||||
custom = JSON.parse(custom);
|
||||
} catch (e) {
|
||||
custom = {};
|
||||
}
|
||||
}
|
||||
if (!custom || Object.keys(custom).length === 0) {
|
||||
return rules;
|
||||
}
|
||||
|
||||
const skuKey = Number(skuPriceId);
|
||||
if (skuKey > 0 && custom[skuKey] && typeof custom[skuKey] === 'object') {
|
||||
custom = custom[skuKey];
|
||||
} else if (skuKey > 0 && custom['sku_' + skuKey] && typeof custom['sku_' + skuKey] === 'object') {
|
||||
custom = custom['sku_' + skuKey];
|
||||
}
|
||||
|
||||
if (Array.isArray(custom.push3_amounts) && custom.push3_amounts.length >= 3) {
|
||||
rules.push3_amounts = custom.push3_amounts.map(Number);
|
||||
}
|
||||
if (custom.maker_bonus && typeof custom.maker_bonus === 'object') {
|
||||
const mb = {};
|
||||
for (const [k, v] of Object.entries(custom.maker_bonus)) {
|
||||
mb[parseInt(k)] = Number(v);
|
||||
}
|
||||
rules.maker_bonus = mb;
|
||||
}
|
||||
if (custom.maker_peer_rate !== undefined) rules.maker_peer_rate = Number(custom.maker_peer_rate);
|
||||
if (custom.maker_overtaken_rate !== undefined) rules.maker_overtaken_rate = Number(custom.maker_overtaken_rate);
|
||||
if (custom.venue_split !== undefined) rules.venue_split = Number(custom.venue_split);
|
||||
if (custom.county_full !== undefined) rules.county_full = Number(custom.county_full);
|
||||
if (custom.voucher_times !== undefined) rules.voucher_times = Number(custom.voucher_times);
|
||||
if (custom.voucher_days !== undefined) rules.voucher_days = parseInt(custom.voucher_days);
|
||||
if (custom.venue_share !== undefined) rules.venue_share = Number(custom.venue_share);
|
||||
if (custom.county_share !== undefined) rules.county_share = Number(custom.county_share);
|
||||
|
||||
rules.is_custom = true;
|
||||
return rules;
|
||||
}
|
||||
|
||||
// 模拟场景 1: 商品未开启计奖 (status = 0)
|
||||
const uncommissionedGoods = { goods_id: 201, status: 0, self_rules: 1 };
|
||||
const res1 = mockGetEffectiveRules(uncommissionedGoods);
|
||||
assert(res1.status === 0, '模拟场景 1: status=0 商品被标记为不参与计奖');
|
||||
assert(res1.is_custom === false, '模拟场景 1: status=0 商品不激活独立规则');
|
||||
|
||||
// 模拟场景 2: 开启计奖但使用默认规则 (self_rules = 0)
|
||||
const defaultGoods = { goods_id: 202, status: 1, self_rules: 0 };
|
||||
const res2 = mockGetEffectiveRules(defaultGoods);
|
||||
assert(res2.status === 1 && res2.self_rules === 0, '模拟场景 2: 开启计奖且 self_rules=0');
|
||||
assert(res2.push3_amounts[0] === 398 && res2.push3_amounts[1] === 400 && res2.push3_amounts[2] === 600, '模拟场景 2: 采用系统默认推三返一金额 (398/400/600)');
|
||||
assert(res2.voucher_times === 1.5, '模拟场景 2: 采用系统默认兑换券倍数 (1.5)');
|
||||
assert(res2.is_custom === false, '模拟场景 2: is_custom 为 false');
|
||||
|
||||
// 模拟场景 3: 独立规则设置单品分成 (self_rules = 1, 单规格)
|
||||
const customGoods = {
|
||||
goods_id: 203,
|
||||
status: 1,
|
||||
self_rules: 1,
|
||||
commission_rules: JSON.stringify({
|
||||
push3_amounts: [500, 500, 800],
|
||||
maker_bonus: { 2: 50, 3: 100, 4: 150, 5: 200, 6: 250, 7: 300 },
|
||||
venue_split: 80,
|
||||
county_full: 150,
|
||||
voucher_times: 2.0,
|
||||
voucher_days: 180
|
||||
})
|
||||
};
|
||||
const res3 = mockGetEffectiveRules(customGoods);
|
||||
assert(res3.status === 1 && res3.is_custom === true, '模拟场景 3: 成功识别单品独立规则 (is_custom = true)');
|
||||
assert(res3.push3_amounts[0] === 500 && res3.push3_amounts[2] === 800, '模拟场景 3: 推三返一正确覆盖为独立金额 [500, 500, 800]');
|
||||
assert(res3.maker_bonus[2] === 50 && res3.maker_bonus[3] === 100, '模拟场景 3: 创客极差正确覆盖为定制梯度');
|
||||
assert(res3.venue_split === 80 && res3.county_full === 150, '模拟场景 3: 渠道运营奖覆盖为 80 / 150');
|
||||
assert(res3.voucher_times === 2.0 && res3.voucher_days === 180, '模拟场景 3: 兑换券计划覆盖为 2.0 倍及 180 天');
|
||||
assert(res3.maker_peer_rate === 0.10, '模拟场景 3: 未设置项 (maker_peer_rate) 自动平滑回退至全局默认值 (0.10)');
|
||||
|
||||
// 模拟场景 4: 多规格分别设置独立分成 (多 SKU)
|
||||
const multiSkuGoods = {
|
||||
goods_id: 204,
|
||||
status: 1,
|
||||
self_rules: 1,
|
||||
commission_rules: JSON.stringify({
|
||||
1001: { // 500ml 规格
|
||||
push3_amounts: [200, 200, 300],
|
||||
venue_split: 30,
|
||||
voucher_times: 1.2
|
||||
},
|
||||
1002: { // 1000ml 礼盒装规格
|
||||
push3_amounts: [600, 600, 1000],
|
||||
venue_split: 120,
|
||||
voucher_times: 2.5
|
||||
}
|
||||
})
|
||||
};
|
||||
const resSku1001 = mockGetEffectiveRules(multiSkuGoods, 1001);
|
||||
const resSku1002 = mockGetEffectiveRules(multiSkuGoods, 1002);
|
||||
assert(resSku1001.push3_amounts[0] === 200 && resSku1001.venue_split === 30 && resSku1001.voucher_times === 1.2, '模拟场景 4: 规格 1001 准确读取小规格独立规则 (推三返一:200, 酒道馆:30, 券倍数:1.2)');
|
||||
assert(resSku1002.push3_amounts[0] === 600 && resSku1002.venue_split === 120 && resSku1002.voucher_times === 2.5, '模拟场景 4: 规格 1002 准确读取大礼盒独立规则 (推三返一:600, 酒道馆:120, 券倍数:2.5)');
|
||||
|
||||
// 模拟场景 5: 订单级规则提取优先级
|
||||
function mockGetOrderEffectiveRules(order, goodsMap) {
|
||||
for (const item of (order.items || [])) {
|
||||
if (item.ext && item.ext.is_commission === false) continue;
|
||||
const g = goodsMap[item.goods_id];
|
||||
const r = mockGetEffectiveRules(g, item.goods_sku_price_id);
|
||||
if (r.is_custom) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return JSON.parse(JSON.stringify(mockSystemDefaults));
|
||||
}
|
||||
|
||||
const mixedOrder = {
|
||||
id: 9901,
|
||||
items: [
|
||||
{ goods_id: 201, goods_sku_price_id: 0, pay_fee: 300, ext: { is_commission: false } }, // 不计奖
|
||||
{ goods_id: 202, goods_sku_price_id: 0, pay_fee: 500, ext: { is_commission: true } }, // 默认规则
|
||||
{ goods_id: 204, goods_sku_price_id: 1002, pay_fee: 1000, ext: { is_commission: true } } // 规格独立规则
|
||||
]
|
||||
};
|
||||
const orderEffective = mockGetOrderEffectiveRules(mixedOrder, {
|
||||
201: uncommissionedGoods,
|
||||
202: defaultGoods,
|
||||
204: multiSkuGoods
|
||||
});
|
||||
assert(orderEffective.is_custom === true, '模拟场景 5: 混合订单成功命中并优先提取独立规则商品配置');
|
||||
assert(orderEffective.push3_amounts[0] === 600 && orderEffective.voucher_times === 2.5, '模拟场景 5: 订单实际结算采用独立规则 (push3:600, 券倍数:2.5)');
|
||||
|
||||
// 模拟场景 6: 奖项结算金额计算断言
|
||||
// (1) 推三返一:第 1 单、第 2 单、第 3 单按商品定制发放
|
||||
const cycleRound = [1, 2, 3];
|
||||
const push3Pay = cycleRound.map((round) => {
|
||||
const idx = (round - 1) % 3;
|
||||
return orderEffective.push3_amounts[idx];
|
||||
});
|
||||
assert(push3Pay[0] === 600 && push3Pay[1] === 600 && push3Pay[2] === 1000, '模拟场景 6.1: 推三返一按定制金额循环 (600 -> 600 -> 1000)');
|
||||
|
||||
// (2) 消费补贴券计算
|
||||
const qualifiedPayFee = 1000; // 假设计奖实付 1000 元
|
||||
const voucherAmount = qualifiedPayFee * orderEffective.voucher_times;
|
||||
assert(voucherAmount === 2500, `模拟场景 6.2: 消费券根据定制 2.5 倍计算实发 2500 券(默认1.5倍为1500)`);
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 测试汇总输出
|
||||
// ----------------------------------------------------
|
||||
console.log('\n--------------------------------------------------------');
|
||||
console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
|
||||
console.log('========================================================\n');
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -56,6 +56,15 @@ if (fs.existsSync(voucherServicePath)) {
|
||||
assert(content.includes('$remainVoucher'), 'refundConsume 严格限制退回上限不超过剩余未退额');
|
||||
}
|
||||
|
||||
// 2.2 PayOper.php 同步核销与防双花验证
|
||||
const payOperPath = path.resolve(__dirname, '../addons/shopro/service/pay/PayOper.php');
|
||||
assert(fs.existsSync(payOperPath), 'PayOper.php 文件存在');
|
||||
if (fs.existsSync(payOperPath)) {
|
||||
const content = fs.readFileSync(payOperPath, 'utf-8');
|
||||
assert(content.includes('consumeByOrder($order)'), 'PayOper.php checkAndPaid 包含兑换券即时同步核销调用');
|
||||
assert(content.includes('voucher_consumed'), 'PayOper.php 具备 voucher_consumed 幂等检查');
|
||||
}
|
||||
|
||||
// 3. confirm.vue 验证
|
||||
const confirmVuePath = path.resolve(__dirname, '../uniapp/pages/order/confirm.vue');
|
||||
assert(fs.existsSync(confirmVuePath), 'confirm.vue 文件存在');
|
||||
@@ -83,7 +92,7 @@ if (fs.existsSync(scoreShopVuePath)) {
|
||||
assert(content.includes('userVoucherAvailable'), '积分商城首页引入可用兑换券计算属性');
|
||||
assert(content.includes('当前积分'), '展示当前积分余额看板');
|
||||
assert(content.includes('可用兑换券'), '展示可用兑换券余额看板');
|
||||
assert(content.includes('💡 兑换商品时支持使用兑换券 1:1 抵扣现金部分'), '包含兑换券抵扣说明提示');
|
||||
assert(content.includes('全额抵扣现金'), '包含兑换券全额抵扣说明提示');
|
||||
assert(content.includes('/pages/commission/voucher'), '提供快捷跳转兑换券明细入口');
|
||||
}
|
||||
|
||||
@@ -93,7 +102,30 @@ assert(fs.existsSync(scoreDetailVuePath), 'score.vue 文件存在');
|
||||
if (fs.existsSync(scoreDetailVuePath)) {
|
||||
const content = fs.readFileSync(scoreDetailVuePath, 'utf-8');
|
||||
assert(content.includes('voucher-tip-tag'), '积分商品详情包含兑换券抵扣标签组件');
|
||||
assert(content.includes('支持兑换券抵扣'), '积分商品详情标明支持兑换券抵扣现金');
|
||||
assert(content.includes('支持兑换券全额抵扣'), '积分商品详情标明支持兑换券全额抵扣');
|
||||
}
|
||||
|
||||
// 6.1 后台页面及模型验证
|
||||
const scoreSkuPricePath = path.resolve(__dirname, '../application/admin/model/shopro/app/ScoreSkuPrice.php');
|
||||
assert(fs.existsSync(scoreSkuPricePath), 'ScoreSkuPrice.php 文件存在');
|
||||
if (fs.existsSync(scoreSkuPricePath)) {
|
||||
const content = fs.readFileSync(scoreSkuPricePath, 'utf-8');
|
||||
assert(content.includes('兑换券'), 'ScoreSkuPrice 具备兑换券输出属性');
|
||||
}
|
||||
|
||||
const addHtmlPath = path.resolve(__dirname, '../application/admin/view/shopro/app/score_shop/add.html');
|
||||
assert(fs.existsSync(addHtmlPath), 'add.html 文件存在');
|
||||
if (fs.existsSync(addHtmlPath)) {
|
||||
const content = fs.readFileSync(addHtmlPath, 'utf-8');
|
||||
assert(content.includes('所需兑换券(元)'), 'add.html 包含所需兑换券表头');
|
||||
assert(content.includes('所需积分'), 'add.html 包含所需积分表头');
|
||||
}
|
||||
|
||||
const indexHtmlPath = path.resolve(__dirname, '../application/admin/view/shopro/app/score_shop/index.html');
|
||||
assert(fs.existsSync(indexHtmlPath), 'index.html 文件存在');
|
||||
if (fs.existsSync(indexHtmlPath)) {
|
||||
const content = fs.readFileSync(indexHtmlPath, 'utf-8');
|
||||
assert(content.includes('兑换门槛 (兑换券/积分)'), 'index.html 包含兑换门槛表头');
|
||||
}
|
||||
|
||||
// 7. 结算抵扣算法模拟
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 团队作战中心全功能补齐与手机号脱敏专项验证测试
|
||||
* 覆盖范围:
|
||||
* 1. 服务端:Agent.php 手机号脱敏与收益业绩统计
|
||||
* 2. 服务端:Index.php 收益明细 buyer_id 穿透过滤与 buyer_info
|
||||
* 3. 移动端:team.vue 专属邀请码、团队收益看板、卡片脱敏、一键拨打/复制/查收益
|
||||
* 4. 移动端:rewards.vue 穿透筛选提示、参数捕获与 API 传参
|
||||
* 5. 移动端:index.vue 收益中心首页团队快捷入口看板
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
function assert(condition, testName, detail = '') {
|
||||
if (condition) {
|
||||
console.log(` \x1b[32m[PASS]\x1b[0m ${testName}`);
|
||||
passCount++;
|
||||
} else {
|
||||
console.error(` \x1b[31m[FAIL]\x1b[0m ${testName} - ${detail}`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('========================================================');
|
||||
console.log(' 团队作战中心全功能闭环与手机号脱敏专项验证测试');
|
||||
console.log('========================================================\n');
|
||||
|
||||
// 1. 服务端 Agent.php 校验
|
||||
const agentPhpPath = path.resolve(__dirname, '../addons/shopro/controller/commission/Agent.php');
|
||||
assert(fs.existsSync(agentPhpPath), 'Agent.php 控制器存在');
|
||||
if (fs.existsSync(agentPhpPath)) {
|
||||
const content = fs.readFileSync(agentPhpPath, 'utf-8');
|
||||
assert(content.includes("use app\\admin\\model\\shopro\\partner\\Reward as RewardModel;"), 'Agent.php 引入了 RewardModel');
|
||||
assert(content.includes('$user->mobile = $maskedMobile;'), 'Agent.php 覆写了 $user->mobile 为脱敏号码');
|
||||
assert(content.includes('$user->mobile_hide = $maskedMobile;'), 'Agent.php 提供了 $user->mobile_hide 字段');
|
||||
assert(content.includes('$user->mobile_raw = $rawMobile;'), 'Agent.php 保留了 $user->mobile_raw 供客户端外呼');
|
||||
assert(content.includes('$user->parentUser->mobile = $pMasked;'), 'Agent.php 对所属间推推荐人手机号执行了脱敏');
|
||||
assert(content.includes("'team_total_consume'"), 'getTeamHealthSummary 返回团队自购总业绩');
|
||||
assert(content.includes("'team_total_reward'"), 'getTeamHealthSummary 返回团队贡献总收益');
|
||||
assert(content.includes("'team_month_reward'"), 'getTeamHealthSummary 返回本月团队贡献收益');
|
||||
assert(content.includes("'team_order_count'"), 'getTeamHealthSummary 返回团队总订单数');
|
||||
}
|
||||
|
||||
// 2. 服务端 partner/Index.php 校验
|
||||
const partnerIndexPhpPath = path.resolve(__dirname, '../addons/shopro/controller/partner/Index.php');
|
||||
assert(fs.existsSync(partnerIndexPhpPath), 'partner/Index.php 控制器存在');
|
||||
if (fs.existsSync(partnerIndexPhpPath)) {
|
||||
const content = fs.readFileSync(partnerIndexPhpPath, 'utf-8');
|
||||
assert(content.includes("intval($this->request->param('buyer_id', 0))"), 'rewards 接口接收 buyer_id 参数');
|
||||
assert(content.includes("$query->where('buyer_id', $buyerId);"), 'rewards 接口支持按 buyer_id 过滤明细');
|
||||
assert(content.includes("'buyer_info' => $buyerInfo"), 'rewards 接口返回穿透成员信息 buyer_info');
|
||||
}
|
||||
|
||||
// 3. 移动端 team.vue 校验
|
||||
const teamVuePath = path.resolve(__dirname, '../uniapp/pages/commission/team.vue');
|
||||
assert(fs.existsSync(teamVuePath), 'team.vue 页面文件存在');
|
||||
if (fs.existsSync(teamVuePath)) {
|
||||
const content = fs.readFileSync(teamVuePath, 'utf-8');
|
||||
// 专属邀请码
|
||||
assert(content.includes('专属邀请码:'), 'team.vue 包含专属邀请码标签');
|
||||
assert(content.includes('HL{{ userInfo.id ||'), 'team.vue 包含 HL + 用户ID 的邀请码渲染');
|
||||
assert(content.includes('onCopyInviteCode'), 'team.vue 包含 onCopyInviteCode 复制方法');
|
||||
assert(content.includes('onCopyShareLink'), 'team.vue 包含 onCopyShareLink 复制链接方法');
|
||||
// 团队收益看板
|
||||
assert(content.includes('team-wealth-box'), 'team.vue 包含 team-wealth-box 团队收益看板');
|
||||
assert(content.includes('state.summary.team_total_reward'), 'team.vue 绑定团队贡献总收益');
|
||||
assert(content.includes('state.summary.team_total_consume'), 'team.vue 绑定团队自购总业绩');
|
||||
assert(content.includes('state.summary.team_month_reward'), 'team.vue 绑定本月贡献收益');
|
||||
assert(content.includes('state.summary.team_order_count'), 'team.vue 绑定团队订单数');
|
||||
// 手机号脱敏展示与操作
|
||||
assert(content.includes('item.mobile_hide'), 'team.vue 列表卡片展示脱敏手机号');
|
||||
assert(content.includes('state.currentMember.mobile_hide'), 'team.vue 抽屉详情展示脱敏手机号');
|
||||
assert(content.includes('btn-reward'), 'team.vue 列表卡片提供直达收益流水按钮');
|
||||
assert(content.includes('onJumpRewards(item.id, item.nickname)'), 'team.vue 卡片支持一键穿透查收益流水');
|
||||
assert(content.includes('onJumpRewards(state.currentMember.id, state.currentMember.nickname)'), 'team.vue 抽屉支持一键穿透查收益流水');
|
||||
}
|
||||
|
||||
// 4. 移动端 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('buyer-filter-bar'), 'rewards.vue 包含 buyer-filter-bar 穿透筛选提示条');
|
||||
assert(content.includes('onClearBuyerFilter'), 'rewards.vue 包含 onClearBuyerFilter 清除筛选方法');
|
||||
assert(content.includes('options?.buyer_id'), 'rewards.vue onLoad 中捕获 buyer_id 参数');
|
||||
assert(content.includes('params.buyer_id = state.buyerId;'), 'rewards.vue 请求 API 时传递 buyer_id');
|
||||
}
|
||||
|
||||
// 5. 移动端 index.vue 校验
|
||||
const indexVuePath = path.resolve(__dirname, '../uniapp/pages/commission/index.vue');
|
||||
assert(fs.existsSync(indexVuePath), 'index.vue 页面文件存在');
|
||||
if (fs.existsSync(indexVuePath)) {
|
||||
const content = fs.readFileSync(indexVuePath, 'utf-8');
|
||||
assert(content.includes('team-quick-card'), 'index.vue 包含 team-quick-card 快捷看板');
|
||||
assert(content.includes('团队作战中心'), 'index.vue 明确展示团队作战中心标题');
|
||||
assert(content.includes('/pages/commission/team'), 'index.vue 点击直达 /pages/commission/team');
|
||||
}
|
||||
|
||||
// 6. 算法测试:脱敏与邀请码逻辑
|
||||
const testMobile = "13912345678";
|
||||
const maskedMobile = testMobile.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
|
||||
assert(maskedMobile === "139****5678", `手机号脱敏算法验证正常: ${maskedMobile}`);
|
||||
|
||||
const testUserId = 2048;
|
||||
const inviteCode = `HL${testUserId}`;
|
||||
assert(inviteCode === "HL2048", `邀请码生成格式符合规范: ${inviteCode}`);
|
||||
|
||||
console.log('\n--------------------------------------------------------');
|
||||
console.log(`测试结果统计: 通过 ${passCount} 项, 失败 ${failCount} 项`);
|
||||
console.log('========================================================');
|
||||
|
||||
process.exit(failCount === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,237 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail = '') {
|
||||
if (condition) {
|
||||
console.log(` [PASS] ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.error(` [FAIL] ${testName} - ${detail}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("========================================================");
|
||||
console.log(" 微信小程序默认登录采用手机号一键快捷登录规格测试");
|
||||
console.log("========================================================\n");
|
||||
|
||||
// 1. 后端服务层: MiniProgram.php
|
||||
const miniProgramPhpPath = path.resolve(__dirname, '../addons/shopro/service/third/wechat/MiniProgram.php');
|
||||
assert(fs.existsSync(miniProgramPhpPath), 'MiniProgram.php 文件存在');
|
||||
const miniProgramPhpContent = fs.readFileSync(miniProgramPhpPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
miniProgramPhpContent.includes('wxa/business/getuserphonenumber') && miniProgramPhpContent.includes("['code']"),
|
||||
'MiniProgram.php 支持微信新版 code 获取手机号接口',
|
||||
'未包含 wxa/business/getuserphonenumber 逻辑'
|
||||
);
|
||||
|
||||
assert(
|
||||
miniProgramPhpContent.includes('encryptor->decryptData') && miniProgramPhpContent.includes('purePhoneNumber'),
|
||||
'MiniProgram.php 保留传统 iv + encryptedData 解密降级兼容',
|
||||
'未包含 decryptData 解密兼容'
|
||||
);
|
||||
|
||||
assert(
|
||||
miniProgramPhpContent.includes('$mobile = $this->getUserPhoneNumber()') && miniProgramPhpContent.includes("'mobile' => $mobile"),
|
||||
'MiniProgram.php 在 login() 方法中当传入手机号凭证时自动提取 mobile',
|
||||
'login() 中未提取手机号'
|
||||
);
|
||||
|
||||
// 2. 后端服务层: Wechat.php
|
||||
const wechatServicePath = path.resolve(__dirname, '../addons/shopro/service/third/wechat/Wechat.php');
|
||||
assert(fs.existsSync(wechatServicePath), 'Wechat.php (Service) 文件存在');
|
||||
const wechatServiceContent = fs.readFileSync(wechatServicePath, 'utf-8');
|
||||
|
||||
assert(
|
||||
wechatServiceContent.includes("UserModel::where('mobile', $extend['mobile'])->find()"),
|
||||
'Wechat.php 在 registerOrBindUser 中检查已有手机号并直接绑定关联',
|
||||
'未检测到已有手机号用户直接关联逻辑'
|
||||
);
|
||||
|
||||
assert(
|
||||
wechatServiceContent.includes('$user->mobile = $extend['),
|
||||
'Wechat.php 为已绑定微信但尚无手机号的用户自动补充手机号',
|
||||
'未检测到已绑定账号补充手机号逻辑'
|
||||
);
|
||||
|
||||
// 3. 后端控制器层: Wechat.php
|
||||
const wechatControllerPath = path.resolve(__dirname, '../addons/shopro/controller/third/Wechat.php');
|
||||
assert(fs.existsSync(wechatControllerPath), 'Wechat.php (Controller) 文件存在');
|
||||
const wechatControllerContent = fs.readFileSync(wechatControllerPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
wechatControllerContent.includes("'token' => $auth->isLogin() ? $auth->getToken() : ''"),
|
||||
'Wechat.php (Controller) 在 login 成功响应数据中返回 token',
|
||||
'响应未返回 token'
|
||||
);
|
||||
|
||||
// 4. 前端平台驱动层: miniProgram.js
|
||||
const miniProgramJsPath = path.resolve(__dirname, '../uniapp/sheep/platform/provider/wechat/miniProgram.js');
|
||||
assert(fs.existsSync(miniProgramJsPath), 'miniProgram.js 文件存在');
|
||||
const miniProgramJsContent = fs.readFileSync(miniProgramJsPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
miniProgramJsContent.includes('const mobileLogin = async (e) => {') && miniProgramJsContent.includes('third.wechat.login'),
|
||||
'miniProgram.js 包含封装完善的 mobileLogin 方法',
|
||||
'未包含 mobileLogin 方法'
|
||||
);
|
||||
|
||||
assert(
|
||||
miniProgramJsContent.includes('mobileLogin,') && miniProgramJsContent.includes('export default {'),
|
||||
'miniProgram.js 在 export default 中导出了 mobileLogin',
|
||||
'未导出 mobileLogin'
|
||||
);
|
||||
|
||||
// 5. 前端弹窗状态控制: useModal.js
|
||||
const useModalPath = path.resolve(__dirname, '../uniapp/sheep/hooks/useModal.js');
|
||||
assert(fs.existsSync(useModalPath), 'useModal.js 文件存在');
|
||||
const useModalContent = fs.readFileSync(useModalPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
useModalContent.includes('MP-WEIXIN') && useModalContent.includes("targetType = 'mpMobileLogin'"),
|
||||
'useModal.js 微信小程序环境下默认弹窗类型为 mpMobileLogin',
|
||||
'未设置微信小程序默认弹窗类型为 mpMobileLogin'
|
||||
);
|
||||
|
||||
// 6. 新增组件: mp-mobile-login.vue
|
||||
const mpMobileLoginPath = path.resolve(__dirname, '../uniapp/sheep/components/s-auth-modal/components/mp-mobile-login.vue');
|
||||
assert(fs.existsSync(mpMobileLoginPath), 'mp-mobile-login.vue 文件存在');
|
||||
const mpMobileLoginContent = fs.readFileSync(mpMobileLoginPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
mpMobileLoginContent.includes('open-type="getPhoneNumber"') && mpMobileLoginContent.includes('@getphonenumber="onGetPhoneNumber"'),
|
||||
'mp-mobile-login.vue 包含 open-type="getPhoneNumber" 授权按钮',
|
||||
'缺少 getPhoneNumber 授权按钮'
|
||||
);
|
||||
|
||||
assert(
|
||||
mpMobileLoginContent.includes("showAuthModal('smsLogin')") && mpMobileLoginContent.includes("showAuthModal('accountLogin')"),
|
||||
'mp-mobile-login.vue 提供切换短信登录和账号登录的快速入口',
|
||||
'缺少其他登录方式入口'
|
||||
);
|
||||
|
||||
assert(
|
||||
mpMobileLoginContent.includes('agreeStatus') && mpMobileLoginContent.includes('mobileLogin'),
|
||||
'mp-mobile-login.vue 校验用户协议并调用 mobileLogin 登录',
|
||||
'缺少协议校验或登录调用'
|
||||
);
|
||||
|
||||
// 7. 弹窗容器: s-auth-modal.vue
|
||||
const sAuthModalPath = path.resolve(__dirname, '../uniapp/sheep/components/s-auth-modal/s-auth-modal.vue');
|
||||
assert(fs.existsSync(sAuthModalPath), 's-auth-modal.vue 文件存在');
|
||||
const sAuthModalContent = fs.readFileSync(sAuthModalPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
sAuthModalContent.includes("import mpMobileLogin from './components/mp-mobile-login.vue'") && sAuthModalContent.includes("authType === 'mpMobileLogin'"),
|
||||
's-auth-modal.vue 成功挂载 mp-mobile-login 组件',
|
||||
'未挂载 mp-mobile-login'
|
||||
);
|
||||
|
||||
assert(
|
||||
sAuthModalContent.includes("'mpMobileLogin'") && sAuthModalContent.includes('agreement-box'),
|
||||
's-auth-modal.vue 协议栏支持 mpMobileLogin 显示与勾选状态关联',
|
||||
'协议栏未支持 mpMobileLogin'
|
||||
);
|
||||
|
||||
// 8. 个人中心卡片组件: s-user-card.vue
|
||||
const sUserCardPath = path.resolve(__dirname, '../uniapp/sheep/components/s-user-card/s-user-card.vue');
|
||||
assert(fs.existsSync(sUserCardPath), 's-user-card.vue 文件存在');
|
||||
const sUserCardContent = fs.readFileSync(sUserCardPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
sUserCardContent.includes('#ifdef MP-WEIXIN') && sUserCardContent.includes('open-type="getPhoneNumber"') && sUserCardContent.includes('@getphonenumber="onQuickLogin"'),
|
||||
's-user-card.vue 在微信小程序端未登录时渲染 open-type="getPhoneNumber" 快捷登录按钮',
|
||||
'未检测到微信小程序快捷登录按钮'
|
||||
);
|
||||
|
||||
assert(
|
||||
sUserCardContent.includes('async function onQuickLogin(e)') && sUserCardContent.includes("mobileLogin(e.detail)"),
|
||||
's-user-card.vue 包含 onQuickLogin 处理函数调用 mobileLogin 完成快捷登录',
|
||||
'未检测到 onQuickLogin 处理逻辑'
|
||||
);
|
||||
|
||||
assert(
|
||||
sUserCardContent.includes('hasBoundMobile') && sUserCardContent.includes('userInfo.value?.mobile || userInfo.value?.verification?.mobile'),
|
||||
's-user-card.vue 使用 hasBoundMobile 判定:只要存在手机号则不展示绑定提示条',
|
||||
'缺少 hasBoundMobile 判定逻辑'
|
||||
);
|
||||
|
||||
assert(
|
||||
sUserCardContent.includes('onBindPhoneNumber') && sUserCardContent.includes('bindUserPhoneNumber(e.detail)'),
|
||||
's-user-card.vue 在微信小程序端去绑定支持 open-type="getPhoneNumber" 一键绑定',
|
||||
'缺少一键去绑定逻辑'
|
||||
);
|
||||
|
||||
assert(
|
||||
sUserCardContent.includes('#ifndef MP-WEIXIN') && sUserCardContent.includes('showAuthModal()'),
|
||||
's-user-card.vue 保留非微信小程序端正常点击弹窗唤醒逻辑',
|
||||
'未保留非微信小程序端唤醒逻辑'
|
||||
);
|
||||
|
||||
// 9. 用户模型与用户仓库:User.php & user.js
|
||||
const userModelPath = path.resolve(__dirname, '../application/admin/model/shopro/user/User.php');
|
||||
assert(fs.existsSync(userModelPath), 'User.php 模型存在');
|
||||
const userModelContent = fs.readFileSync(userModelPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
userModelContent.includes("if (!empty($data['mobile']))") && userModelContent.includes("$value['mobile'] = 1"),
|
||||
'User.php 模型 getVerificationAttr 当会员已有手机号时自动认定 mobile = 1',
|
||||
'缺少 mobile 存在时自动认定 mobile = 1 逻辑'
|
||||
);
|
||||
|
||||
assert(
|
||||
userModelContent.includes('function setVerificationAttr'),
|
||||
'User.php 模型定义了 setVerificationAttr 支持对象/数组自动 json 序列化',
|
||||
'缺少 setVerificationAttr 方法'
|
||||
);
|
||||
|
||||
const userStorePath = path.resolve(__dirname, '../uniapp/sheep/store/user.js');
|
||||
assert(fs.existsSync(userStorePath), 'user.js Store 存在');
|
||||
const userStoreContent = fs.readFileSync(userStorePath, 'utf-8');
|
||||
|
||||
assert(
|
||||
userStoreContent.includes('if (merged.mobile)') && userStoreContent.includes('mobile: 1'),
|
||||
'user.js getInfo() 当存在 mobile 时自动认定 verification.mobile = 1',
|
||||
'缺少 getInfo() 补充 verification.mobile 逻辑'
|
||||
);
|
||||
|
||||
// 10. 售后申请页编译修复: apply.vue
|
||||
const applyVuePath = path.resolve(__dirname, '../uniapp/pages/order/aftersale/apply.vue');
|
||||
assert(fs.existsSync(applyVuePath), 'apply.vue 存在');
|
||||
const applyVueContent = fs.readFileSync(applyVuePath, 'utf-8');
|
||||
|
||||
assert(
|
||||
applyVueContent.includes(':modelValue="formData"') && !applyVueContent.includes('v-model="formData"'),
|
||||
'apply.vue 使用 :modelValue="formData" 消除 const 重赋值编译错误',
|
||||
'仍包含 v-model="formData"'
|
||||
);
|
||||
|
||||
assert(
|
||||
applyVueContent.includes('let formData = reactive({'),
|
||||
'apply.vue 中 formData 使用 let 声明提供双重防护',
|
||||
'formData 不是 let 声明'
|
||||
);
|
||||
|
||||
// 11. bindUserPhoneNumber 弹性支持与自动更新
|
||||
assert(
|
||||
miniProgramJsContent.includes('bindUserPhoneNumber') && miniProgramJsContent.includes("sheep.$store('user').getInfo()"),
|
||||
'miniProgram.js bindUserPhoneNumber 成功后自动调用 getInfo() 刷新用户信息',
|
||||
'bindUserPhoneNumber 未自动刷新用户信息'
|
||||
);
|
||||
|
||||
// 12. 个人中心绑定横条受 showBindMobile 精确控制(微信小程序环境不冗余展示)
|
||||
assert(
|
||||
sUserCardContent.includes('v-if="showBindMobile"') && sUserCardContent.includes('#ifdef MP-WEIXIN'),
|
||||
's-user-card.vue 使用 showBindMobile 精确控制,微信小程序端不显示冗余绑定提示条',
|
||||
'未配置 showBindMobile 精确控制'
|
||||
);
|
||||
|
||||
console.log("\n--------------------------------------------------------");
|
||||
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
|
||||
console.log("========================================================");
|
||||
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
在新工单中引用
屏蔽一个用户