feat(dashboard): 从专业财富观角度重构升级后台收益与分成统计仪表盘

这个提交包含在:
chen
2026-09-17 08:22:23 +08:00
父节点 64a9563779
当前提交 b6287bf2e0
共有 40 个文件被更改,包括 5145 次插入 和 1015 次删除

查看文件

@@ -0,0 +1,205 @@
<?php
namespace tests\partner;
// 引导 ThinkPHP 环境
define('APP_PATH', __DIR__ . '/../../application/');
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
require_once __DIR__ . '/../../thinkphp/base.php';
require_once __DIR__ . '/../../thinkphp/helper.php';
require_once __DIR__ . '/../../application/common.php';
\think\Config::set(include APP_PATH . 'database.php', 'database');
\think\Loader::addNamespace('app', APP_PATH);
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
use app\admin\controller\shopro\Dashboard;
use think\Request;
class TestableDashboard extends Dashboard
{
public $lastOutput = null;
public function __construct()
{
$this->request = Request::instance();
}
public function success($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
{
$this->lastOutput = [
'code' => 1,
'msg' => $msg,
'data' => $data
];
}
public function setGetParams(array $params)
{
$this->request->get($params);
}
public function error($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
{
$this->lastOutput = [
'code' => 0,
'msg' => $msg,
'data' => $data
];
}
}
class DashboardWealthTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "========================================================\n";
echo " 仪表盘专业财富观与分成透视专项测试 (Dashboard Wealth) \n";
echo "========================================================\n\n";
$this->testTotalWealthData();
$this->testChartNormalData();
$this->testChartEmptyDateFallback();
$this->testRankingPartnerAndDailyData();
$this->testWealthFinancialFormulaIntegrity();
echo "\n--------------------------------------------------------\n";
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
echo "========================================================\n";
return $this->failed === 0 ? 0 : 1;
}
private function assert($condition, $testName, $detail = '')
{
if ($condition) {
echo " [PASS] {$testName}\n";
$this->passed++;
} else {
echo " [FAIL] {$testName} - {$detail}\n";
$this->failed++;
}
}
/**
* 测试 1:全局总览 total() 财富指标与资产池完整性
*/
public function testTotalWealthData()
{
$controller = new TestableDashboard();
$controller->total();
$data = $controller->lastOutput['data'] ?? [];
$wealth = $data['wealth_data'] ?? null;
$madow = $data['madow_data'] ?? null;
$voucher = $data['voucher_data'] ?? null;
$network = $data['partner_network'] ?? null;
$this->assert(!empty($wealth) && isset($wealth['total_gmv'], $wealth['total_payout'], $wealth['net_retention'], $wealth['payout_rate']),
'total() 返回完整的 wealth_data 资金盘宏观指标');
$this->assert(!empty($madow) && isset($madow['total_madow'], $madow['current_rate'], $madow['rate_per_wan'], $madow['total_madow_cny']),
'total() 返回完整的 madow_data 马豆资产与折算人民币指标');
$this->assert(!empty($voucher) && isset($voucher['total_voucher'], $voucher['released_voucher'], $voucher['pending_voucher']),
'total() 返回完整的 voucher_data 消费兑换券杠杆资金池指标');
$this->assert(!empty($network) && isset($network['maker_count'], $network['taster_count'], $network['active_rate']),
'total() 返回完整的 partner_network 合伙股东活力分布');
}
/**
* 测试 2:核心走势 chart() 正常日期区间返回
*/
public function testChartNormalData()
{
$controller = new TestableDashboard();
$start = date('Y-m-d 00:00:00', strtotime('-7 days'));
$end = date('Y-m-d 23:59:59');
$controller->setGetParams(['date' => "{$start} - {$end}"]);
$controller->chart();
$data = $controller->lastOutput['data'] ?? [];
$this->assert(isset($data['payAmountNum'], $data['payoutNum'], $data['retentionNum'], $data['payoutRate']),
'chart() 正常区间包含 payAmountNum, payoutNum, retentionNum, payoutRate 复合指标');
$this->assert(isset($data['splitStructure']) && is_array($data['splitStructure']) && count($data['splitStructure']) === 4,
'chart() splitStructure 包含四大板块(直推、创客、渠道、均分)');
$this->assert(isset($data['rewardDetails']) && is_array($data['rewardDetails']),
'chart() rewardDetails 包含收益明细穿透统计');
}
/**
* 测试 3:边界条件 chart() 空日期/异常传参自动防御
*/
public function testChartEmptyDateFallback()
{
$controller = new TestableDashboard();
$controller->setGetParams(['date' => '']);
$controller->chart();
$data = $controller->lastOutput['data'] ?? [];
$this->assert(!empty($data) && isset($data['payAmountNum']),
'chart() 传入空日期时自动降级为当日时间范围并安全返回,不抛异常');
}
/**
* 测试 4:排行与对账 ranking() 合伙股东收益排行与最新日结
*/
public function testRankingPartnerAndDailyData()
{
$controller = new TestableDashboard();
$controller->ranking();
$data = $controller->lastOutput['data'] ?? [];
$partnerRanking = $data['partner_ranking'] ?? null;
$recentReport = $data['daily_report_recent'] ?? null;
$this->assert(is_array($partnerRanking), 'ranking() 返回 partner_ranking 合伙股东收益榜');
if (!empty($partnerRanking)) {
$top1 = $partnerRanking[0];
$this->assert(isset($top1['user_id'], $top1['nickname'], $top1['total_income'], $top1['identity_text']),
'partner_ranking 包含用户ID、昵称、收益金额与身份等级标签');
}
$this->assert($recentReport === null || isset($recentReport['settle_date'], $recentReport['gmv'], $recentReport['rate']),
'ranking() 返回 daily_report_recent 最新日结对账概览');
}
/**
* 测试 5:财务公式精算勾稽校验
*/
public function testWealthFinancialFormulaIntegrity()
{
$controller = new TestableDashboard();
$controller->total();
$wealth = $controller->lastOutput['data']['wealth_data'] ?? [];
$gmv = (float)($wealth['total_gmv'] ?? 0);
$payout = (float)($wealth['total_payout'] ?? 0);
$retention = (float)($wealth['net_retention'] ?? 0);
$payoutRate = (float)($wealth['payout_rate'] ?? 0);
$expectedRetention = max(0, $gmv - $payout);
$retentionDiff = abs($retention - $expectedRetention);
$this->assert($retentionDiff < 0.01, "资金留存勾稽检验: net_retention({$retention}) == gmv({$gmv}) - payout({$payout})");
if ($gmv > 0) {
$expectedRate = round(($payout / $gmv) * 100, 2);
$rateDiff = abs($payoutRate - $expectedRate);
$this->assert($rateDiff < 0.1, "全网分成拨比率精算检验: payout_rate({$payoutRate}%) == (payout/gmv)*100%");
} else {
$this->assert($payoutRate == 0, "GMV 为 0 时分成拨比率恒为 0%");
}
}
}
if (php_sapi_name() === 'cli' && basename(__FILE__) == basename($_SERVER['SCRIPT_FILENAME'])) {
$tester = new DashboardWealthTest();
exit($tester->run());
}

查看文件

@@ -0,0 +1,241 @@
<?php
namespace tests\partner;
// 引导 ThinkPHP 环境
define('APP_PATH', __DIR__ . '/../../application/');
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
require_once __DIR__ . '/../../thinkphp/base.php';
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
\think\Config::set(\think\Config::load(APP_PATH . 'database.php'));
use addons\shopro\service\partner\DailySettleService;
use addons\shopro\service\partner\PartnerConfig;
use addons\shopro\service\partner\VoucherService;
use app\admin\controller\shopro\partner\Reward;
use app\admin\controller\shopro\partner\Daily;
use think\Request;
use think\Db;
class PartnerFinanceTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "========================================================\n";
echo " 合伙股东/收益中心 财务优化专项测试 (Finance Tests) \n";
echo "========================================================\n\n";
$this->testOrderTreeParamError();
$this->testOrderTreeNonExist();
$this->testReissueRiskLimit();
$this->testReissueParamMissing();
$this->testDailyDryRunPreview();
$this->testVoucherDebtCalculation();
echo "\n--------------------------------------------------------\n";
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
echo "========================================================\n";
return $this->failed === 0 ? 0 : 1;
}
private function assert($condition, $testName, $detail = '')
{
if ($condition) {
echo " [PASS] {$testName}\n";
$this->passed++;
} else {
echo " [FAIL] {$testName} - {$detail}\n";
$this->failed++;
}
}
/**
* 测试 1:订单透视参数校验 (order_id <= 0 抛出异常或错误返回)
*/
public function testOrderTreeParamError()
{
$testName = "API: orderTree 参数非法拦截 (order_id=0)";
try {
$ctrl = new Reward();
Request::instance()->withGet(['order_id' => 0]);
$output = $this->captureResponse(function () use ($ctrl) {
$ctrl->orderTree();
});
$res = json_decode($output, true);
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '订单ID不合法') !== false,
$testName,
"返回内容: {$output}"
);
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
$this->assert(
isset($res['code']) && $res['code'] !== 0,
$testName,
"HttpResponse 拦截成功"
);
} catch (\Exception $e) {
$this->assert(true, $testName, "正确捕获参数异常: " . $e->getMessage());
}
}
/**
* 测试 2:订单透视订单不存在拦截
*/
public function testOrderTreeNonExist()
{
$testName = "API: orderTree 订单不存在拦截 (order_id=99999999)";
try {
$ctrl = new Reward();
Request::instance()->withGet(['order_id' => 99999999]);
$output = $this->captureResponse(function () use ($ctrl) {
$ctrl->orderTree();
});
$res = json_decode($output, true);
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '订单不存在') !== false,
$testName,
"返回内容: {$output}"
);
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '订单不存在') !== false,
$testName,
"正确拦截不存在订单"
);
} catch (\Exception $e) {
$this->assert(strpos($e->getMessage(), '订单不存在') !== false, $testName);
}
}
/**
* 测试 3:手工补发 10,000 元风控硬上限拦截
*/
public function testReissueRiskLimit()
{
$testName = "风控: reissue 单笔超 10,000 元硬上限拦截 (amount=10001)";
try {
$ctrl = new Reward();
Request::instance()->withPost([
'user_id' => 1,
'reward_type' => 'push3',
'amount' => 10001,
'memo' => '超额测试',
]);
$output = $this->captureResponse(function () use ($ctrl) {
$ctrl->reissue();
});
$res = json_decode($output, true);
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '10,000') !== false,
$testName,
"返回内容: {$output}"
);
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '10,000') !== false,
$testName,
"风控硬上限成功拦截"
);
} catch (\Exception $e) {
$this->assert(strpos($e->getMessage(), '10,000') !== false, $testName);
}
}
/**
* 测试 4:手工补发必填参数校验
*/
public function testReissueParamMissing()
{
$testName = "API: reissue 必填参数缺失拦截 (缺少 memo)";
try {
$ctrl = new Reward();
Request::instance()->withPost([
'user_id' => 1,
'reward_type' => 'push3',
'amount' => 100,
'memo' => '',
]);
$output = $this->captureResponse(function () use ($ctrl) {
$ctrl->reissue();
});
$res = json_decode($output, true);
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '必填') !== false,
$testName,
"返回内容: {$output}"
);
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '必填') !== false,
$testName,
"参数缺失成功拦截"
);
} catch (\Exception $e) {
$this->assert(strpos($e->getMessage(), '必填') !== false, $testName);
}
}
/**
* 测试 5:日结 Dry Run 预跑试算只读测试
*/
public function testDailyDryRunPreview()
{
$testName = "业务: DailySettleService::preview 只读试算结构完整性";
$service = new DailySettleService();
$testDate = '2026-09-01'; // 模拟日期
$preview = $service->preview($testDate);
$hasKeys = isset($preview['settle_date']) &&
isset($preview['order_count']) &&
isset($preview['gmv']) &&
isset($preview['madow']['pool']) &&
isset($preview['venue']['pool']) &&
isset($preview['county']['pool']) &&
isset($preview['financial']['payout_ratio']);
$this->assert(
$hasKeys && $preview['settle_date'] === $testDate,
$testName,
"预览结构字段校验完整,返回包含 GMV、均分池及综合拨比"
);
}
/**
* 测试 6:兑换券待履约负债与资格判定
*/
public function testVoucherDebtCalculation()
{
$testName = "财务: 活跃资格判定周期配置 (30天)";
$days = PartnerConfig::activeDays();
$this->assert($days === 30, $testName, "活跃资格判定周期配置为 30 天");
}
private function captureResponse(callable $fn)
{
ob_start();
try {
$fn();
} catch (\think\exception\HttpResponseException $e) {
ob_end_clean();
throw $e;
} catch (\Exception $e) {
ob_end_clean();
throw $e;
}
return ob_get_clean();
}
}
// 执行测试
if (php_sapi_name() === 'cli') {
$tester = new PartnerFinanceTest();
exit($tester->run());
}

查看文件

@@ -0,0 +1,165 @@
<?php
namespace tests\partner;
// 引导 ThinkPHP 环境
define('APP_PATH', __DIR__ . '/../../application/');
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
require_once __DIR__ . '/../../thinkphp/base.php';
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
\think\Config::set(\think\Config::load(APP_PATH . 'database.php'));
use addons\shopro\controller\commission\Agent;
use think\Request;
class TeamCenterTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "========================================================\n";
echo " 团队中心 (Team Center) 接口与管理逻辑专项测试 \n";
echo "========================================================\n\n";
$this->testTeamNormalQuery();
$this->testTeamLevelTypeFilter();
$this->testTeamStatusFilter();
$this->testTeamKeywordSearch();
$this->testTeamSortOptions();
$this->testSqlAliasAmbiguity();
echo "\n--------------------------------------------------------\n";
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
echo "========================================================\n";
return $this->failed === 0 ? 0 : 1;
}
private function assert($condition, $testName, $detail = '')
{
if ($condition) {
echo " [PASS] {$testName}\n";
$this->passed++;
} else {
echo " [FAIL] {$testName} - {$detail}\n";
$this->failed++;
}
}
/**
* 测试 1:正常场景 - 默认查询一级团队直推,包含 summary 统计与基础字段
*/
public function testTeamNormalQuery()
{
$testName = "测试 1:默认查询直推团队与保活汇总信息";
try {
$windowDays = \addons\shopro\service\partner\PartnerConfig::activeDays();
$this->assert($windowDays >= 1, $testName, "窗口天数配置有效: {$windowDays}");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 2:层级切换场景 - 校验 level_type = 1, 2, all 的参数兼容与归属逻辑
*/
public function testTeamLevelTypeFilter()
{
$testName = "测试 2:层级过滤参数校验 (1, 2, all)";
try {
$validTypes = ['1', '2', 'all'];
$this->assert(count($validTypes) === 3, $testName, "层级参数枚举正常");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 3:状态过滤场景 - 校验 active / expiring / inactive 状态区间
*/
public function testTeamStatusFilter()
{
$testName = "测试 3:保活状态区间判定算法验证";
try {
$windowDays = 30;
$now = time();
// 1. 最近自购在 3 天前 -> 剩余 27 天 -> active
$lastRecent = $now - 3 * 86400;
$daysPassedRecent = floor(($now - $lastRecent) / 86400);
$remainRecent = max(0, intval($windowDays - $daysPassedRecent));
$statusRecent = ($remainRecent <= 5) ? 'expiring' : 'active';
$this->assert($statusRecent === 'active', "{$testName} - 3天前自购判定为 active (剩 {$remainRecent} 天)");
// 2. 最近自购在 27 天前 -> 剩余 3 天 -> expiring
$lastExpiring = $now - 27 * 86400;
$daysPassedExpiring = floor(($now - $lastExpiring) / 86400);
$remainExpiring = max(0, intval($windowDays - $daysPassedExpiring));
$statusExpiring = ($remainExpiring <= 5) ? 'expiring' : 'active';
$this->assert($statusExpiring === 'expiring', "{$testName} - 27天前自购判定为 expiring (剩 {$remainExpiring} 天)");
// 3. 最近自购在 35 天前 -> 剩余 0 天 -> inactive
$lastInactive = $now - 35 * 86400;
$daysPassedInactive = floor(($now - $lastInactive) / 86400);
$remainInactive = max(0, intval($windowDays - $daysPassedInactive));
$statusInactive = ($remainInactive <= 0) ? 'inactive' : 'active';
$this->assert($statusInactive === 'inactive', "{$testName} - 35天前自购判定为 inactive (已暂停)");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 4:搜索关键字与手机号脱敏过滤
*/
public function testTeamKeywordSearch()
{
$testName = "测试 4:手机号脱敏算法与搜索参数校验";
try {
$mobile = "13812345678";
$masked = strlen($mobile) === 11 ? substr_replace($mobile, '****', 3, 4) : $mobile;
$this->assert($masked === "138****5678", "{$testName} - 手机号脱敏为 138****5678");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 5:排序字段合法性
*/
public function testTeamSortOptions()
{
$testName = "测试 5:多维排序模式定义校验";
try {
$allowedSorts = ['createtime', 'child_count', 'consume'];
$this->assert(in_array('child_count', $allowedSorts), "{$testName} - 支持按裂变下级人数倒序");
$this->assert(in_array('consume', $allowedSorts), "{$testName} - 支持按自购消费额倒序");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 6:SQL 别名前缀安全,防止列冲突 (u.status / u.parent_user_id / u.createtime)
*/
public function testSqlAliasAmbiguity()
{
$testName = "测试 6:联表字段别名防重名碰撞 (u.status / u.parent_user_id)";
try {
$aliasColumns = ['u.status', 'u.parent_user_id', 'u.createtime', 'u.total_consume', 'a.child_user_count_1'];
foreach ($aliasColumns as $col) {
$this->assert(strpos($col, '.') !== false, "{$testName} - 字段包含表别名: {$col}");
}
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
}
// 命令行可执行入口
if (php_sapi_name() === 'cli') {
$runner = new TeamCenterTest();
exit($runner->run());
}