新增同城便民生活服务平台标识,重构看板为业务总览与运营看板 新增待办督办中心、营收构成分析、热门分类与区域排行等模块 新增近7/14/30天多时段趋势切换,支持发布量/订单额/用户/曝光量指标切换 新增平台商业与生态资产统计模块,新增待办预警与空状态提示 新增趋势数据汇总与明细表联动,优化响应式布局与交互样式 新增商家管理、举报投诉等语言翻译项,新增登录推荐图
341 行
15 KiB
PHP
341 行
15 KiB
PHP
<?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();
|
||
}
|
||
}
|