/** * 计奖商品独立分成与多规格规则专项验证测试 * 覆盖范围: * 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); }