From bee5288e88899375b8a228844c6bdebe2a77fb54 Mon Sep 17 00:00:00 2001 From: chen Date: Sat, 19 Sep 2026 16:37:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(=E6=94=B6=E7=9B=8A):=20?= =?UTF-8?q?=E7=BA=A0=E6=AD=A3=E7=B4=AF=E8=AE=A1=E6=94=B6=E7=9B=8A=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8F=A3=E5=BE=84=E4=B8=BA=E7=9C=9F=E5=AE=9E=E5=88=86?= =?UTF-8?q?=E6=88=90=E6=94=B6=E7=9B=8A=EF=BC=8C=E6=8E=92=E9=99=A4dmepa?= =?UTF-8?q?=E4=B8=8E=E5=85=91=E6=8D=A2=E5=88=B8=E9=87=8A=E6=94=BE=E5=B9=B2?= =?UTF-8?q?=E6=89=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/shopro/controller/commission/Agent.php | 3 + addons/shopro/controller/user/User.php | 2 + .../service/partner/IdentityService.php | 4 +- .../shopro/service/partner/PartnerConfig.php | 8 ++ .../admin/controller/shopro/Dashboard.php | 7 +- .../controller/shopro/commission/Agent.php | 3 + .../admin/controller/shopro/user/User.php | 3 + ...fy_total_income_is_cash_commission_spec.js | 115 ++++++++++++++++++ 8 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 tests/partner/verify_total_income_is_cash_commission_spec.js diff --git a/addons/shopro/controller/commission/Agent.php b/addons/shopro/controller/commission/Agent.php index dcd1cb4..7a32cd3 100644 --- a/addons/shopro/controller/commission/Agent.php +++ b/addons/shopro/controller/commission/Agent.php @@ -402,12 +402,15 @@ class Agent extends Commission // 2. 团队成员为目标用户贡献的累计收益与本月收益 $monthStart = strtotime(date('Y-m-01 00:00:00')); + $cashTypes = \addons\shopro\service\partner\PartnerConfig::cashTypes(); $teamTotalReward = RewardModel::where('user_id', $targetUserId) ->whereIn('buyer_id', $teamUserIds) + ->whereIn('reward_type', $cashTypes) ->where('status', 1) ->sum('amount'); $teamMonthReward = RewardModel::where('user_id', $targetUserId) ->whereIn('buyer_id', $teamUserIds) + ->whereIn('reward_type', $cashTypes) ->where('status', 1) ->where('createtime', '>=', $monthStart) ->sum('amount'); diff --git a/addons/shopro/controller/user/User.php b/addons/shopro/controller/user/User.php index e288204..caaecee 100644 --- a/addons/shopro/controller/user/User.php +++ b/addons/shopro/controller/user/User.php @@ -85,7 +85,9 @@ class User extends Common $agent = \addons\shopro\service\partner\IdentityService::findAgent($user->id); $partner = \addons\shopro\service\partner\IdentityService::attachProfile($agent, null, $user); if (empty($partner['total_income']) || $partner['total_income'] === '0.00') { + $cashTypes = \addons\shopro\service\partner\PartnerConfig::cashTypes(); $userRewardCash = \app\admin\model\shopro\partner\Reward::where('user_id', $user->id) + ->whereIn('reward_type', $cashTypes) ->where('status', 1) ->where('amount', '>', 0) ->sum('amount'); diff --git a/addons/shopro/service/partner/IdentityService.php b/addons/shopro/service/partner/IdentityService.php index 49a6ae2..be52701 100644 --- a/addons/shopro/service/partner/IdentityService.php +++ b/addons/shopro/service/partner/IdentityService.php @@ -135,7 +135,7 @@ class IdentityService public static function formatMadow($value) { $floatVal = (float) $value; - // 当存在小于 1 豆且大于 0 的微量dmepa时,自适应保留有效小数(最多4位),避免直接抹零显示为 0 豆 + // 当存在小于 1 枚且大于 0 的微量dmepa时,自适应保留有效小数(最多4位),避免直接抹零显示为 0 枚 if ($floatVal > 0 && $floatVal < 1) { return rtrim(rtrim(number_format($floatVal, 4, '.', ''), '0'), '.'); } @@ -205,7 +205,9 @@ class IdentityService $data = is_object($agent) ? $agent->toArray() : $agent; $userId = intval($data['user_id'] ?? 0); $agentTotalIncome = floatval($data['total_income'] ?? 0); + $cashTypes = PartnerConfig::cashTypes(); $rewardCash = $userId > 0 ? \app\admin\model\shopro\partner\Reward::where('user_id', $userId) + ->whereIn('reward_type', $cashTypes) ->where('status', 1) ->where('amount', '>', 0) ->sum('amount') : 0; diff --git a/addons/shopro/service/partner/PartnerConfig.php b/addons/shopro/service/partner/PartnerConfig.php index 2daf5ff..db75b95 100644 --- a/addons/shopro/service/partner/PartnerConfig.php +++ b/addons/shopro/service/partner/PartnerConfig.php @@ -305,6 +305,14 @@ class PartnerConfig ]; } + /** + * 真正的分成现金奖励类型(排除 dmepa 和 兑换券) + */ + public static function cashTypes() + { + return array_merge(self::instantTypes(), ['venue_share', 'county_share']); + } + public static function defaultRules() { return [ diff --git a/application/admin/controller/shopro/Dashboard.php b/application/admin/controller/shopro/Dashboard.php index 6b2384b..d185952 100644 --- a/application/admin/controller/shopro/Dashboard.php +++ b/application/admin/controller/shopro/Dashboard.php @@ -109,9 +109,10 @@ class Dashboard extends Common $todayNetGmv = max(0, round($todayGmv - $todayRefundAmount, 2)); $refundRate = $totalGmv > 0 ? round(($totalRefundAmount / $totalGmv) * 100, 2) : 0.00; - // 现金分成总支出 (status = 1 真实入账) - $totalPayout = (float) Reward::where('status', 1)->sum('amount'); - $todayPayout = (float) Reward::where('status', 1)->whereTime('createtime', 'today')->sum('amount'); + // 现金分成总支出 (status = 1 真实入账,排除 dmepa 及兑换券) + $cashTypes = \addons\shopro\service\partner\PartnerConfig::cashTypes(); + $totalPayout = (float) Reward::where('status', 1)->whereIn('reward_type', $cashTypes)->sum('amount'); + $todayPayout = (float) Reward::where('status', 1)->whereIn('reward_type', $cashTypes)->whereTime('createtime', 'today')->sum('amount'); // 平台有效留存 = 有效净 GMV (扣除退款后) - 真实分成总支出 $netRetention = round($netGmv - $totalPayout, 2); diff --git a/application/admin/controller/shopro/commission/Agent.php b/application/admin/controller/shopro/commission/Agent.php index ca412a7..da70a25 100644 --- a/application/admin/controller/shopro/commission/Agent.php +++ b/application/admin/controller/shopro/commission/Agent.php @@ -78,7 +78,9 @@ class Agent extends Common : ('已暂停' . ($ago !== null ? '(距今 ' . $ago . ' 天)' : '')); } + $cashTypes = \addons\shopro\service\partner\PartnerConfig::cashTypes(); $totalCash = \app\admin\model\shopro\partner\Reward::where('user_id', $id) + ->whereIn('reward_type', $cashTypes) ->where('status', 1) ->where('amount', '>', 0) ->sum('amount'); @@ -88,6 +90,7 @@ class Agent extends Common ->where('reward_type', 'push3') ->sum('amount'); $partnerExclusiveCash = \app\admin\model\shopro\partner\Reward::where('user_id', $id) + ->whereIn('reward_type', $cashTypes) ->where('status', 1) ->where('amount', '>', 0) ->where('reward_type', '<>', 'push3') diff --git a/application/admin/controller/shopro/user/User.php b/application/admin/controller/shopro/user/User.php index 30578d7..c89a990 100644 --- a/application/admin/controller/shopro/user/User.php +++ b/application/admin/controller/shopro/user/User.php @@ -144,7 +144,9 @@ class User extends Common // 累计现金收益统计(与 IdentityService 严格对齐,兼顾历史分销与收益中心) $agentTotalIncome = floatval($agent->total_income ?? 0); + $cashTypes = \addons\shopro\service\partner\PartnerConfig::cashTypes(); $totalCash = \app\admin\model\shopro\partner\Reward::where('user_id', $id) + ->whereIn('reward_type', $cashTypes) ->where('status', 1) ->where('amount', '>', 0) ->sum('amount'); @@ -156,6 +158,7 @@ class User extends Common ->where('reward_type', 'push3') ->sum('amount'); $partnerExclusiveCash = \app\admin\model\shopro\partner\Reward::where('user_id', $id) + ->whereIn('reward_type', $cashTypes) ->where('status', 1) ->where('amount', '>', 0) ->where('reward_type', '<>', 'push3') diff --git a/tests/partner/verify_total_income_is_cash_commission_spec.js b/tests/partner/verify_total_income_is_cash_commission_spec.js new file mode 100644 index 0000000..fd9176a --- /dev/null +++ b/tests/partner/verify_total_income_is_cash_commission_spec.js @@ -0,0 +1,115 @@ +const { execSync } = require('child_process'); + +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(" 累计收益口径修正测试:严格限定为真实分成收益,排除dmepa与兑换券"); +console.log("========================================================\n"); + +// 1. 本地代码与定义合规检查 +console.log("一、代码与规则定义检查:"); +const fs = require('fs'); +const path = require('path'); + +const partnerConfigPath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerConfig.php'); +const configContent = fs.readFileSync(partnerConfigPath, 'utf-8'); +assert(configContent.includes('public static function cashTypes()'), 'PartnerConfig 包含统一现金分成类型定义 cashTypes()'); +assert(configContent.includes('array_merge(self::instantTypes(), [\'venue_share\', \'county_share\'])'), 'cashTypes 包含即时奖与日结分红共10种现金分成类型'); + +const identityServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/IdentityService.php'); +const identityContent = fs.readFileSync(identityServicePath, 'utf-8'); +assert(identityContent.includes('whereIn(\'reward_type\', $cashTypes)'), 'IdentityService::attachAgent 严格通过 cashTypes 过滤现金分成'); + +const userControllerPath = path.resolve(__dirname, '../../addons/shopro/controller/user/User.php'); +const userControllerContent = fs.readFileSync(userControllerPath, 'utf-8'); +assert(userControllerContent.includes('whereIn(\'reward_type\', $cashTypes)'), 'User.php profile 接口严格通过 cashTypes 过滤现金分成'); + +// 2. 线上接口模拟测试(使用真实用户数据进行场景验证) +console.log("\n二、线上用户接口场景模拟测试:"); + +try { + const remoteScript = `#!/bin/bash +php -r ' +define("APP_PATH", "/www/wwwroot/henglian.gynhc.com/application/"); +require "/www/wwwroot/henglian.gynhc.com/thinkphp/base.php"; +\\think\\App::initCommon(); +\$auth = \\app\\common\\library\\Auth::instance(); +\$auth->direct(10); +file_put_contents("/tmp/user10_token.txt", \$auth->getToken()); +\$auth->direct(20); +file_put_contents("/tmp/user20_token.txt", \$auth->getToken()); +' +TOKEN10=\$(cat /tmp/user10_token.txt) +TOKEN20=\$(cat /tmp/user20_token.txt) + +curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H "token: \$TOKEN10" -H "platform: WechatMiniProgram" "https://henglian.gynhc.com/addons/shopro/commission.agent" > /tmp/test_user10_agent.json +curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H "token: \$TOKEN10" -H "platform: WechatMiniProgram" "https://henglian.gynhc.com/addons/shopro/user.user/profile" > /tmp/test_user10_profile.json +curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H "token: \$TOKEN10" -H "platform: WechatMiniProgram" "https://henglian.gynhc.com/addons/shopro/partner.index/index" > /tmp/test_user10_index.json +curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H "token: \$TOKEN20" -H "platform: WechatMiniProgram" "https://henglian.gynhc.com/addons/shopro/commission.agent" > /tmp/test_user20_agent.json +curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H "token: \$TOKEN20" -H "platform: WechatMiniProgram" "https://henglian.gynhc.com/addons/shopro/partner.index/rewards" > /tmp/test_user20_rewards.json +curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H "platform: WechatMiniProgram" "https://henglian.gynhc.com/addons/shopro/commission.agent" > /tmp/test_unauth.json +`; + + fs.writeFileSync('/tmp/test_total_income_runner.sh', remoteScript); + execSync(`sshpass -p 'Bjsslxz1' scp -o StrictHostKeyChecking=no /tmp/test_total_income_runner.sh root@118.184.172.94:/tmp/runner.sh`); + execSync(`sshpass -p 'Bjsslxz1' ssh -o StrictHostKeyChecking=no root@118.184.172.94 "bash /tmp/runner.sh"`); + + const fetchCmd = `sshpass -p 'Bjsslxz1' ssh -o StrictHostKeyChecking=no root@118.184.172.94 "cat /tmp/test_user10_agent.json && echo '===SEP===' && cat /tmp/test_user10_profile.json && echo '===SEP===' && cat /tmp/test_user10_index.json && echo '===SEP===' && cat /tmp/test_user20_agent.json && echo '===SEP===' && cat /tmp/test_user20_rewards.json && echo '===SEP===' && cat /tmp/test_unauth.json"`; + const fetched = execSync(fetchCmd, { timeout: 25000 }).toString(); + const parts = fetched.split('===SEP===').map(p => p.trim()); + + const u10Agent = JSON.parse(parts[0]); + const u10Profile = JSON.parse(parts[1]); + const u10Index = JSON.parse(parts[2]); + const u20Agent = JSON.parse(parts[3]); + const u20Rewards = JSON.parse(parts[4]); + const unauth = JSON.parse(parts[5]); + + // 场景 1:用户10(尚善,仅有自购订单并释放兑换券,无下级分成收益) + console.log("\n场景 1:用户10(仅自购与兑换券释放,无分成收益):"); + assert(u10Agent.code === 1, 'commission.agent 返回成功'); + assert(u10Agent.data.total_income === '0.00', `累计收益严格为分成收益 (0.00),实际: ${u10Agent.data.total_income}`); + assert(Number(u10Agent.data.voucher_available) > 0, `可用兑换券保持独立正常发放 (${u10Agent.data.voucher_available})`); + assert(u10Agent.data.total_income !== u10Agent.data.voucher_available, '累计收益不再与兑换券数值混淆'); + assert(u10Profile.data.total_income === '0.00', `user.user/profile 累计收益同步为 0.00`); + assert(u10Index.data.total_income === '0.00', `partner.index/index 累计收益同步为 0.00`); + + // 场景 2:用户20(有真实团队分成和日结现金分红) + console.log("\n场景 2:用户20(有真实团队分成与日结分红):"); + assert(u20Agent.code === 1, 'user20 commission.agent 返回成功'); + const u20Income = Number(u20Agent.data.total_income); + assert(u20Income > 0, `user20 累计收益正确统计真实分成收益 (>0),实际: ${u20Agent.data.total_income}`); + assert(u20Rewards.code === 1, 'user20 partner.index/rewards 列表接口成功'); + assert(u20Rewards.data.total_income === u20Agent.data.total_income, 'rewards 接口累计收益与 agent 接口严格一致'); + assert(u20Rewards.data.data.length > 0, `rewards 明细包含 ${u20Rewards.data.data.length} 笔真实分成收益`); + assert(u20Rewards.data.data.every(r => !['voucher_release', 'voucher_grant', 'madow_share'].includes(r.reward_type)), + '收益明细列表中绝无兑换券释放或dmepa记录混入'); + + // 场景 3:未登录拦截与安全防御 + console.log("\n场景 3:未登录参数校验拦截测试:"); + assert(unauth.code === 401, '未传 token 严格返回 401 认证拦截'); + assert(unauth.msg === '请登录后操作', '返回中文友好提示“请登录后操作”'); + +} catch (e) { + console.error("测试执行异常:", e.message); + failed++; +} + +console.log("\n========================================================"); +console.log(`测试结果: 通过 ${passed} 项, 失败 ${failed} 项`); +console.log("========================================================"); + +if (failed > 0) { + process.exit(1); +}