修复(收益): 纠正累计收益统计口径为真实分成收益,排除dmepa与兑换券释放干扰
这个提交包含在:
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户