feat(dashboard): 增加CFO财务资金浮存与资产负债中枢、COO运营转化复购与合伙人断档雷达全貌分析
这个提交包含在:
@@ -16,6 +16,9 @@ use addons\shopro\service\SearchHistory;
|
||||
use addons\shopro\service\partner\RateService;
|
||||
use addons\shopro\service\partner\IdentityService;
|
||||
use addons\shopro\service\partner\PartnerConfig;
|
||||
use app\admin\model\shopro\Withdraw;
|
||||
use app\admin\model\shopro\Refund;
|
||||
use app\admin\model\shopro\goods\SkuPrice as SkuPriceModel;
|
||||
|
||||
class Dashboard extends Common
|
||||
{
|
||||
@@ -283,6 +286,261 @@ class Dashboard extends Common
|
||||
'role_stats' => $roleStats,
|
||||
];
|
||||
|
||||
// 8. CFO 视角:资金浮存、提现出款风控与资产负债中枢
|
||||
$userCommissionFloat = (float) User::sum('commission');
|
||||
$userMoneyFloat = (float) User::sum('money');
|
||||
$totalFloatFunds = round($userCommissionFloat + $userMoneyFloat, 2);
|
||||
|
||||
$totalWithdrawApplied = (float) Withdraw::sum('amount');
|
||||
$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;
|
||||
$realNetCashFlow = round($totalGmv - $totalRefundAmount - $totalWithdrawPaid, 2);
|
||||
|
||||
// 未来总承兑责任 (待释放消费券 + 马豆折算市值 + 用户未提现佣金)
|
||||
$pendingVoucherLiability = $pendingVoucher;
|
||||
$madowMarketLiability = $totalMadowCny;
|
||||
$commissionLiability = $userCommissionFloat;
|
||||
$totalFutureLiability = round($pendingVoucherLiability + $madowMarketLiability + $commissionLiability, 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,
|
||||
'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, '.', ''),
|
||||
];
|
||||
|
||||
// 9. COO 视角:转化复购漏斗、合伙人30天掉队雷达与供应链库存安全
|
||||
// 9.1 用户购买频次聚合与转化漏斗
|
||||
$paidOrderUsers = Order::whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED])
|
||||
->group('user_id')
|
||||
->field('user_id, count(*) as buy_count')
|
||||
->select();
|
||||
|
||||
$paidUsersCount = count($paidOrderUsers);
|
||||
$singleBuyCount = 0;
|
||||
$repeatBuyCount = 0;
|
||||
foreach ($paidOrderUsers as $buyer) {
|
||||
if ((int)$buyer->buy_count > 1) {
|
||||
$repeatBuyCount++;
|
||||
} else {
|
||||
$singleBuyCount++;
|
||||
}
|
||||
}
|
||||
$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);
|
||||
|
||||
$activePartnersCount = 0;
|
||||
$warningPartnersCount = 0;
|
||||
$frozenPartnersCount = 0;
|
||||
$warningList = [];
|
||||
$frozenList = [];
|
||||
|
||||
// 批量拉取合伙人用户信息,避免循环内查询
|
||||
$agentUserIds = array_column(collection($allAgents)->toArray(), 'user_id');
|
||||
$agentUserMap = [];
|
||||
if (!empty($agentUserIds)) {
|
||||
$agentUserRows = User::whereIn('id', $agentUserIds)->field('id, nickname, mobile, avatar')->select();
|
||||
foreach ($agentUserRows as $uRow) {
|
||||
$agentUserMap[$uRow->id] = $uRow;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($allAgents as $ag) {
|
||||
$lastTime = (int) $ag->getData('last_self_order_time');
|
||||
$uid = $ag->user_id;
|
||||
$u = $agentUserMap[$uid] ?? null;
|
||||
$nickname = $u ? ($u->nickname ?: '用户#' . $uid) : ('用户#' . $uid);
|
||||
$mobile = ($u && $u->mobile) ? substr_replace($u->mobile, '****', 3, 4) : '-';
|
||||
$avatar = $u ? ($u->avatar ?: '') : '';
|
||||
$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[] = [
|
||||
'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} 天",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 9.3 供应链库存安全与动销商品
|
||||
$onSaleGoodsCount = (int) Goods::where('status', 'up')->count();
|
||||
$totalStock = (int) SkuPriceModel::where('status', 'up')->sum('stock');
|
||||
$lowStockCount = (int) SkuPriceModel::alias('sp')
|
||||
->join('shopro_goods g', 'sp.goods_id = g.id')
|
||||
->where('g.status', 'up')
|
||||
->where('sp.status', 'up')
|
||||
->where('sp.stock', '<=', 10)
|
||||
->count();
|
||||
|
||||
$lowStockSkuList = SkuPriceModel::alias('sp')
|
||||
->join('shopro_goods g', 'sp.goods_id = g.id')
|
||||
->where('g.status', 'up')
|
||||
->where('sp.status', 'up')
|
||||
->where('sp.stock', '<=', 10)
|
||||
->field('sp.id as sku_id, sp.goods_id, g.title as goods_title, g.image as goods_image, sp.goods_sku_text, sp.stock, sp.price')
|
||||
->order('sp.stock', 'asc')
|
||||
->limit(10)
|
||||
->select();
|
||||
|
||||
$lowStockFormatted = [];
|
||||
foreach ($lowStockSkuList as $lsku) {
|
||||
$skuText = is_array($lsku->goods_sku_text) ? implode(' ', $lsku->goods_sku_text) : ($lsku->goods_sku_text ?: '默认单规格');
|
||||
$lowStockFormatted[] = [
|
||||
'sku_id' => $lsku->sku_id,
|
||||
'goods_id' => $lsku->goods_id,
|
||||
'goods_title' => $lsku->goods_title,
|
||||
'goods_image' => $lsku->goods_image,
|
||||
'sku_text' => $skuText,
|
||||
'stock' => (int) $lsku->stock,
|
||||
'price' => $lsku->price,
|
||||
];
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
$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 = [];
|
||||
if ($payoutRate <= 35) {
|
||||
$healthSuggestions[] = "当前综合拨比为 {$payoutRate}%,处于精算设定的安全区间,净现金流沉淀稳固。";
|
||||
} else {
|
||||
$healthSuggestions[] = "当前综合拨比为 {$payoutRate}%,略高于基准安全线,建议关注毛利率较高的商品排期。";
|
||||
}
|
||||
if ($warningPartnersCount > 0) {
|
||||
$healthSuggestions[] = "监测到有 {$warningPartnersCount} 位合伙人进入 7 天内即将断档预警期,建议及时定向唤醒催单。";
|
||||
} else {
|
||||
$healthSuggestions[] = "合伙股东团队自购状态良好,暂无 7 天内即将断档的合伙人。";
|
||||
}
|
||||
if ($lowStockCount > 0) {
|
||||
$healthSuggestions[] = "仓储有 {$lowStockCount} 款商品 SKU 库存告急(≤10件),请及时补充库存避免断货缺货。";
|
||||
} else {
|
||||
$healthSuggestions[] = "在售商品库存充沛,供应链运转健康。";
|
||||
}
|
||||
if ($pendingWithdrawCount > 0) {
|
||||
$healthSuggestions[] = "当前有 {$pendingWithdrawCount} 笔提现申请等待审核(¥{$financeOverview['pending_withdraw_amount']}),请及时审核保障出款体验。";
|
||||
}
|
||||
|
||||
$operationOverview = [
|
||||
'total_registered' => $totalRegisteredUsers,
|
||||
'paid_users_count' => $paidUsersCount,
|
||||
'pay_conversion_rate' => $payConversionRate,
|
||||
'single_buy_users_count' => $singleBuyCount,
|
||||
'repeat_buy_users_count' => $repeatBuyCount,
|
||||
'repeat_purchase_rate' => $repeatPurchaseRate,
|
||||
'arpu' => number_format($arpu, 2, '.', ''),
|
||||
'active_partners' => $activePartnersCount,
|
||||
'warning_partners' => $warningPartnersCount,
|
||||
'frozen_partners' => $frozenPartnersCount,
|
||||
'warning_list' => $warningList,
|
||||
'frozen_list' => $frozenList,
|
||||
'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,
|
||||
]
|
||||
];
|
||||
|
||||
$this->success('获取成功', null, [
|
||||
'user_data' => $userData,
|
||||
'agent_data' => $agentData ?? null,
|
||||
@@ -291,6 +549,8 @@ class Dashboard extends Common
|
||||
'madow_data' => $madowData,
|
||||
'voucher_data' => $voucherData,
|
||||
'partner_network' => $partnerNetwork,
|
||||
'finance_overview' => $financeOverview,
|
||||
'operation_overview' => $operationOverview,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户