文件
fenleixinxi/application/admin/controller/fc/Dashboard.php
MiAn 5b616634ab feat: 重构运营看板,新增多维度数据展示与交互功能
新增同城便民生活服务平台标识,重构看板为业务总览与运营看板
新增待办督办中心、营收构成分析、热门分类与区域排行等模块
新增近7/14/30天多时段趋势切换,支持发布量/订单额/用户/曝光量指标切换
新增平台商业与生态资产统计模块,新增待办预警与空状态提示
新增趋势数据汇总与明细表联动,优化响应式布局与交互样式
新增商家管理、举报投诉等语言翻译项,新增登录推荐图
2026-10-30 17:39:15 +08:00

341 行
15 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\admin\controller\fc;
use app\common\controller\Backend;
use think\Db;
/**
* 同城分类信息 - 数据总览与运营看板
*
* 后台核心运营仪表盘:
* 1. 今日核心指标与昨日对比(发布量 / 订单额 / 新增用户 / 浏览曝光量)
* 2. 待办督办中心(帖子待审 / 商家待审 / 提现待审 / 举报待处理)
* 3. 多时段多维趋势分析(近 7 天 / 14 天 / 30 天,支持发布量、订单额、新增用户、浏览量平滑折线图)
* 4. 营收来源构成分析(会员购买 / 发帖付费 / 置顶推广 / 付费查看等占比环形图)
* 5. 热门分类发帖分布与同城区域活跃排行
* 6. 用户、会员、商户与分销生态资产全景
*
* @icon fa fa-dashboard
*/
class Dashboard extends Backend
{
/**
* 数据总览首页入口与数据下发
*
* @return string|\think\response\Json
*/
public function index()
{
$todayStart = strtotime('today');
$todayEnd = $todayStart + 86400 - 1;
$yesterdayStart = $todayStart - 86400;
$yesterdayEnd = $todayStart - 1;
/* =========================================================
* 1. 核心 KPI(今日、较昨日对比、累计总量)
* ========================================================= */
// 发布量
$todayPost = (int)Db::name('fc_post')->where('createtime', 'between', [$todayStart, $todayEnd])->count();
$yesterdayPost = (int)Db::name('fc_post')->where('createtime', 'between', [$yesterdayStart, $yesterdayEnd])->count();
$totalPost = (int)Db::name('fc_post')->count();
// 订单额与笔数(仅统计已支付订单)
$todayOrderAmount = round((float)Db::name('fc_order')->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$todayStart, $todayEnd])->sum('amount'), 2);
$yesterdayOrderAmount = round((float)Db::name('fc_order')->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$yesterdayStart, $yesterdayEnd])->sum('amount'), 2);
$totalOrderAmount = round((float)Db::name('fc_order')->where('status', 'paid')->sum('amount'), 2);
$todayOrderCount = (int)Db::name('fc_order')->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$todayStart, $todayEnd])->count();
$totalOrderCount = (int)Db::name('fc_order')->where('status', 'paid')->count();
// 用户与会员
$todayUser = (int)Db::name('user')->where('createtime', 'between', [$todayStart, $todayEnd])->count();
$yesterdayUser = (int)Db::name('user')->where('createtime', 'between', [$yesterdayStart, $yesterdayEnd])->count();
$totalUser = (int)Db::name('user')->count();
$memberCount = (int)Db::name('fc_user_ext')->where('is_member', 1)->count();
$merchantCount = (int)Db::name('fc_merchant')->count();
$recommendMerchantCount = (int)Db::name('fc_merchant')->where('recommend', 1)->count();
// 浏览量 PV
$todayViews = (int)Db::name('fc_view_log')->where('createtime', 'between', [$todayStart, $todayEnd])->count();
$yesterdayViews = (int)Db::name('fc_view_log')->where('createtime', 'between', [$yesterdayStart, $yesterdayEnd])->count();
$totalViews = (int)Db::name('fc_post')->sum('views');
$metrics = [
'todayPost' => $todayPost,
'yesterdayPost' => $yesterdayPost,
'postDelta' => $todayPost - $yesterdayPost,
'totalPost' => $totalPost,
'todayOrderAmount' => $todayOrderAmount,
'yesterdayOrderAmount' => $yesterdayOrderAmount,
'orderDelta' => round($todayOrderAmount - $yesterdayOrderAmount, 2),
'totalOrderAmount' => $totalOrderAmount,
'todayOrderCount' => $todayOrderCount,
'totalOrderCount' => $totalOrderCount,
'todayUser' => $todayUser,
'yesterdayUser' => $yesterdayUser,
'userDelta' => $todayUser - $yesterdayUser,
'totalUser' => $totalUser,
'memberCount' => $memberCount,
'merchantCount' => $merchantCount,
'recommendMerchant' => $recommendMerchantCount,
'todayViews' => $todayViews,
'yesterdayViews' => $yesterdayViews,
'viewsDelta' => $todayViews - $yesterdayViews,
'totalViews' => $totalViews,
];
/* =========================================================
* 2. 待办督办中心(待审核事项与预警)
* ========================================================= */
$pendingPost = (int)Db::name('fc_post')->where('audit_status', 0)->count();
$pendingMerchant = (int)Db::name('fc_merchant')->where('audit_status', 0)->count();
$pendingWithdraw = (int)Db::name('fc_withdraw')->where('status', 'auditing')->count();
$pendingWithdrawAmount = round((float)Db::name('fc_withdraw')->where('status', 'auditing')->sum('amount'), 2);
$pendingReport = (int)Db::name('fc_report')->where('status', 'pending')->count();
$pendingTotal = $pendingPost + $pendingMerchant + $pendingWithdraw + $pendingReport;
$pending = [
'total' => $pendingTotal,
'post' => $pendingPost,
'merchant' => $pendingMerchant,
'withdraw' => $pendingWithdraw,
'withdrawAmount' => $pendingWithdrawAmount,
'report' => $pendingReport,
];
/* =========================================================
* 3. 平台生态与分销资产
* ========================================================= */
$commissionPending = round((float)Db::name('fc_commission')->where('status', 0)->sum('amount'), 2);
$commissionSettled = round((float)Db::name('fc_commission')->where('status', 1)->sum('amount'), 2);
$distributorCount = (int)Db::name('fc_dist_relation')->distinct(true)->field('parent_id')->count();
$ecosystem = [
'commissionPending' => $commissionPending,
'commissionSettled' => $commissionSettled,
'distributorCount' => $distributorCount,
'memberRate' => $totalUser > 0 ? round(($memberCount / $totalUser) * 100, 1) : 0,
];
/* =========================================================
* 4. 营收构成分析(按订单类型统计已支付实收)
* ========================================================= */
$orderTypeLabels = [
'member' => 'VIP会员购买',
'post' => '发布信息付费',
'payview' => '付费查看电话',
'recharge' => '钱包充值',
'top' => '置顶推广套餐',
'refresh' => '信息刷新',
];
$rawRevenue = Db::name('fc_order')
->where('status', 'paid')
->fieldRaw("type, count(*) as count, sum(amount) as amount")
->group('type')
->order('amount', 'desc')
->select();
$revenueTotalSum = 0;
$revenueTypes = [];
foreach ($rawRevenue as $r) {
$amt = round((float)$r['amount'], 2);
$cnt = (int)$r['count'];
$label = isset($orderTypeLabels[$r['type']]) ? $orderTypeLabels[$r['type']] : ($r['type'] ? $r['type'] : '其他服务');
$revenueTotalSum += $amt;
$revenueTypes[] = [
'type' => $r['type'],
'label' => $label,
'count' => $cnt,
'amount' => $amt,
];
}
foreach ($revenueTypes as &$item) {
$item['percent'] = $revenueTotalSum > 0 ? round(($item['amount'] / $revenueTotalSum) * 100, 1) : 0;
}
unset($item);
/* =========================================================
* 5. 热门分类发帖分布与同城区域排行
* ========================================================= */
// 一级分类发帖分布
$rawCategories = Db::name('fc_category')
->alias('c')
->join('__FC_POST__ p', '(p.category_id = c.id OR p.category_id IN (SELECT sub.id FROM ' . config('database.prefix') . 'fc_category sub WHERE sub.pid = c.id))', 'LEFT')
->where('c.pid', 0)
->field('c.id, c.name, count(p.id) as post_count')
->group('c.id')
->order('post_count', 'desc')
->select();
$categoryTotal = 0;
$categoryRanks = [];
foreach ($rawCategories as $c) {
$cnt = (int)$c['post_count'];
$categoryTotal += $cnt;
$categoryRanks[] = [
'id' => $c['id'],
'name' => $c['name'],
'count' => $cnt,
];
}
foreach ($categoryRanks as &$cItem) {
$cItem['percent'] = $categoryTotal > 0 ? round(($cItem['count'] / $categoryTotal) * 100, 1) : 0;
}
unset($cItem);
// 同城热门地区 Top 5
$rawAreas = Db::name('fc_post')
->fieldRaw("IF(area != '', area, city) as region_name, count(*) as count")
->where("area != '' OR city != ''")
->group('region_name')
->order('count', 'desc')
->limit(5)
->select();
$areaRanks = [];
$areaMaxCount = 1;
foreach ($rawAreas as $ar) {
$cnt = (int)$ar['count'];
if ($cnt > $areaMaxCount) {
$areaMaxCount = $cnt;
}
$areaRanks[] = [
'region_name' => $ar['region_name'] ? $ar['region_name'] : '未知区域',
'count' => $cnt,
];
}
foreach ($areaRanks as &$arItem) {
$arItem['percent'] = round(($arItem['count'] / $areaMaxCount) * 100, 1);
}
unset($arItem);
/* =========================================================
* 6. 多时段趋势数据计算(近 7 天 / 14 天 / 30 天)
* ========================================================= */
$maxTrendDays = 30;
$trendStart = $todayStart - ($maxTrendDays - 1) * 86400;
// 发帖趋势
$postsMap = [];
$postsQuery = Db::name('fc_post')
->where('createtime', 'between', [$trendStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(`createtime`,'%Y-%m-%d') AS day, COUNT(*) AS num")
->group('day')
->select();
foreach ($postsQuery as $row) {
$postsMap[$row['day']] = (int)$row['num'];
}
// 订单额趋势
$ordersMap = [];
$ordersQuery = Db::name('fc_order')
->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$trendStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(IFNULL(`paytime`,`createtime`),'%Y-%m-%d') AS day, SUM(`amount`) AS num")
->group('day')
->select();
foreach ($ordersQuery as $row) {
$ordersMap[$row['day']] = round((float)$row['num'], 2);
}
// 新增用户趋势
$usersMap = [];
$usersQuery = Db::name('user')
->where('createtime', 'between', [$trendStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(`createtime`,'%Y-%m-%d') AS day, COUNT(*) AS num")
->group('day')
->select();
foreach ($usersQuery as $row) {
$usersMap[$row['day']] = (int)$row['num'];
}
// 浏览量趋势
$viewsMap = [];
$viewsQuery = Db::name('fc_view_log')
->where('createtime', 'between', [$trendStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(`createtime`,'%Y-%m-%d') AS day, COUNT(*) AS num")
->group('day')
->select();
foreach ($viewsQuery as $row) {
$viewsMap[$row['day']] = (int)$row['num'];
}
// 计算 7/14/30 天数据包
$ranges = [7, 14, 30];
$allTrends = [];
foreach ($ranges as $span) {
$startTs = $todayStart - ($span - 1) * 86400;
$dates = [];
$posts = [];
$orders = [];
$users = [];
$views = [];
$rows = [];
for ($i = 0; $i < $span; $i++) {
$day = date('Y-m-d', $startTs + $i * 86400);
$p = isset($postsMap[$day]) ? $postsMap[$day] : 0;
$o = isset($ordersMap[$day]) ? $ordersMap[$day] : 0;
$u = isset($usersMap[$day]) ? $usersMap[$day] : 0;
$v = isset($viewsMap[$day]) ? $viewsMap[$day] : 0;
$dates[] = $day;
$posts[] = $p;
$orders[] = $o;
$users[] = $u;
$views[] = $v;
$rows[] = [
'date' => $day,
'posts' => $p,
'orders' => $o,
'users' => $u,
'views' => $v,
];
}
$allTrends[$span] = [
'dates' => $dates,
'posts' => $posts,
'orders' => $orders,
'users' => $users,
'views' => $views,
'rows' => array_reverse($rows),
];
}
// 如果是 AJAX 请求局部刷新趋势,可直接返回对应时段数据
if ($this->request->isAjax() && $this->request->get('action') === 'trend') {
$days = (int)$this->request->get('days', 14);
$trendData = isset($allTrends[$days]) ? $allTrends[$days] : $allTrends[14];
return json(['code' => 1, 'msg' => 'ok', 'data' => $trendData]);
}
// 默认 14 天用于模板首屏初始化
$defaultTrend = $allTrends[14];
/* =========================================================
* 7. 视图变量绑定与前端配置传递
* ========================================================= */
$this->view->assign('metrics', $metrics);
$this->view->assign('pending', $pending);
$this->view->assign('ecosystem', $ecosystem);
$this->view->assign('revenueTypes', $revenueTypes);
$this->view->assign('categoryRanks', $categoryRanks);
$this->view->assign('areaRanks', $areaRanks);
$this->view->assign('trendRows', $defaultTrend['rows']);
$this->view->assign('updatedAt', date('Y-m-d H:i'));
// 向前端 RequireJS 传入完整时段数据包及分类分布,供平滑渲染交互
$this->assignconfig('allTrends', $allTrends);
$this->assignconfig('revenueTypes', $revenueTypes);
$this->assignconfig('categoryRanks', $categoryRanks);
return $this->view->fetch();
}
}