From 464f8b2f38d69c6022c73109ab014c0893b76fdb Mon Sep 17 00:00:00 2001 From: chen Date: Thu, 17 Sep 2026 14:22:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E4=BC=98=E5=8C=96=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8F=B0=E8=B4=A2=E5=8A=A1=E4=B8=8E=E8=BF=90=E8=90=A5?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=EF=BC=8C=E5=BD=BB=E5=BA=95=E6=B6=88=E9=99=A4?= =?UTF-8?q?=E8=AF=AF=E5=AF=BC=E4=BF=A1=E6=81=AF=E5=B9=B6=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E5=BE=85=E5=8A=9E=E9=98=B2=E6=BC=8F=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopro/service/partner/ActiveService.php | 19 +- .../admin/controller/shopro/Dashboard.php | 617 ++++++++++-------- .../admin/view/shopro/dashboard/index.html | 289 ++++---- public/assets/js/backend/shopro/dashboard.js | 42 +- tests/partner/DashboardWealthTest.php | 45 +- 5 files changed, 564 insertions(+), 448 deletions(-) diff --git a/addons/shopro/service/partner/ActiveService.php b/addons/shopro/service/partner/ActiveService.php index 231e53a..7a0e175 100644 --- a/addons/shopro/service/partner/ActiveService.php +++ b/addons/shopro/service/partner/ActiveService.php @@ -50,21 +50,24 @@ class ActiveService public function activeUserIds($start, $end, $extraWhere = []) { - $baseQuery = \think\Db::name('shopro_commission_agent') - ->where('status', 'in', ['normal', 'freeze']) - ->where('level', '>=', 1); - foreach ($extraWhere as $field => $value) { - $baseQuery->where($field, $value); - } + $createBaseQuery = function () use ($extraWhere) { + $q = \think\Db::name('shopro_commission_agent') + ->where('status', 'in', ['normal', 'freeze']) + ->where('level', '>=', 1); + foreach ($extraWhere as $field => $value) { + $q->where($field, $value); + } + return $q; + }; // 1. 优先通过 last_self_order_time 索引快速命中 - $hitUserIds = (clone $baseQuery)->where(function ($q) use ($start, $end) { + $hitUserIds = $createBaseQuery()->where(function ($q) use ($start, $end) { $q->where('last_self_order_time', '>=', $start) ->where('last_self_order_time', '<=', $end); })->column('user_id'); // 2. 容错:查找 last_self_order_time 为空或为 0 的代理商 - $nullUserIds = (clone $baseQuery)->where(function ($q) { + $nullUserIds = $createBaseQuery()->where(function ($q) { $q->whereNull('last_self_order_time')->whereOr('last_self_order_time', 0); })->column('user_id'); diff --git a/application/admin/controller/shopro/Dashboard.php b/application/admin/controller/shopro/Dashboard.php index 8158e09..7fb2f7b 100644 --- a/application/admin/controller/shopro/Dashboard.php +++ b/application/admin/controller/shopro/Dashboard.php @@ -16,9 +16,11 @@ use addons\shopro\service\SearchHistory; use addons\shopro\service\partner\RateService; use addons\shopro\service\partner\IdentityService; use addons\shopro\service\partner\PartnerConfig; +use addons\shopro\service\partner\ActiveService; use app\admin\model\shopro\Withdraw; use app\admin\model\shopro\Refund; use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel; +use app\admin\model\shopro\order\Aftersale; class Dashboard extends Common { @@ -82,33 +84,58 @@ class Dashboard extends Common }) ]; - // 4. 全局财富大盘与资金流 (专业财富指标) + // 4. 全局实收流水与资金大盘 (以 paid_time 为准,与商户号入账对齐) $paidStatuses = [Order::STATUS_PAID, Order::STATUS_COMPLETED]; $totalGmv = (float) Order::whereIn('status', $paidStatuses)->sum('pay_fee'); - $todayGmv = (float) Order::whereIn('status', $paidStatuses)->whereTime('createtime', 'today')->sum('pay_fee'); + + $todayStart = strtotime('today'); + $todayEnd = strtotime('tomorrow') - 1; + $getTodayPaidQuery = function() use ($paidStatuses, $todayStart, $todayEnd) { + return Order::whereIn('status', $paidStatuses) + ->whereRaw("((paid_time >= {$todayStart} AND paid_time <= {$todayEnd}) OR ((paid_time IS NULL OR paid_time = 0) AND createtime >= {$todayStart} AND createtime <= {$todayEnd}))"); + }; + $todayGmv = (float) $getTodayPaidQuery()->sum('pay_fee'); $paidOrderCount = (int) Order::whereIn('status', $paidStatuses)->count(); - $todayPaidOrderCount = (int) Order::whereIn('status', $paidStatuses)->whereTime('createtime', 'today')->count(); + $todayPaidOrderCount = (int) $getTodayPaidQuery()->count(); $avgOrderAmount = $paidOrderCount > 0 ? round($totalGmv / $paidOrderCount, 2) : 0.00; + // 退款数据统计 + $totalRefundAmount = (float) Refund::where('status', 'completed')->sum('refund_fee'); + $totalRefundCount = (int) Refund::where('status', 'completed')->count(); + $todayRefundAmount = (float) Refund::where('status', 'completed')->whereTime('createtime', 'today')->sum('refund_fee'); + + // 净营收 (GMV 扣除退款) + $netGmv = max(0, round($totalGmv - $totalRefundAmount, 2)); + $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'); - $netRetention = round($totalGmv - $totalPayout, 2); - $payoutRate = $totalGmv > 0 ? round(($totalPayout / $totalGmv) * 100, 2) : 0.00; + + // 平台有效留存 = 有效净 GMV (扣除退款后) - 真实分成总支出 + $netRetention = round($netGmv - $totalPayout, 2); + // 真实综合分成拨比 = 真实分成支出 / 有效净 GMV + $payoutRate = $netGmv > 0 ? round(($totalPayout / $netGmv) * 100, 2) : 0.00; $wealthData = [ 'total_gmv' => number_format($totalGmv, 2, '.', ''), 'today_gmv' => number_format($todayGmv, 2, '.', ''), + 'total_refund_amount' => number_format($totalRefundAmount, 2, '.', ''), + 'total_refund_count' => $totalRefundCount, + 'net_gmv' => number_format($netGmv, 2, '.', ''), + 'today_net_gmv' => number_format($todayNetGmv, 2, '.', ''), 'total_payout' => number_format($totalPayout, 2, '.', ''), 'today_payout' => number_format($todayPayout, 2, '.', ''), 'net_retention' => number_format($netRetention, 2, '.', ''), 'payout_rate' => number_format($payoutRate, 2, '.', ''), + 'refund_rate' => number_format($refundRate, 2, '.', ''), 'paid_order_count' => $paidOrderCount, 'today_paid_order_count' => $todayPaidOrderCount, 'avg_order_amount' => number_format($avgOrderAmount, 2, '.', ''), ]; - // 5. 马豆生态资产蓄水池 + // 5. 生态营销资产蓄水池 (基于日结订单利润均分释放,单向升值赋能,无现金刚性回购) $totalMadow = (float) Agent::sum('madow'); $rateService = new RateService(); $currentRate = $rateService->currentRate(); @@ -121,13 +148,13 @@ class Dashboard extends Common $madowData = [ 'total_madow' => IdentityService::formatMadow($totalMadow), 'current_rate' => number_format($currentRate, 10, '.', ''), - 'rate_per_wan' => number_format($currentRate * 10000, 4, '.', ''), // 每万豆价值折算 + 'rate_per_wan' => number_format($currentRate * 10000, 4, '.', ''), 'total_madow_cny' => number_format($totalMadowCny, 2, '.', ''), 'history_order_n' => $historyOrderN, 'yesterday_madow_pool' => IdentityService::formatMadow($yesterdayMadowPool), ]; - // 6. 消费兑换券杠杆池 + // 6. 消费兑换券杠杆池 (商城购物抵扣补贴,非现金兑付,无现金偿债压力) $totalVoucher = (float) VoucherPlan::sum('total_amount'); $releasedVoucher = (float) VoucherPlan::sum('released_amount'); $usedVoucher = (float) Agent::sum('voucher_used'); @@ -142,7 +169,7 @@ 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); @@ -156,28 +183,49 @@ class Dashboard extends Common $venueCount = (int) Agent::where('is_venue', 1)->count(); $countyCount = (int) Agent::where('is_county', 1)->count(); - $activeWindow = time() - (int) PartnerConfig::get('partner_active_days', 30) * 86400; - $activeCount = (int) Agent::where('last_self_order_time', '>=', $activeWindow)->count(); - $inactiveCount = max(0, $totalAgents - $activeCount); - $activeRate = $totalAgents > 0 ? round(($activeCount / $totalAgents) * 100, 1) : 0.0; + // 30天自购活跃度核算 (对接 ActiveService 标准服务类) + $activeService = new ActiveService(); + $now = time(); + $activeDays = PartnerConfig::activeDays(); + $activeWindowStart = $activeService->windowStart($now); + $warningWindowStart = $now - (($activeDays - 7) * 86400); - // 全量计算各角色身份名下资产与分佣 + // 获取全量合格自购活跃用户 ID 集合 (含历史有效订单容错) + $activeUserIds = $activeService->activeUserIds($activeWindowStart, $now); + $activeUserMap = array_flip($activeUserIds); + + // 全量合伙人基础信息 $allAgents = Agent::field('user_id, level, is_venue, is_county, madow, last_self_order_time')->select(); + + // 收益明细按 (user_id + reward_type) 聚合,精确归集各角色产出收益 $userRewardAgg = Reward::where('status', 1) ->where('user_id', '>', 0) - ->field('user_id, sum(amount) as total_amount, count(*) as count') - ->group('user_id') + ->field('user_id, reward_type, sum(amount) as total_amount, count(*) as count') + ->group('user_id, reward_type') ->select(); - $userRewards = []; + $userTypeRewards = []; foreach ($userRewardAgg as $rw) { - $userRewards[$rw->user_id] = [ + $uid = (int) $rw->user_id; + $rType = (string) $rw->reward_type; + if (!isset($userTypeRewards[$uid])) { + $userTypeRewards[$uid] = []; + } + $userTypeRewards[$uid][$rType] = [ '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) { + // 角色专属收益类型映射表 + $roleRewardTypeFilter = [ + 'taster' => ['push3'], + 'maker' => ['maker_diff', 'maker_peer', 'maker_overtaken'], + 'venue' => ['team_venue', 'venue_peer', 'venue_share'], + 'county' => ['team_county', 'county_peer', 'county_share'], + ]; + + $calcRoleStats = function ($filterCallback, $roleKey, $roleName, $category, $unit, $badge, $color, $filterLevel = '', $filterType = '', $rewardTypeCategory = null) use ($allAgents, $userTypeRewards, $currentRate, $activeUserMap) { $uCount = 0; $madowTotal = 0.0; $rewardAmt = 0.0; @@ -188,12 +236,25 @@ class Dashboard extends Common 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']; + $uid = (int) $ag->user_id; + + // 收益按角色类型精准归集 + if (isset($userTypeRewards[$uid])) { + foreach ($userTypeRewards[$uid] as $rtype => $rdata) { + $match = false; + if ($rewardTypeCategory === null) { + $match = true; + } elseif (is_array($rewardTypeCategory) && in_array($rtype, $rewardTypeCategory)) { + $match = true; + } + if ($match) { + $rewardAmt = round($rewardAmt + $rdata['amount'], 2); + $rewardCnt += $rdata['count']; + } + } } - if ((int) $ag->getData('last_self_order_time') >= $activeWindow) { + + if (isset($activeUserMap[$uid])) { $activeCnt++; } } @@ -217,6 +278,7 @@ class Dashboard extends Common 'reward_count' => $rewardCnt, 'active_count' => $activeCnt, 'active_rate' => $activeRate, + 'is_exempt_active' => false, 'filter_level' => $filterLevel, 'filter_type' => $filterType, ]; @@ -224,20 +286,19 @@ class Dashboard extends Common // 统计各角色身份详细指标 $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'), + $calcRoleStats(function($a) { return (int)$a->level == 1; }, 'taster', '品酒师', '基础身份', '人', '品', '#806af6', 1, '', $roleRewardTypeFilter['taster']), + $calcRoleStats(function($a) { return (int)$a->level == 2; }, 'maker_v1', '创客V1', '创客梯队', '人', 'V1', '#13c2c2', 2, '', $roleRewardTypeFilter['maker']), + $calcRoleStats(function($a) { return (int)$a->level == 3; }, 'maker_v2', '创客V2', '创客梯队', '人', 'V2', '#13c2c2', 3, '', $roleRewardTypeFilter['maker']), + $calcRoleStats(function($a) { return (int)$a->level == 4; }, 'maker_v3', '创客V3', '创客梯队', '人', 'V3', '#13c2c2', 4, '', $roleRewardTypeFilter['maker']), + $calcRoleStats(function($a) { return (int)$a->level == 5; }, 'maker_v4', '创客V4', '创客梯队', '人', 'V4', '#13c2c2', 5, '', $roleRewardTypeFilter['maker']), + $calcRoleStats(function($a) { return (int)$a->level == 6; }, 'maker_v5', '创客V5', '创客梯队', '人', 'V5', '#13c2c2', 6, '', $roleRewardTypeFilter['maker']), + $calcRoleStats(function($a) { return (int)$a->level == 7; }, 'maker_v6', '创客V6', '创客梯队', '人', 'V6', '#13c2c2', 7, '', $roleRewardTypeFilter['maker']), + $calcRoleStats(function($a) { return (int)$a->level >= 2; }, 'maker_total', '创客队伍小计', '创客梯队', '人', '创', '#13c2c2', 'maker', '', $roleRewardTypeFilter['maker']), + $calcRoleStats(function($a) { return (int)$a->is_venue == 1; }, 'venue', '实体酒道馆', '渠道授权', '家', '馆', '#fa8c16', '', 'venue', $roleRewardTypeFilter['venue']), + $calcRoleStats(function($a) { return (int)$a->is_county == 1; }, 'county', '区县代理', '渠道授权', '个', '县', '#722ed1', '', 'county', $roleRewardTypeFilter['county']), ]; - // 增加普通注册会员行 + // 增加普通注册会员行 (明确标明无需自购考核) $roleList[] = [ 'role_key' => 'normal_user', 'role_name' => '普通注册会员', @@ -251,18 +312,34 @@ class Dashboard extends Common 'madow_cny_raw' => 0.0, 'reward_amount' => '0.00', 'reward_count' => 0, - 'active_count' => 0, - 'active_rate' => 0.0, + 'active_count' => '-', + 'active_rate' => 100, + 'is_exempt_active' => true, + 'active_text' => '无需自购考核', '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; - } + // 全平台全员合计行 (总马豆与总分佣与顶部大盘完全一致) + $allTotalRow = [ + 'role_key' => 'all_total', + 'role_name' => '全平台全员合计', + 'category' => '全网总计', + 'unit' => '人', + 'badge' => '全', + 'color' => '#1890ff', + 'user_count' => $totalRegisteredUsers, + 'madow_total' => IdentityService::formatMadow($totalMadow), + 'madow_cny' => number_format($totalMadowCny, 2, '.', ''), + 'madow_cny_raw' => $totalMadowCny, + 'reward_amount' => number_format($totalPayout, 2, '.', ''), + 'reward_count' => (int) Reward::where('status', 1)->count(), + 'active_count' => count($activeUserIds), + 'active_rate' => $totalAgents > 0 ? round((count($activeUserIds) / $totalAgents) * 100, 1) : 0.0, + 'is_exempt_active' => false, + 'filter_level' => '', + 'filter_type' => 'all', + ]; $roleList[] = $allTotalRow; $roleStats = []; @@ -270,6 +347,10 @@ class Dashboard extends Common $roleStats[$rItem['role_key']] = $rItem; } + $activePartnersCount = count($activeUserIds); + $inactiveCount = max(0, $totalAgents - $activePartnersCount); + $activeRate = $totalAgents > 0 ? round(($activePartnersCount / $totalAgents) * 100, 1) : 0.0; + $partnerNetwork = [ 'total_registered_users' => $totalRegisteredUsers, 'total_agents' => $totalAgents, @@ -279,14 +360,14 @@ class Dashboard extends Common 'maker_levels' => $makerLevels, 'venue_count' => $venueCount, 'county_count' => $countyCount, - 'active_count' => $activeCount, + 'active_count' => $activePartnersCount, 'inactive_count' => $inactiveCount, 'active_rate' => $activeRate, 'role_list' => $roleList, 'role_stats' => $roleStats, ]; - // 8. CFO 视角:资金浮存、提现出款风控与资产负债中枢 + // 8. CFO 视角:平台资金池结余、刚性法币流动负债与提现风控 $userCommissionFloat = (float) User::sum('commission'); $userMoneyFloat = (float) User::sum('money'); $totalFloatFunds = round($userCommissionFloat + $userMoneyFloat, 2); @@ -295,40 +376,39 @@ class Dashboard extends Common $totalWithdrawPaid = (float) Withdraw::where('status', 2)->sum('paid_fee'); $pendingWithdrawAmount = (float) Withdraw::where('status', 0)->sum('amount'); $pendingWithdrawCount = (int) Withdraw::where('status', 0)->count(); - $withdrawCashRate = $totalPayout > 0 ? round(($totalWithdrawPaid / $totalPayout) * 100, 2) : 0.0; - $totalRefundAmount = (float) Refund::where('status', 'completed')->sum('refund_fee'); - $totalRefundCount = (int) Refund::where('status', 'completed')->count(); - $refundRate = $totalGmv > 0 ? round(($totalRefundAmount / $totalGmv) * 100, 2) : 0.0; + // 提现审核完成率 = 已打款出款 / 累计申请总额 + $withdrawAuditRate = $totalWithdrawApplied > 0 ? round(($totalWithdrawPaid / $totalWithdrawApplied) * 100, 2) : 0.0; + // 佣金提现转化率 = 已打款出款 / 累计产生分成 + $commissionCashRate = $totalPayout > 0 ? round(($totalWithdrawPaid / $totalPayout) * 100, 2) : 0.0; + + // 平台商户号可用流动资金结余 = 实付总额 - 累计退款 - 已打款出款 $realNetCashFlow = round($totalGmv - $totalRefundAmount - $totalWithdrawPaid, 2); - // 未来总承兑责任 (待释放消费券 + 马豆折算市值 + 用户未提现佣金) - $pendingVoucherLiability = $pendingVoucher; - $madowMarketLiability = $totalMadowCny; - $commissionLiability = $userCommissionFloat; - $totalFutureLiability = round($pendingVoucherLiability + $madowMarketLiability + $commissionLiability, 2); + // 平台刚性法币现金流动负债 = 用户未提现佣金 + 用户账户可用余额 + 待审核打款提现 + $rigidCashLiability = round($userCommissionFloat + $userMoneyFloat + $pendingWithdrawAmount, 2); $financeOverview = [ 'commission_float' => number_format($userCommissionFloat, 2, '.', ''), 'user_balance_float' => number_format($userMoneyFloat, 2, '.', ''), 'total_float_funds' => number_format($totalFloatFunds, 2, '.', ''), - 'total_withdraw_applied' => number_format($totalWithdrawApplied, 2, '.', ''), - 'total_withdraw_paid' => number_format($totalWithdrawPaid, 2, '.', ''), 'pending_withdraw_amount' => number_format($pendingWithdrawAmount, 2, '.', ''), 'pending_withdraw_count' => $pendingWithdrawCount, - 'withdraw_cash_rate' => $withdrawCashRate, + 'rigid_cash_liability' => number_format($rigidCashLiability, 2, '.', ''), // 刚性法币流动负债 + 'total_withdraw_applied' => number_format($totalWithdrawApplied, 2, '.', ''), + 'total_withdraw_paid' => number_format($totalWithdrawPaid, 2, '.', ''), + 'withdraw_audit_rate' => $withdrawAuditRate, // 提现审核完成率 + 'commission_cash_rate' => $commissionCashRate, // 佣金提现转化率 'total_refund_amount' => number_format($totalRefundAmount, 2, '.', ''), 'total_refund_count' => $totalRefundCount, 'refund_rate' => $refundRate, - 'real_net_cash_flow' => number_format($realNetCashFlow, 2, '.', ''), - 'pending_voucher_liability' => number_format($pendingVoucherLiability, 2, '.', ''), - 'madow_market_liability' => number_format($madowMarketLiability, 2, '.', ''), - 'commission_liability' => number_format($commissionLiability, 2, '.', ''), - 'total_future_liability' => number_format($totalFutureLiability, 2, '.', ''), + 'real_net_cash_flow' => number_format($realNetCashFlow, 2, '.', ''), // 真实资金留存 + // 生态非现金权益存量(清晰标注) + 'madow_market_val' => number_format($totalMadowCny, 2, '.', ''), + 'pending_voucher_quota' => number_format($pendingVoucher, 2, '.', ''), ]; - // 9. COO 视角:转化复购漏斗、合伙人30天掉队雷达与供应链库存安全 - // 9.1 用户购买频次聚合与转化漏斗 + // 9. COO 视角:转化复购漏斗、合伙人自购续期雷达、供应链库存与实时待办看板 $paidOrderUsers = Order::whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED]) ->group('user_id') ->field('user_id, count(*) as buy_count') @@ -346,22 +426,16 @@ class Dashboard extends Common } $payConversionRate = $totalRegisteredUsers > 0 ? round(($paidUsersCount / $totalRegisteredUsers) * 100, 1) : 0.0; $repeatPurchaseRate = $paidUsersCount > 0 ? round(($repeatBuyCount / $paidUsersCount) * 100, 1) : 0.0; - $arpu = $paidUsersCount > 0 ? round($totalGmv / $paidUsersCount, 2) : 0.0; - - // 9.2 合伙人 30 天自购断档掉队雷达(安全活跃、7天即将断档预警、已断档失效) - $now = time(); - $activeDays = (int) PartnerConfig::get('partner_active_days', 30); - $warningThresholdDays = 7; - $activeWindow = $now - ($activeDays * 86400); - $warningWindow = $now - (($activeDays - $warningThresholdDays) * 86400); + $arpu = $paidUsersCount > 0 ? round($netGmv / $paidUsersCount, 2) : 0.0; + // 9.2 合伙人 30 天自购续期雷达 (客观四态分类,避免恐慌误判) $activePartnersCount = 0; $warningPartnersCount = 0; - $frozenPartnersCount = 0; + $pendingRenewCount = 0; + $newNoOrderCount = 0; $warningList = []; - $frozenList = []; + $pendingRenewList = []; - // 批量拉取合伙人用户信息,避免循环内查询 $agentUserIds = array_column(collection($allAgents)->toArray(), 'user_id'); $agentUserMap = []; if (!empty($agentUserIds)) { @@ -372,8 +446,7 @@ class Dashboard extends Common } foreach ($allAgents as $ag) { - $lastTime = (int) $ag->getData('last_self_order_time'); - $uid = $ag->user_id; + $uid = (int) $ag->user_id; $u = $agentUserMap[$uid] ?? null; $nickname = $u ? ($u->nickname ?: '用户#' . $uid) : ('用户#' . $uid); $mobile = ($u && $u->mobile) ? substr_replace($u->mobile, '****', 3, 4) : '-'; @@ -381,36 +454,46 @@ class Dashboard extends Common $identityData = IdentityService::build($ag); $identityText = $identityData['identity_text'] ?: ('Lv.' . $ag->level); - if ($lastTime > 0 && $lastTime >= $warningWindow) { - // 安全活跃期 (距今 < 23天) - $activePartnersCount++; - } elseif ($lastTime > 0 && $lastTime >= $activeWindow) { - // 7 天内即将断档预警 (距今 23~30天) - $warningPartnersCount++; - $daysLeft = max(1, (int) ceil(($lastTime + $activeDays * 86400 - $now) / 86400)); - $warningList[] = [ - 'user_id' => $uid, - 'nickname' => $nickname, - 'avatar' => $avatar, - 'mobile' => $mobile, - 'level_text' => $identityText, - 'last_order_date' => date('Y-m-d H:i', $lastTime), - 'days_left' => $daysLeft, - ]; - } else { - // 已断档冻结 (> 30天 或 从未自购) - $frozenPartnersCount++; - if (count($frozenList) < 10) { - $overdueDays = $lastTime > 0 ? max(1, (int) floor(($now - $lastTime) / 86400) - $activeDays) : 999; - $frozenList[] = [ + $lastTime = (int) $ag->getData('last_self_order_time'); + $isActive = isset($activeUserMap[$uid]); + + if ($isActive) { + if ($lastTime > 0 && $lastTime < $warningWindowStart) { + // 临近续期预警 (1~7天内需续期自购) + $warningPartnersCount++; + $daysLeft = max(1, (int) ceil(($lastTime + $activeDays * 86400 - $now) / 86400)); + $warningList[] = [ 'user_id' => $uid, 'nickname' => $nickname, 'avatar' => $avatar, 'mobile' => $mobile, 'level_text' => $identityText, - 'last_order_date' => $lastTime > 0 ? date('Y-m-d H:i', $lastTime) : '从未自购', - 'overdue_days' => $overdueDays == 999 ? '从未自购' : "逾期 {$overdueDays} 天", + 'last_order_date' => date('Y-m-d H:i', $lastTime), + 'days_left' => $daysLeft, ]; + } else { + // 安全活跃达标 (>7天余量) + $activePartnersCount++; + } + } else { + if ($lastTime > 0) { + // 待续期激活 (已超30天未自购,自购1单即可恢复极差资格) + $pendingRenewCount++; + if (count($pendingRenewList) < 10) { + $overdueDays = max(1, (int) floor(($now - $lastTime) / 86400) - $activeDays); + $pendingRenewList[] = [ + 'user_id' => $uid, + 'nickname' => $nickname, + 'avatar' => $avatar, + 'mobile' => $mobile, + 'level_text' => $identityText, + 'last_order_date' => date('Y-m-d H:i', $lastTime), + 'overdue_text' => "超期 {$overdueDays} 天待续", + ]; + } + } else { + // 新成员待自购首购 (尚未下单) + $newNoOrderCount++; } } } @@ -449,68 +532,60 @@ class Dashboard extends Common ]; } - // 9.4 平台经营健康度评分模型 (满分 100) - // ① 拨比健康度 (30分) - $payoutScore = 30.0; - if ($totalGmv > 0) { - if ($payoutRate > 60) { - $payoutScore = 10.0; - } elseif ($payoutRate > 40) { - $payoutScore = 20.0; - } elseif ($payoutRate < 15) { - $payoutScore = 25.0; - } else { - $payoutScore = 30.0; - } - } - // ② 合伙人活跃健康度 (25分) - $partnerActiveRate = $totalAgents > 0 ? ($activePartnersCount / $totalAgents) : 0; - $activeScore = round($partnerActiveRate * 25, 1); - // ③ 用户复购粘性 (25分, 基准30%得满分) - $repeatScore = $paidUsersCount > 0 ? min(25.0, round(($repeatPurchaseRate / 30) * 25, 1)) : 15.0; - // ④ 资金履约安全度 (20分) - $safetyScore = 20.0; - if ($refundRate > 5.0) { - $safetyScore = 10.0; - } elseif ($refundRate > 2.0) { - $safetyScore = 15.0; - } else { - $safetyScore = 20.0; - } + // 9.4 平台客观风控指标看板 (客观真实区间,替换主观伪评分) + $riskIndicators = [ + 'payout_rate' => [ + 'val' => $payoutRate, + 'text' => $payoutRate <= 35 ? '精算安全 (≤35%)' : ($payoutRate <= 50 ? '轻度偏高 (35%~50%)' : '需重点关注 (>50%)'), + 'status' => $payoutRate <= 35 ? 'success' : ($payoutRate <= 50 ? 'warning' : 'danger'), + ], + 'partner_active_rate' => [ + 'val' => $activeRate, + 'text' => "达标率 {$activeRate}%", + 'status' => $activeRate >= 50 ? 'success' : 'warning', + ], + 'withdraw_audit_rate' => [ + 'val' => $withdrawAuditRate, + 'text' => "出款率 {$withdrawAuditRate}%", + 'status' => $withdrawAuditRate >= 90 ? 'success' : 'warning', + ], + 'refund_rate' => [ + 'val' => $refundRate, + 'text' => $refundRate <= 2.0 ? '退款极低 (≤2%)' : ($refundRate <= 5.0 ? '正常可控 (2%~5%)' : '退款偏高 (>5%)'), + 'status' => $refundRate <= 2.0 ? 'success' : ($refundRate <= 5.0 ? 'warning' : 'danger'), + ], + ]; - $healthTotalScore = round($payoutScore + $activeScore + $repeatScore + $safetyScore, 1); - $healthGrade = 'S·卓越'; - $healthGradeColor = '#52c41a'; - if ($healthTotalScore < 70) { - $healthGrade = 'C·预警'; - $healthGradeColor = '#f5222d'; - } elseif ($healthTotalScore < 80) { - $healthGrade = 'B·稳健'; - $healthGradeColor = '#fa8c16'; - } elseif ($healthTotalScore < 90) { - $healthGrade = 'A·良好'; - $healthGradeColor = '#1890ff'; - } - - $healthSuggestions = []; + $riskSuggestions = []; if ($payoutRate <= 35) { - $healthSuggestions[] = "当前综合拨比为 {$payoutRate}%,处于精算设定的安全区间,净现金流沉淀稳固。"; + $riskSuggestions[] = "综合拨比 {$payoutRate}%,处于精算安全区间,净利润沉淀稳固。"; + } elseif ($payoutRate <= 50) { + $riskSuggestions[] = "综合拨比 {$payoutRate}%,略高于基础基准,建议提升高毛利商品动销比例。"; } else { - $healthSuggestions[] = "当前综合拨比为 {$payoutRate}%,略高于基准安全线,建议关注毛利率较高的商品排期。"; + $riskSuggestions[] = "综合拨比 {$payoutRate}% 超出常规安全线,请复核极差与日结各奖项拨出参数。"; } if ($warningPartnersCount > 0) { - $healthSuggestions[] = "监测到有 {$warningPartnersCount} 位合伙人进入 7 天内即将断档预警期,建议及时定向唤醒催单。"; - } else { - $healthSuggestions[] = "合伙股东团队自购状态良好,暂无 7 天内即将断档的合伙人。"; - } - if ($lowStockCount > 0) { - $healthSuggestions[] = "仓储有 {$lowStockCount} 款商品 SKU 库存告急(≤10件),请及时补充库存避免断货缺货。"; - } else { - $healthSuggestions[] = "在售商品库存充沛,供应链运转健康。"; + $riskSuggestions[] = "有 {$warningPartnersCount} 位合伙人进入 7 天临近到期预警期,建议及时定向提醒自购续期。"; } if ($pendingWithdrawCount > 0) { - $healthSuggestions[] = "当前有 {$pendingWithdrawCount} 笔提现申请等待审核(¥{$financeOverview['pending_withdraw_amount']}),请及时审核保障出款体验。"; + $riskSuggestions[] = "当前有 {$pendingWithdrawCount} 笔提现申请等待审核(¥{$financeOverview['pending_withdraw_amount']}),请及时处理。"; } + if ($lowStockCount > 0) { + $riskSuggestions[] = "仓储有 {$lowStockCount} 款商品 SKU 库存告急(≤10件),请及时补充库存。"; + } + + // 9.5 全局实时待办看板 (100% 实时统计,不受日期选择框影响,杜绝漏单漏审) + $realtimeNosend = (int) Order::pretendPaid()->nosend()->count(); + $realtimeAftersale = (int) Aftersale::whereIn('aftersale_status', [Aftersale::AFTERSALE_STATUS_NOOPER, Aftersale::AFTERSALE_STATUS_ING])->count(); + $realtimeRefundApply = (int) Order::paid()->where('apply_refund_status', Order::APPLY_REFUND_STATUS_APPLY)->count(); + + $todoOverview = [ + 'nosend_count' => $realtimeNosend, + 'aftersale_count' => $realtimeAftersale, + 'refund_apply_count' => $realtimeRefundApply, + 'pending_withdraw_count' => $pendingWithdrawCount, + 'low_stock_count' => $lowStockCount, + ]; $operationOverview = [ 'total_registered' => $totalRegisteredUsers, @@ -522,23 +597,17 @@ class Dashboard extends Common 'arpu' => number_format($arpu, 2, '.', ''), 'active_partners' => $activePartnersCount, 'warning_partners' => $warningPartnersCount, - 'frozen_partners' => $frozenPartnersCount, + 'pending_renew_partners' => $pendingRenewCount, + 'new_no_order_partners' => $newNoOrderCount, 'warning_list' => $warningList, - 'frozen_list' => $frozenList, + 'pending_renew_list' => $pendingRenewList, 'on_sale_goods_count' => $onSaleGoodsCount, 'total_stock' => $totalStock, 'low_stock_count' => $lowStockCount, 'low_stock_list' => $lowStockFormatted, - 'health' => [ - 'score' => $healthTotalScore, - 'grade' => $healthGrade, - 'grade_color' => $healthGradeColor, - 'payout_score' => $payoutScore, - 'active_score' => $activeScore, - 'repeat_score' => $repeatScore, - 'safety_score' => $safetyScore, - 'suggestions' => $healthSuggestions, - ] + 'risk_indicators' => $riskIndicators, + 'risk_suggestions' => $riskSuggestions, + 'todo_overview' => $todoOverview, ]; $this->success('获取成功', null, [ @@ -563,117 +632,79 @@ class Dashboard extends Common if (empty($date) || count($date) < 2) { $date = [date('Y-m-d 00:00:00'), date('Y-m-d 23:59:59')]; } + $startTimestamp = strtotime($date[0]); + $endTimestamp = strtotime($date[1]); - $orders = Order::with(['items'])->whereTime('createtime', 'between', $date) - ->order('id', 'asc')->select(); + // 1. 时段内支付订单 (以支付时间为准,与分成支出时间严格对齐) + $paidOrders = Order::with(['items']) + ->paid() + ->whereRaw("((paid_time >= {$startTimestamp} AND paid_time <= {$endTimestamp}) OR ((paid_time IS NULL OR paid_time = 0) AND createtime >= {$startTimestamp} AND createtime <= {$endTimestamp}))") + ->order('id', 'asc') + ->select(); - // 订单数 - $data['orderNum'] = count($orders); + // 2. 时段内下单订单数 (反映下单活跃度) + $createdOrders = Order::whereTime('createtime', 'between', $date) + ->order('id', 'asc') + ->select(); + + $data['orderNum'] = count($createdOrders); $data['orderArr'] = []; + foreach ($createdOrders as $co) { + $rawCt = $co->getData('createtime'); + $cTime = (is_numeric($rawCt) ? intval($rawCt) : strtotime($rawCt)) * 1000; + $data['orderArr'][] = [ + 'counter' => 1, + 'createtime' => $cTime, + 'user_id' => $co->user_id, + ]; + } - // 支付订单(包含退款的订单, 不包含货到付款还未收货(未支付)订单) - $data['payOrderNum'] = 0; + // 3. 支付统计 + $data['payOrderNum'] = count($paidOrders); $data['payOrderArr'] = []; - // 支付金额(包含退款的订单, 不包含货到付款还未收货(未支付)订单) - $data['payAmountNum'] = 0; + $data['payAmountNum'] = '0.00'; $data['payAmountArr'] = []; - - // 支付用户 $userIds = []; $data['payUserNum'] = 0; $data['payUserArr'] = []; - // 待发货(包含货到付款) - $data['noSendNum'] = 0; - $data['noSendArr'] = []; - // 售后维权 - $data['aftersaleNum'] = 0; - $data['aftersaleArr'] = []; - // 退款订单 - $data['refundNum'] = 0; - $data['refundArr'] = []; - - foreach ($orders as $key => $order) { - $data['orderArr'][] = [ - 'counter' => 1, - 'createtime' => $order->getData('createtime') * 1000, - 'user_id' => $order->user_id - ]; - - // 已支付的,不包含货到付款未支付的 - if (in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED])) { - $data['payOrderNum']++; - - $data['payOrderArr'][] = [ - 'counter' => 1, - 'createtime' => $order->getData('createtime') * 1000, - 'user_id' => $order->user_id - ]; - - // 支付金额 - $data['payAmountNum'] = bcadd((string)$data['payAmountNum'], $order->pay_fee, 2); - - $data['payAmountArr'][] = [ - 'counter' => (float) $order->pay_fee, - 'createtime' => $order->getData('createtime') * 1000, - ]; - - // 下单用户 - if (!in_array($order->user_id, $userIds)) { - $data['payUserNum']++; - $data['payUserArr'][] = [ - 'counter' => 1, - 'createtime' => $order->getData('createtime') * 1000, - 'user_id' => $order->user_id - ]; - } + foreach ($paidOrders as $order) { + $rawPaid = $order->getData('paid_time'); + $rawCt = $order->getData('createtime'); + $oTimestamp = 0; + if ($rawPaid) { + $oTimestamp = is_numeric($rawPaid) ? intval($rawPaid) : strtotime($rawPaid); + } elseif ($rawCt) { + $oTimestamp = is_numeric($rawCt) ? intval($rawCt) : strtotime($rawCt); } - - // 已支付的和货到付款未支付的 - if (in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED]) || $order->isOffline($order)) { - $flagnoSend = false; - $flagaftersale = false; - $flagrefund = false; - $aftersaleIng = false; - foreach ($order->items as $k => $item) { - if ( - !$flagnoSend - && $item->dispatch_status == OrderItem::DISPATCH_STATUS_NOSEND - && $item->refund_status == OrderItem::REFUND_STATUS_NOREFUND - && in_array($order->apply_refund_status, [ - Order::APPLY_REFUND_STATUS_NOAPPLY, - Order::APPLY_REFUND_STATUS_REFUSE - ]) - ) { - $flagnoSend = true; - } - - if ( - $item->aftersale_status == OrderItem::AFTERSALE_STATUS_ING - && $item->dispatch_status == OrderItem::DISPATCH_STATUS_NOSEND - && $item->refund_status == OrderItem::REFUND_STATUS_NOREFUND - ) { - $aftersaleIng = true; - } - - if (!$flagaftersale && $item->aftersale_status != OrderItem::AFTERSALE_STATUS_NOAFTER) { - $data['aftersaleNum']++; - $flagaftersale = true; - } - - if (!$flagrefund && $item->refund_status > OrderItem::REFUND_STATUS_NOREFUND) { - $data['refundNum']++; - $flagrefund = true; - } - } - - if (!$aftersaleIng && $flagnoSend) { - $data['noSendNum']++; - } + $pTime = $oTimestamp * 1000; + $data['payOrderArr'][] = [ + 'counter' => 1, + 'createtime' => $pTime, + 'user_id' => $order->user_id, + ]; + $data['payAmountNum'] = bcadd((string)$data['payAmountNum'], (string)$order->pay_fee, 2); + $data['payAmountArr'][] = [ + 'counter' => (float) $order->pay_fee, + 'createtime' => $pTime, + ]; + if (!in_array($order->user_id, $userIds)) { + $userIds[] = $order->user_id; + $data['payUserNum']++; + $data['payUserArr'][] = [ + 'counter' => 1, + 'createtime' => $pTime, + 'user_id' => $order->user_id, + ]; } } - // ================= 财富与收益分成时段穿透 (新增) ================= + // 全局实时待办数据 (不受时段过滤影响,彻底杜绝漏单漏审) + $data['noSendNum'] = (int) Order::pretendPaid()->nosend()->count(); + $data['aftersaleNum'] = (int) Aftersale::whereIn('aftersale_status', [Aftersale::AFTERSALE_STATUS_NOOPER, Aftersale::AFTERSALE_STATUS_ING])->count(); + $data['refundNum'] = (int) Order::paid()->where('apply_refund_status', Order::APPLY_REFUND_STATUS_APPLY)->count(); + + // 4. 收益分成时段穿透 (以分成入账时间为准) $rewards = Reward::where('status', 1) ->whereTime('createtime', 'between', $date) ->order('id', 'asc') @@ -746,9 +777,11 @@ class Dashboard extends Common if ($amt > 0) { $payoutTotal = round($payoutTotal + $amt, 2); + $rCt = $reward->getData('createtime'); + $rTime = (is_numeric($rCt) ? intval($rCt) : strtotime($rCt)) * 1000; $payoutArr[] = [ 'counter' => $amt, - 'createtime' => $reward->getData('createtime') * 1000, + 'createtime' => $rTime, 'type' => $rwType, ]; @@ -800,27 +833,47 @@ class Dashboard extends Common public function ranking() { - // 1. 商品销量排行 (原有) - $goods = Goods::limit(5)->order('sales', 'desc')->select(); - foreach ($goods as $key => $gd) { - $gd->append(['real_sales']); - $result = OrderItem::field('sum(goods_num * goods_price) as sale_total_money')->where('goods_id', $gd['id']) - ->whereExists(function ($query) use ($gd) { - $order_table_name = (new Order())->getQuery()->getTable(); - $table_name = (new OrderItem())->getQuery()->getTable(); + // 1. 商品真实销量排行 (基于真实已支付订单聚合,杜绝后台虚拟销量刷榜误导) + $goodsRanking = \think\Db::name('shopro_order_item')->alias('oi') + ->join('shopro_order o', 'oi.order_id = o.id') + ->whereIn('o.status', [Order::STATUS_PAID, Order::STATUS_COMPLETED]) + ->field('oi.goods_id, sum(oi.goods_num) as real_sales, sum(oi.goods_num * oi.goods_price) as sale_total_money') + ->group('oi.goods_id') + ->order('real_sales', 'desc') + ->limit(5) + ->select(); - $query->table($order_table_name)->where($table_name . '.order_id=' . $order_table_name . '.id') - ->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED]); // 已支付的订单 - })->find(); - - $gd['sale_total_money'] = $result['sale_total_money'] ?: 0; + $goods = []; + if (!empty($goodsRanking)) { + $goodsIds = array_column($goodsRanking, 'goods_id'); + $goodsModels = Goods::whereIn('id', $goodsIds)->select(); + $goodsMap = []; + foreach ($goodsModels as $gm) { + $goodsMap[$gm->id] = $gm; + } + foreach ($goodsRanking as $gr) { + $gid = $gr['goods_id']; + $gItem = $goodsMap[$gid] ?? null; + if ($gItem) { + $gItem->real_sales = (int) $gr['real_sales']; + $gItem->sale_total_money = (float) $gr['sale_total_money']; + $goods[] = $gItem; + } + } + } + if (empty($goods)) { + $goods = Goods::where('status', 'up')->limit(5)->order('id', 'desc')->select(); + foreach ($goods as $gd) { + $gd->real_sales = 0; + $gd->sale_total_money = 0; + } } - // 2. 热搜排行 (原有) + // 2. 热搜排行 $searchHistory = new SearchHistory(); $hotSearch = $searchHistory->hotSearch(); - // 3. 合伙股东收益排行 TOP 5 (新增) + // 3. 合伙股东收益排行 TOP 5 $partnerRanking = []; $rankingRows = Reward::field('user_id, sum(amount) as total_income, count(*) as reward_count') ->where('status', 1) @@ -848,7 +901,7 @@ class Dashboard extends Common ]; } - // 4. 最新日结概览 (新增) + // 4. 最新日结概览 $dailyReport = DailyReport::order('settle_date', 'desc')->find(); $recentReport = null; if ($dailyReport) { diff --git a/application/admin/view/shopro/dashboard/index.html b/application/admin/view/shopro/dashboard/index.html index dd64973..dbc28db 100644 --- a/application/admin/view/shopro/dashboard/index.html +++ b/application/admin/view/shopro/dashboard/index.html @@ -340,18 +340,18 @@
商城实收流水 (GMV) - 实付总额 + 实付到账
¥{{ state.wealth.total_gmv }}
- 今日实收:¥{{ state.wealth.today_gmv }} - 客单均价:¥{{ state.wealth.avg_order_amount }} + 今日实付:¥{{ state.wealth.today_gmv }} + 有效净GMV:¥{{ state.wealth.net_gmv }}
@@ -361,8 +361,8 @@
现金分成支出与拨比 - - + + 拨比 {{ state.wealth.payout_rate }}% @@ -371,7 +371,7 @@ ¥{{ state.wealth.total_payout }}
- 平台净留存:¥{{ state.wealth.net_retention }} + 平台有效留存:¥{{ state.wealth.net_retention }} 明细 →