diff --git a/addons/shopro/service/partner/PartnerConfig.php b/addons/shopro/service/partner/PartnerConfig.php index a2b678c..ec5a7d6 100644 --- a/addons/shopro/service/partner/PartnerConfig.php +++ b/addons/shopro/service/partner/PartnerConfig.php @@ -290,6 +290,7 @@ class PartnerConfig 'not_maker' => '不是' . $maker . '身份', 'not_venue' => '不是' . $venue . '身份', 'not_county' => '不是' . $county . '身份', + 'venue_above_county' => '县以上酒道馆无收益', 'cap_not_higher' => '等级未超过基数水位', 'already_settled' => '幂等跳过', 'partner_goods_off' => '商品未参与', @@ -302,6 +303,7 @@ class PartnerConfig 'not_maker' => '', 'not_venue' => '', 'not_county' => '', + 'venue_above_county' => '县以上的酒道馆不发放管理奖', 'cap_not_higher' => '同阶不计进阶差额,享受平级奖励', 'already_settled' => '', 'partner_goods_off' => '', diff --git a/addons/shopro/service/partner/TeamSplitReward.php b/addons/shopro/service/partner/TeamSplitReward.php index fe3c7c4..49f5cf3 100644 --- a/addons/shopro/service/partner/TeamSplitReward.php +++ b/addons/shopro/service/partner/TeamSplitReward.php @@ -3,7 +3,11 @@ namespace addons\shopro\service\partner; /** - * 渠道管理奖(馆/县拆分):无馆有县=100;有馆有县=50+50;有馆无县=50 + * 渠道管理奖(馆/县拆分),链下标越小越靠近买家: + * - 无馆有县:县 100 + * - 有馆无县:馆 50 + * - 最近的馆在最近的县下方,或馆与县是同一人:馆 50 + 县 50 + * - 最近的县在最近的馆下方:县 100,县以上的馆不发 */ class TeamSplitReward { @@ -18,14 +22,18 @@ class TeamSplitReward $result = ['venue' => null, 'county' => null]; $venue = $this->nearest($chain, 'is_venue', $active); - $county = $this->nearestCounty($chain, $venue, $active); + $county = $this->nearest($chain, 'is_county', $active); if (!$venue && !$county) { PartnerRewardService::writeSkip($order, $buyerId, 0, 'team_venue', 'system', 'not_venue', '链上无酒道馆且无区县'); return $result; } - if ($venue) { + // 县比馆更靠近买家时,馆在县之上,不发酒道馆管理奖 + $venueAboveCounty = $venue && $county && intval($venue['index']) > intval($county['index']); + $venueEarns = $venue && !$venueAboveCounty; + + if ($venueEarns) { if ($venue['inactive']) { PartnerRewardService::writeSkip($order, $buyerId, $venue['user_id'], 'team_venue', 'venue', 'inactive_30d', '酒道馆 30 日无自购'); } else { @@ -34,22 +42,30 @@ class TeamSplitReward ]); $result['venue'] = ['user_id' => $venue['user_id'], 'amount' => $venueSplit, 'index' => $venue['index']]; } + } elseif ($venueAboveCounty) { + PartnerRewardService::writeSkip($order, $buyerId, $venue['user_id'], 'team_venue', 'venue', 'venue_above_county', '县以上酒道馆无收益'); } if ($county) { - $amount = $venue ? $venueSplit : $countyFull; + $amount = $venueEarns ? $venueSplit : $countyFull; if ($county['inactive']) { PartnerRewardService::writeSkip($order, $buyerId, $county['user_id'], 'team_county', 'county', 'inactive_30d', '区县 30 日无自购'); } else { - $formulaText = $venue - ? (($venue['user_id'] == $county['user_id']) ? "区县管理奖 {$amount}(馆兼县)" : "区县管理奖 {$amount}(馆上)") - : "区县管理奖 {$amount}(直达)"; + if ($venue && intval($venue['user_id']) === intval($county['user_id'])) { + $formulaText = "区县管理奖 {$amount}(馆兼县)"; + } elseif ($venueEarns) { + $formulaText = "区县管理奖 {$amount}(馆上)"; + } elseif ($venueAboveCounty) { + $formulaText = "区县管理奖 {$amount}(县在馆下)"; + } else { + $formulaText = "区县管理奖 {$amount}(直达)"; + } PartnerRewardService::account($order, $buyerId, $county['user_id'], 'team_county', 'county', $amount, $amount, [ 'formula' => $formulaText, ]); $result['county'] = ['user_id' => $county['user_id'], 'amount' => $amount, 'index' => $county['index']]; } - } elseif ($venue && !$county) { + } elseif ($venue) { PartnerRewardService::writeSkip($order, $buyerId, 0, 'team_county', 'system', 'not_county', '有馆无区县'); } @@ -71,26 +87,4 @@ class TeamSplitReward } return null; } - - protected function nearestCounty($chain, $venue, ActiveService $active) - { - // 若酒道馆节点本身兼任区县代理,允许从该酒道馆节点开始匹配(兼具双重身份场景);若未兼任,循环会自动检查其上一级 - $start = $venue ? $venue['index'] : 0; - for ($i = $start; $i < count($chain); $i++) { - if (!intval($chain[$i]['is_county'] ?? 0)) { - continue; - } - return [ - 'user_id' => $chain[$i]['user_id'], - 'index' => $i, - 'inactive' => !$active->isActive($chain[$i]['agent'] ?? $chain[$i]), - 'node' => $chain[$i], - ]; - } - // 无馆时从链头找区县(含自购区县) - if (!$venue) { - return $this->nearest($chain, 'is_county', $active); - } - return null; - } } diff --git a/tests/partner/verify_team_county_dual_identity_spec.js b/tests/partner/verify_team_county_dual_identity_spec.js index bddbd73..88dcb1e 100644 --- a/tests/partner/verify_team_county_dual_identity_spec.js +++ b/tests/partner/verify_team_county_dual_identity_spec.js @@ -27,7 +27,9 @@ const teamSplitFile = path.resolve(__dirname, '../../addons/shopro/service/partn assert(fs.existsSync(teamSplitFile), 'TeamSplitReward.php 文件存在'); const code = fs.readFileSync(teamSplitFile, 'utf8'); -assert(code.includes('$start = $venue ? $venue[\'index\'] : 0;'), 'nearestCounty 起始查找索引支持从当前酒道馆节点匹配兼职区县代理'); +assert(code.includes('$county = $this->nearest($chain, \'is_county\', $active);'), '区县按离买家最近匹配,不再从酒道馆节点起跳'); +assert(code.includes('venue_above_county'), '县在馆下时,县以上酒道馆记为无收益'); +assert(code.includes('县在馆下'), '县在馆下时区县管理奖走全额公式'); assert(code.includes('馆兼县'), '支持「馆兼县」专属流水明细公式标识'); // 2. 模拟 TeamSplitReward 核心算法逻辑 @@ -44,32 +46,44 @@ class MockTeamSplitReward { const result = { venue: null, county: null, skips: [] }; const venue = this.nearest(chain, 'is_venue', active); - const county = this.nearestCounty(chain, venue, active); + const county = this.nearest(chain, 'is_county', active); if (!venue && !county) { result.skips.push({ type: 'team_venue', reason: 'not_venue', formula: '链上无酒道馆且无区县' }); return result; } - if (venue) { + const venueAboveCounty = venue && county && venue.index > county.index; + const venueEarns = venue && !venueAboveCounty; + + if (venueEarns) { 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 }; } + } else if (venueAboveCounty) { + result.skips.push({ type: 'team_venue', user_id: venue.user_id, reason: 'venue_above_county', formula: '县以上酒道馆无收益' }); } if (county) { - const amount = venue ? venueSplit : countyFull; + const amount = venueEarns ? 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}(直达)`; + let formulaText; + if (venue && venue.user_id === county.user_id) { + formulaText = `区县管理奖 ${amount}(馆兼县)`; + } else if (venueEarns) { + formulaText = `区县管理奖 ${amount}(馆上)`; + } else if (venueAboveCounty) { + formulaText = `区县管理奖 ${amount}(县在馆下)`; + } else { + formulaText = `区县管理奖 ${amount}(直达)`; + } result.county = { user_id: county.user_id, amount, index: county.index, formula: formulaText }; } - } else if (venue && !county) { + } else if (venue) { result.skips.push({ type: 'team_county', user_id: 0, reason: 'not_county', formula: '有馆无区县' }); } @@ -89,22 +103,6 @@ class MockTeamSplitReward { 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(); @@ -184,6 +182,33 @@ const active = new MockActiveService(); assert(res.venue.amount + res.county.amount === 100, '场景6: 自购节点拆分总额 100 元'); } +// 场景 7:县在馆下面(买家 -> 县13 -> 馆12),馆不拿,县拿 100 +{ + const chain = [ + { user_id: 162, is_venue: 0, is_county: 0 }, + { user_id: 13, is_venue: 0, is_county: 1, is_active: true }, + { user_id: 12, is_venue: 1, is_county: 1, is_active: true } + ]; + const res = runner.run({ id: 10000046 }, 162, chain, active); + assert(res.venue === null, '场景7: 县以上的酒道馆不发放管理奖'); + assert(res.county !== null && res.county.user_id === 13 && res.county.amount === 100, '场景7: 离买家最近的县获得 100 元'); + assert(res.county.formula.includes('县在馆下'), '场景7: 公式标识为「区县管理奖 100(县在馆下)」'); + assert(res.skips.some(s => s.reason === 'venue_above_county' && s.user_id === 12), '场景7: 记录馆12无收益'); +} + +// 场景 8:馆在县下面(买家 -> 馆20 -> 县30),馆和县都拿 50 +{ + 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: 18 }, 25, chain, active); + assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景8: 县下方的酒道馆获得 50 元'); + assert(res.county !== null && res.county.user_id === 30 && res.county.amount === 50, '场景8: 馆之上的县获得 50 元'); + assert(res.skips.length === 0, '场景8: 馆在县下时双方都入账'); +} + console.log('\n--------------------------------------------------------'); console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`); console.log('========================================================\n');