Files
webman-buildadmin/app/admin/controller/Dashboard.php
zhenhui e65c3474bd 1.修改电话号码格式为60前缀,马来西亚格式
2.优化渠道可以查看分红方式,可以查看游玩详情
2026-05-30 11:09:54 +08:00

403 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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));
}
}