403 lines
14 KiB
PHP
403 lines
14 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\admin\controller;
|
||
|
||
use app\common\controller\Backend;
|
||
use support\think\Db;
|
||
use Webman\Http\Request;
|
||
use support\Response;
|
||
|
||
/**
|
||
* 后台首页统计:按渠道数据范围(非超管仅本渠道)汇总用户、充值、提现、投注等核心指标。
|
||
*/
|
||
class Dashboard extends Backend
|
||
{
|
||
public function index(Request $request): Response
|
||
{
|
||
$response = $this->initializeBackend($request);
|
||
if ($response !== null) {
|
||
return $response;
|
||
}
|
||
|
||
$ownerAdminId = $this->ownerAdminIdOrNull();
|
||
|
||
$todayStart = strtotime(date('Y-m-d'));
|
||
$todayEnd = $todayStart + 86400 - 1;
|
||
$yesterdayStart = $todayStart - 86400;
|
||
$yesterdayEnd = $todayStart - 1;
|
||
|
||
$userTotal = $this->countUsers($ownerAdminId);
|
||
$newToday = $this->countUsersInRange($ownerAdminId, $todayStart, $todayEnd);
|
||
$newYesterday = $this->countUsersInRange($ownerAdminId, $yesterdayStart, $yesterdayEnd);
|
||
$growthPct = null;
|
||
if ($newYesterday > 0) {
|
||
$growthPct = round(($newToday - $newYesterday) / $newYesterday * 100, 1);
|
||
} elseif ($newToday > 0) {
|
||
$growthPct = 100.0;
|
||
}
|
||
|
||
$depositAgg = $this->aggregateDepositToday($ownerAdminId, $todayStart, $todayEnd);
|
||
$withdrawPending = $this->countWithdrawPending($ownerAdminId);
|
||
$betAgg = $this->aggregateBetToday($ownerAdminId, $todayStart, $todayEnd);
|
||
|
||
$trend = $this->buildSevenDayTrend($ownerAdminId);
|
||
$channelShare = $this->buildChannelShare($ownerAdminId);
|
||
$depositAmountChannelShare = $this->buildDepositAmountChannelShare($ownerAdminId);
|
||
$recentUsers = $this->fetchRecentUsers($ownerAdminId, 10);
|
||
|
||
return $this->success('', [
|
||
'remark' => get_route_remark(),
|
||
'stats' => [
|
||
'user_total' => $userTotal,
|
||
'user_new_today' => $newToday,
|
||
'user_new_yesterday' => $newYesterday,
|
||
'user_new_growth_pct' => $growthPct,
|
||
'deposit_today_amount' => $depositAgg['amount'],
|
||
'deposit_today_count' => $depositAgg['count'],
|
||
'withdraw_pending' => $withdrawPending,
|
||
'bet_today_amount' => $betAgg['amount'],
|
||
'bet_today_count' => $betAgg['count'],
|
||
],
|
||
'trend' => $trend,
|
||
'channel_share' => $channelShare,
|
||
'deposit_amount_channel_share' => $depositAmountChannelShare,
|
||
'recent_users' => $recentUsers,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @param int|null $ownerAdminId null=超管不限制;非 null 时 where admin_id
|
||
*/
|
||
private function countUsers(?int $ownerAdminId): int
|
||
{
|
||
$q = Db::name('user');
|
||
if ($ownerAdminId !== null) {
|
||
$q->where('admin_id', '=', $ownerAdminId);
|
||
}
|
||
return intval($q->count());
|
||
}
|
||
|
||
/**
|
||
* @param int|null $ownerAdminId
|
||
*/
|
||
private function countUsersInRange(?int $ownerAdminId, int $start, int $end): int
|
||
{
|
||
$q = Db::name('user')
|
||
->where('create_time', '>=', $start)
|
||
->where('create_time', '<=', $end);
|
||
if ($ownerAdminId !== null) {
|
||
$q->where('admin_id', '=', $ownerAdminId);
|
||
}
|
||
return intval($q->count());
|
||
}
|
||
|
||
/**
|
||
* 今日成功充值:status=1,按创建日落在今日(与 mock 即时成功一致)。
|
||
*
|
||
* @param int|null $ownerAdminId
|
||
* @return array{count:int, amount:string}
|
||
*/
|
||
private function aggregateDepositToday(?int $ownerAdminId, int $todayStart, int $todayEnd): array
|
||
{
|
||
$q = Db::name('deposit_order')
|
||
->where('status', 1)
|
||
->where('create_time', '>=', $todayStart)
|
||
->where('create_time', '<=', $todayEnd);
|
||
if ($ownerAdminId !== null) {
|
||
$q->whereIn('user_id', $this->scopedUserIds($ownerAdminId));
|
||
}
|
||
$rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),0) AS s')->find();
|
||
if (!is_array($rows)) {
|
||
$rows = [];
|
||
}
|
||
$count = isset($rows['c']) ? intval($rows['c']) : 0;
|
||
$sum = isset($rows['s']) ? strval($rows['s']) : '0';
|
||
$amount = $this->formatMoney2($sum);
|
||
|
||
return ['count' => $count, 'amount' => $amount];
|
||
}
|
||
|
||
/**
|
||
* @param int|null $ownerAdminId
|
||
*/
|
||
private function countWithdrawPending(?int $ownerAdminId): int
|
||
{
|
||
$q = Db::name('withdraw_order')->where('status', 0);
|
||
if ($ownerAdminId !== null) {
|
||
$q->whereIn('user_id', $this->scopedUserIds($ownerAdminId));
|
||
}
|
||
return intval($q->count());
|
||
}
|
||
|
||
/**
|
||
* 今日投注:创建时间在今日且订单未作废(status 1 或 2)。
|
||
*
|
||
* @param int|null $ownerAdminId
|
||
* @return array{count:int, amount:string}
|
||
*/
|
||
private function aggregateBetToday(?int $ownerAdminId, int $todayStart, int $todayEnd): array
|
||
{
|
||
$q = Db::name('bet_order')
|
||
->whereIn('status', [1, 2])
|
||
->where('create_time', '>=', $todayStart)
|
||
->where('create_time', '<=', $todayEnd);
|
||
if ($ownerAdminId !== null) {
|
||
$q->whereIn('user_id', $this->scopedUserIds($ownerAdminId));
|
||
}
|
||
$rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(total_amount AS DECIMAL(18,2))),0) AS s')->find();
|
||
if (!is_array($rows)) {
|
||
$rows = [];
|
||
}
|
||
$count = isset($rows['c']) ? intval($rows['c']) : 0;
|
||
$sum = isset($rows['s']) ? strval($rows['s']) : '0';
|
||
|
||
return ['count' => $count, 'amount' => $this->formatMoney2($sum)];
|
||
}
|
||
|
||
/**
|
||
* @param int|null $ownerAdminId
|
||
* @return array{days:string[], new_users:int[], deposit_amount:string[], bet_amount:string[]}
|
||
*/
|
||
private function buildSevenDayTrend(?int $ownerAdminId): array
|
||
{
|
||
$days = [];
|
||
$newUsers = [];
|
||
$depositAmounts = [];
|
||
$betAmounts = [];
|
||
|
||
for ($i = 6; $i >= 0; $i--) {
|
||
$dayStart = strtotime(date('Y-m-d', strtotime('-' . $i . ' day')));
|
||
$dayEnd = $dayStart + 86400 - 1;
|
||
$days[] = date('m-d', $dayStart);
|
||
|
||
$newUsers[] = $this->countUsersInRange($ownerAdminId, $dayStart, $dayEnd);
|
||
|
||
$dq = Db::name('deposit_order')
|
||
->where('status', 1)
|
||
->where('create_time', '>=', $dayStart)
|
||
->where('create_time', '<=', $dayEnd);
|
||
if ($ownerAdminId !== null) {
|
||
$dq->whereIn('user_id', $this->scopedUserIds($ownerAdminId));
|
||
}
|
||
$drow = $dq->fieldRaw('COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),0) AS s')->find();
|
||
$dsum = is_array($drow) && isset($drow['s']) ? strval($drow['s']) : '0';
|
||
$depositAmounts[] = $this->formatMoney2($dsum);
|
||
|
||
$bq = Db::name('bet_order')
|
||
->whereIn('status', [1, 2])
|
||
->where('create_time', '>=', $dayStart)
|
||
->where('create_time', '<=', $dayEnd);
|
||
if ($ownerAdminId !== null) {
|
||
$bq->whereIn('user_id', $this->scopedUserIds($ownerAdminId));
|
||
}
|
||
$brow = $bq->fieldRaw('COALESCE(SUM(CAST(total_amount AS DECIMAL(18,2))),0) AS s')->find();
|
||
$bsum = is_array($brow) && isset($brow['s']) ? strval($brow['s']) : '0';
|
||
$betAmounts[] = $this->formatMoney2($bsum);
|
||
}
|
||
|
||
return [
|
||
'days' => $days,
|
||
'new_users' => $newUsers,
|
||
'deposit_amount' => $depositAmounts,
|
||
'bet_amount' => $betAmounts,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 用户按渠道分布(取前 8 名,其余合并为「其他」)。
|
||
*
|
||
* @param int|null $ownerAdminId
|
||
* @return list<array{name:string, value:int}>
|
||
*/
|
||
private function buildChannelShare(?int $ownerAdminId): array
|
||
{
|
||
$q = Db::name('user')->fieldRaw('channel_id, COUNT(*) AS c')->group('channel_id');
|
||
if ($ownerAdminId !== null) {
|
||
$q->where('admin_id', '=', $ownerAdminId);
|
||
}
|
||
$rows = $q->orderRaw('c DESC')->select()->toArray();
|
||
if ($rows === []) {
|
||
return [];
|
||
}
|
||
|
||
$channelNames = Db::name('channel')->column('name', 'id');
|
||
|
||
$list = [];
|
||
foreach ($rows as $row) {
|
||
$cid = $row['channel_id'];
|
||
$cnt = intval($row['c'] ?? 0);
|
||
if ($cid === null || $cid === '' || intval(strval($cid)) === 0) {
|
||
$name = '未分配渠道';
|
||
} else {
|
||
$id = intval(strval($cid));
|
||
$name = $channelNames[$id] ?? ('#' . strval($id));
|
||
}
|
||
$list[] = ['name' => $name, 'value' => $cnt];
|
||
}
|
||
|
||
if (count($list) <= 8) {
|
||
return $list;
|
||
}
|
||
|
||
$head = array_slice($list, 0, 8);
|
||
$rest = array_slice($list, 8);
|
||
$other = 0;
|
||
foreach ($rest as $item) {
|
||
$other += $item['value'];
|
||
}
|
||
if ($other > 0) {
|
||
$head[] = ['name' => '其他', 'value' => $other];
|
||
}
|
||
|
||
return $head;
|
||
}
|
||
|
||
/**
|
||
* 成功充值金额按订单归属渠道汇总(status=1,受渠道范围限制)。
|
||
*
|
||
* @param int|null $ownerAdminId
|
||
* @return list<array{name:string, value:string}> value 为两位小数字符串,供前端饼图展示
|
||
*/
|
||
private function buildDepositAmountChannelShare(?int $ownerAdminId): array
|
||
{
|
||
$q = Db::name('deposit_order')
|
||
->where('status', 1)
|
||
->fieldRaw('channel_id, COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),0) AS s')
|
||
->group('channel_id');
|
||
if ($ownerAdminId !== null) {
|
||
$q->whereIn('user_id', $this->scopedUserIds($ownerAdminId));
|
||
}
|
||
$rows = $q->select()->toArray();
|
||
if ($rows === []) {
|
||
return [];
|
||
}
|
||
|
||
$channelNames = Db::name('channel')->column('name', 'id');
|
||
|
||
$list = [];
|
||
foreach ($rows as $row) {
|
||
$cid = $row['channel_id'];
|
||
$sumRaw = isset($row['s']) ? strval($row['s']) : '0';
|
||
$amountStr = $this->formatMoney2($sumRaw);
|
||
if (bccomp($amountStr, '0', 2) <= 0) {
|
||
continue;
|
||
}
|
||
if ($cid === null || $cid === '' || intval(strval($cid)) === 0) {
|
||
$name = '未分配渠道';
|
||
} else {
|
||
$id = intval(strval($cid));
|
||
$name = $channelNames[$id] ?? ('#' . strval($id));
|
||
}
|
||
$list[] = [
|
||
'name' => $name,
|
||
'value' => $amountStr,
|
||
];
|
||
}
|
||
|
||
usort($list, static function (array $a, array $b): int {
|
||
return bccomp($b['value'], $a['value'], 2);
|
||
});
|
||
|
||
if (count($list) <= 8) {
|
||
return $list;
|
||
}
|
||
|
||
$head = array_slice($list, 0, 8);
|
||
$rest = array_slice($list, 8);
|
||
$other = '0';
|
||
foreach ($rest as $item) {
|
||
$other = bcadd($other, $item['value'], 2);
|
||
}
|
||
$otherFormatted = $this->formatMoney2($other);
|
||
if (bccomp($otherFormatted, '0', 2) > 0) {
|
||
$head[] = [
|
||
'name' => '其他',
|
||
'value' => $otherFormatted,
|
||
];
|
||
}
|
||
|
||
return $head;
|
||
}
|
||
|
||
/**
|
||
* @param int|null $ownerAdminId
|
||
* @return list<array{id:int, username:string, create_time:int, channel_name:string, head_image:string}>
|
||
*/
|
||
private function fetchRecentUsers(?int $ownerAdminId, int $limit): array
|
||
{
|
||
$q = Db::name('user')
|
||
->field(['id', 'username', 'create_time', 'channel_id', 'head_image'])
|
||
->order('id', 'desc')
|
||
->limit($limit);
|
||
if ($ownerAdminId !== null) {
|
||
$q->where('admin_id', '=', $ownerAdminId);
|
||
}
|
||
$rows = $q->select()->toArray();
|
||
if ($rows === []) {
|
||
return [];
|
||
}
|
||
|
||
$channelNames = Db::name('channel')->column('name', 'id');
|
||
$out = [];
|
||
foreach ($rows as $row) {
|
||
$cid = $row['channel_id'] ?? null;
|
||
if ($cid === null || $cid === '' || intval(strval($cid)) === 0) {
|
||
$cname = '';
|
||
} else {
|
||
$id = intval(strval($cid));
|
||
$cname = $channelNames[$id] ?? '';
|
||
}
|
||
$out[] = [
|
||
'id' => intval($row['id'] ?? 0),
|
||
'username' => strval($row['username'] ?? ''),
|
||
'create_time' => intval($row['create_time'] ?? 0),
|
||
'channel_name' => $cname,
|
||
'head_image' => strval($row['head_image'] ?? ''),
|
||
];
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
private function formatMoney2(string $amount): string
|
||
{
|
||
if (!is_numeric($amount)) {
|
||
return '0.00';
|
||
}
|
||
$normalized = bcadd($amount, '0', 2);
|
||
|
||
return bcadd($normalized, '0', 2);
|
||
}
|
||
|
||
/**
|
||
* 非超管:按当前管理员名下用户过滤。
|
||
* 超管:返回 null,表示 SQL 不加管理员条件。
|
||
*
|
||
* @return int|null
|
||
*/
|
||
private function ownerAdminIdOrNull(): ?int
|
||
{
|
||
if (!$this->auth || $this->auth->isSuperAdmin() || $this->hasGlobalReadScope()) {
|
||
return null;
|
||
}
|
||
$idRaw = $this->auth->id;
|
||
if ($idRaw === null || $idRaw === '' || !is_numeric(strval($idRaw))) {
|
||
return 0;
|
||
}
|
||
$id = intval(strval($idRaw));
|
||
return $id > 0 ? $id : 0;
|
||
}
|
||
|
||
/**
|
||
* @return int[]
|
||
*/
|
||
private function scopedUserIds(int $ownerAdminId): array
|
||
{
|
||
$ids = Db::name('user')->where('admin_id', '=', $ownerAdminId)->column('id');
|
||
$ids = array_map('intval', $ids);
|
||
return $ids === [] ? [0] : array_values(array_unique($ids));
|
||
}
|
||
}
|