diff --git a/addons/shopro/service/partner/RateService.php b/addons/shopro/service/partner/RateService.php index aa01b9c..93bd25d 100644 --- a/addons/shopro/service/partner/RateService.php +++ b/addons/shopro/service/partner/RateService.php @@ -14,11 +14,11 @@ class RateService public function currentRate() { $latest = RateLog::order('id', 'desc')->find(); - if ($latest) { + if ($latest && (float) $latest->rate > 0) { return (float) $latest->rate; } $cached = cache('partner_madow_current_rate'); - if ($cached !== null && $cached !== false) { + if ($cached !== null && $cached !== false && (float) $cached > 0) { return (float) $cached; } $rate = $this->calcRate($this->countN()); @@ -32,7 +32,8 @@ class RateService $base = (float) PartnerConfig::get('partner_madow_base_rate', 0.00000075); $step = intval(PartnerConfig::get('partner_madow_step_orders', 1)); $step = $step > 0 ? $step : 1; - return $base * floor($n / $step); + $multiple = max(1, floor($n / $step)); + return function_exists('bcmul') ? (float) bcmul((string) $base, (string) $multiple, 10) : (float) ($base * $multiple); } public function countN() diff --git a/application/admin/controller/shopro/Dashboard.php b/application/admin/controller/shopro/Dashboard.php index 541f48a..839e327 100644 --- a/application/admin/controller/shopro/Dashboard.php +++ b/application/admin/controller/shopro/Dashboard.php @@ -139,8 +139,11 @@ class Dashboard extends Common 'available_voucher' => number_format($availableVoucher, 2, '.', ''), ]; - // 7. 合伙股东网络与资本活力 + // 7. 合伙股东网络与全角色身份资产矩阵 (会员数、马豆数量、分佣数量、马豆价值) + $totalRegisteredUsers = (int) User::count(); $totalAgents = (int) Agent::count(); + $normalUserCount = max(0, $totalRegisteredUsers - $totalAgents); + $tasterCount = (int) Agent::where('level', 1)->count(); $makerCount = (int) Agent::where('level', '>=', 2)->count(); $makerLevels = []; @@ -155,8 +158,119 @@ class Dashboard extends Common $inactiveCount = max(0, $totalAgents - $activeCount); $activeRate = $totalAgents > 0 ? round(($activeCount / $totalAgents) * 100, 1) : 0.0; + // 全量计算各角色身份名下资产与分佣 + $allAgents = Agent::field('user_id, level, is_venue, is_county, madow, last_self_order_time')->select(); + $userRewardAgg = Reward::where('status', 1) + ->where('user_id', '>', 0) + ->field('user_id, sum(amount) as total_amount, count(*) as count') + ->group('user_id') + ->select(); + + $userRewards = []; + foreach ($userRewardAgg as $rw) { + $userRewards[$rw->user_id] = [ + 'amount' => (float) $rw->total_amount, + 'count' => (int) $rw->count, + ]; + } + + $calcRoleStats = function ($filterCallback, $roleKey, $roleName, $category, $unit, $badge, $color, $filterLevel = '', $filterType = '') use ($allAgents, $userRewards, $currentRate, $activeWindow) { + $uCount = 0; + $madowTotal = 0.0; + $rewardAmt = 0.0; + $rewardCnt = 0; + $activeCnt = 0; + + foreach ($allAgents as $ag) { + if ($filterCallback($ag)) { + $uCount++; + $madowTotal = (float) bcadd((string)$madowTotal, (string)($ag->madow ?? 0), 8); + $uid = $ag->user_id; + if (isset($userRewards[$uid])) { + $rewardAmt = round($rewardAmt + $userRewards[$uid]['amount'], 2); + $rewardCnt += $userRewards[$uid]['count']; + } + if ((int) $ag->getData('last_self_order_time') >= $activeWindow) { + $activeCnt++; + } + } + } + + $madowCny = round($madowTotal * $currentRate, 2); + $activeRate = $uCount > 0 ? round(($activeCnt / $uCount) * 100, 1) : 0.0; + + return [ + 'role_key' => $roleKey, + 'role_name' => $roleName, + 'category' => $category, + 'unit' => $unit, + 'badge' => $badge, + 'color' => $color, + 'user_count' => $uCount, + 'madow_total' => IdentityService::formatMadow($madowTotal), + 'madow_cny' => number_format($madowCny, 2, '.', ''), + 'madow_cny_raw' => round($madowTotal * $currentRate, 4), + 'reward_amount' => number_format($rewardAmt, 2, '.', ''), + 'reward_count' => $rewardCnt, + 'active_count' => $activeCnt, + 'active_rate' => $activeRate, + 'filter_level' => $filterLevel, + 'filter_type' => $filterType, + ]; + }; + + // 统计各角色身份详细指标 + $roleList = [ + $calcRoleStats(function($a) { return (int)$a->level == 1; }, 'taster', '品酒师', '基础身份', '人', '品', '#806af6', 1, ''), + $calcRoleStats(function($a) { return (int)$a->level == 2; }, 'maker_v1', '创客V1', '创客梯队', '人', 'V1', '#13c2c2', 2, ''), + $calcRoleStats(function($a) { return (int)$a->level == 3; }, 'maker_v2', '创客V2', '创客梯队', '人', 'V2', '#13c2c2', 3, ''), + $calcRoleStats(function($a) { return (int)$a->level == 4; }, 'maker_v3', '创客V3', '创客梯队', '人', 'V3', '#13c2c2', 4, ''), + $calcRoleStats(function($a) { return (int)$a->level == 5; }, 'maker_v4', '创客V4', '创客梯队', '人', 'V4', '#13c2c2', 5, ''), + $calcRoleStats(function($a) { return (int)$a->level == 6; }, 'maker_v5', '创客V5', '创客梯队', '人', 'V5', '#13c2c2', 6, ''), + $calcRoleStats(function($a) { return (int)$a->level == 7; }, 'maker_v6', '创客V6', '创客梯队', '人', 'V6', '#13c2c2', 7, ''), + $calcRoleStats(function($a) { return (int)$a->level >= 2; }, 'maker_total', '创客队伍小计', '创客梯队', '人', '创', '#13c2c2', 'maker', ''), + $calcRoleStats(function($a) { return (int)$a->is_venue == 1; }, 'venue', '实体酒道馆', '渠道网络', '家', '馆', '#fa8c16', '', 'venue'), + $calcRoleStats(function($a) { return (int)$a->is_county == 1; }, 'county', '区县代理', '渠道网络', '个', '县', '#722ed1', '', 'county'), + $calcRoleStats(function($a) { return (int)$a->level >= 2 || (int)$a->is_venue == 1 || (int)$a->is_county == 1; }, 'partner_total', '合伙股东汇总', '汇总统计', '人', '合', '#eb2f96', '', 'partner'), + ]; + + // 增加普通注册会员行 + $roleList[] = [ + 'role_key' => 'normal_user', + 'role_name' => '普通注册会员', + 'category' => '电商用户', + 'unit' => '人', + 'badge' => '普', + 'color' => '#8c8c8c', + 'user_count' => $normalUserCount, + 'madow_total' => '0.00000000', + 'madow_cny' => '0.00', + 'madow_cny_raw' => 0.0, + 'reward_amount' => '0.00', + 'reward_count' => 0, + 'active_count' => 0, + 'active_rate' => 0.0, + 'filter_level' => '', + 'filter_type' => 'normal', + ]; + + // 全平台全员合计行 + $allTotalRow = $calcRoleStats(function($a) { return true; }, 'all_total', '全平台全员合计', '汇总统计', '人', '全', '#1890ff', '', 'all'); + if ($normalUserCount > 0) { + $allTotalRow['user_count'] = $totalRegisteredUsers; + $allTotalRow['active_rate'] = $totalRegisteredUsers > 0 ? round(($allTotalRow['active_count'] / $totalRegisteredUsers) * 100, 1) : 0.0; + } + $roleList[] = $allTotalRow; + + $roleStats = []; + foreach ($roleList as $rItem) { + $roleStats[$rItem['role_key']] = $rItem; + } + $partnerNetwork = [ + 'total_registered_users' => $totalRegisteredUsers, 'total_agents' => $totalAgents, + 'normal_user_count' => $normalUserCount, 'taster_count' => $tasterCount, 'maker_count' => $makerCount, 'maker_levels' => $makerLevels, @@ -165,6 +279,8 @@ class Dashboard extends Common 'active_count' => $activeCount, 'inactive_count' => $inactiveCount, 'active_rate' => $activeRate, + 'role_list' => $roleList, + 'role_stats' => $roleStats, ]; $this->success('获取成功', null, [ diff --git a/application/admin/view/shopro/dashboard/index.html b/application/admin/view/shopro/dashboard/index.html index 7f2b59a..3f1e84e 100644 --- a/application/admin/view/shopro/dashboard/index.html +++ b/application/admin/view/shopro/dashboard/index.html @@ -110,6 +110,13 @@ .dashboard-index .bg-primary { background: #806af6; } .dashboard-index .bg-cyan { background: #13c2c2; } .dashboard-index .bg-orange { background: #fa8c16; } + .dashboard-index .bg-purple { background: #722ed1; } + + .dashboard-index .role-table-card { + padding: 16px 20px; + margin-top: 14px; + margin-bottom: 16px; + } .dashboard-index #userTotal, .dashboard-index #agentTotal, @@ -336,7 +343,8 @@ - + + @@ -344,17 +352,21 @@
品 - 品酒师基础会员 + 品酒师队伍
{{ state.partner.taster_count }} 人
- 30日自购活跃率:{{ state.partner.active_rate }}% - 正常:{{ state.partner.active_count }} 人 / 暂停:{{ state.partner.inactive_count }} 人 + 持豆:{{ state.partner.role_stats?.taster?.madow_total || '0.00000000' }} 豆 + 分佣:¥{{ state.partner.role_stats?.taster?.reward_amount || '0.00' }} ({{ state.partner.role_stats?.taster?.reward_count || 0 }}笔)
- +
+ 30日活跃率:{{ state.partner.role_stats?.taster?.active_rate || 0 }}% ({{ state.partner.role_stats?.taster?.active_count || 0 }}人) + 马豆现值:¥{{ state.partner.role_stats?.taster?.madow_cny || '0.00' }} +
+
@@ -364,12 +376,20 @@
创 - 创客合伙股东 (V1~V6) + 创客合伙骨干 (V1~V6)
{{ state.partner.maker_count }} 人
+
+ 持豆:{{ state.partner.role_stats?.maker_total?.madow_total || '0.00000000' }} 豆 + 分佣:¥{{ state.partner.role_stats?.maker_total?.reward_amount || '0.00' }} ({{ state.partner.role_stats?.maker_total?.reward_count || 0 }}笔) +
+
+ 马豆现值:¥{{ state.partner.role_stats?.maker_total?.madow_cny || '0.00' }} + 活跃率:{{ state.partner.role_stats?.maker_total?.active_rate || 0 }}% ({{ state.partner.role_stats?.maker_total?.active_count || 0 }}人) +
{{ lv }}: {{ cnt }}人 @@ -383,21 +403,118 @@
- 馆 + 渠 实体与区域渠道
- 酒道馆 {{ state.partner.venue_count }} 家 - 区县 {{ state.partner.county_count }} 个 + 馆 {{ state.partner.venue_count }} 家 + 县 {{ state.partner.county_count }} 个
-
+
+ 酒道馆:持豆 {{ state.partner.role_stats?.venue?.madow_total || '0.00' }} / 分佣 ¥{{ state.partner.role_stats?.venue?.reward_amount || '0.00' }} +
+
+ 区县代理:持豆 {{ state.partner.role_stats?.county?.madow_total || '0.00' }} / 分佣 ¥{{ state.partner.role_stats?.county?.reward_amount || '0.00' }} +
+
享团队拆分 (50~100元) + 平台日结均分池 渠道档案 →
+ + + + +
+
+
+ 全角色身份会员资产与分佣统计全景矩阵 + + 逐一统计每一个角色身份的会员人数、持豆存量、实时市值、累计分佣金额与笔数 + +
+
+ 合伙股东档案 → + 全部分成流水 → +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
{/if} {if $auth->check('shopro/dashboard/chart')} diff --git a/public/assets/js/backend/shopro/dashboard.js b/public/assets/js/backend/shopro/dashboard.js index 8571e04..c20e5b0 100644 --- a/public/assets/js/backend/shopro/dashboard.js +++ b/public/assets/js/backend/shopro/dashboard.js @@ -33,7 +33,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function ( available_voucher: '0.00' }, partner: { + total_registered_users: 0, total_agents: 0, + normal_user_count: 0, taster_count: 0, maker_count: 0, maker_levels: {}, @@ -41,7 +43,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function ( county_count: 0, active_count: 0, inactive_count: 0, - active_rate: 0 + active_rate: 0, + role_list: [], + role_stats: {} }, total: { user: { @@ -734,6 +738,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function ( onOpenTab('shopro/partner/voucher/index', "兑换券管理"); } + function onOpenRoleAgent(item) { + if (!item) return onOpenAgent(''); + if (item.filter_type === 'normal') { + onOpenTab('shopro/user/user/index', '普通会员'); + } else if (item.filter_type === 'venue') { + onOpenTab('shopro/commission/agent/index?is_venue=1', '酒道馆档案'); + } else if (item.filter_type === 'county') { + onOpenTab('shopro/commission/agent/index?is_county=1', '区县代理档案'); + } else if (item.filter_type === 'partner') { + onOpenTab('shopro/commission/agent/index', '合伙股东档案'); + } else if (item.filter_level === 'maker') { + onOpenTab('shopro/commission/agent/index', '创客团队档案'); + } else if (item.filter_level) { + onOpenTab(`shopro/commission/agent/index?level=${item.filter_level}`, `${item.role_name}成员`); + } else { + onOpenTab('shopro/commission/agent/index', '合伙人档案'); + } + } + + function onOpenRoleReward(item) { + let url = 'shopro/partner/reward/index?status=1'; + onOpenTab(url, '收益明细'); + } + onMounted(() => { Config.cardList.forEach(item => { if (item.name == 'total' && item.status) { @@ -763,6 +791,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function ( onOpen, onOpenReward, onOpenAgent, + onOpenRoleAgent, + onOpenRoleReward, onOpenDaily, onOpenVoucher, onOpenTab, diff --git a/tests/partner/DashboardWealthTest.php b/tests/partner/DashboardWealthTest.php index 2552024..ae9bc0e 100644 --- a/tests/partner/DashboardWealthTest.php +++ b/tests/partner/DashboardWealthTest.php @@ -61,6 +61,7 @@ class DashboardWealthTest echo "========================================================\n\n"; $this->testTotalWealthData(); + $this->testAllRolesStatisticsIntegrity(); $this->testChartNormalData(); $this->testChartEmptyDateFallback(); $this->testRankingPartnerAndDailyData(); @@ -108,10 +109,49 @@ class DashboardWealthTest $this->assert(!empty($network) && isset($network['maker_count'], $network['taster_count'], $network['active_rate']), 'total() 返回完整的 partner_network 合伙股东活力分布'); + + $this->assert(!empty($network['role_list']) && is_array($network['role_list']), + 'total() 返回 role_list 全角色身份矩阵列表'); + + $this->assert(!empty($network['role_stats']) && is_array($network['role_stats']), + 'total() 返回 role_stats 全角色身份字典索引'); } /** - * 测试 2:核心走势 chart() 正常日期区间返回 + * 测试 2:每一个角色身份的会员数、马豆数量、分佣数量与马豆价值精算校验 + */ + public function testAllRolesStatisticsIntegrity() + { + $controller = new TestableDashboard(); + $controller->total(); + + $roleStats = $controller->lastOutput['data']['partner_network']['role_stats'] ?? []; + $requiredRoles = ['taster', 'maker_v1', 'maker_v2', 'maker_v3', 'maker_v4', 'maker_v5', 'maker_v6', 'maker_total', 'venue', 'county', 'partner_total', 'all_total']; + + foreach ($requiredRoles as $roleKey) { + $this->assert(isset($roleStats[$roleKey]), "矩阵包含角色身份: {$roleKey}"); + $item = $roleStats[$roleKey] ?? []; + $this->assert(isset($item['user_count'], $item['madow_total'], $item['madow_cny'], $item['reward_amount'], $item['reward_count'], $item['active_rate']), + "角色 {$roleKey} 完整包含会员数、持豆量、马豆市值、分佣总额、分佣笔数与活跃率"); + } + + // 勾稽校验:品酒师 (553.75) + 创客队伍 (838.00) = 全网分佣 (1391.75) + $tasterReward = (float)($roleStats['taster']['reward_amount'] ?? 0); + $makerReward = (float)($roleStats['maker_total']['reward_amount'] ?? 0); + $allReward = (float)($roleStats['all_total']['reward_amount'] ?? 0); + $this->assert(abs(($tasterReward + $makerReward) - $allReward) < 0.01, + "分佣勾稽校验: 品酒师({$tasterReward}) + 创客队伍({$makerReward}) == 全员分佣({$allReward})"); + + // 马豆总量勾稽校验:品酒师 (699.0) + 创客队伍 (699.0) = 全网马豆 (1398.0) + $tasterMadow = (float)($roleStats['taster']['madow_total'] ?? 0); + $makerMadow = (float)($roleStats['maker_total']['madow_total'] ?? 0); + $allMadow = (float)($roleStats['all_total']['madow_total'] ?? 0); + $this->assert(abs(($tasterMadow + $makerMadow) - $allMadow) < 0.0001, + "马豆勾稽校验: 品酒师({$tasterMadow}) + 创客队伍({$makerMadow}) == 全网马豆({$allMadow})"); + } + + /** + * 测试 3:核心走势 chart() 正常日期区间返回 */ public function testChartNormalData() { diff --git a/uniapp/pages/commission/index.vue b/uniapp/pages/commission/index.vue index 51e2cc8..d344656 100644 --- a/uniapp/pages/commission/index.vue +++ b/uniapp/pages/commission/index.vue @@ -4,7 +4,7 @@ - + 近 30 日暂无本人订单,推广与分红已暂停。