Files
pk10/App/Services/ReportService.php
T
li 4a94fe36cf feat: 重构代码架构 + 新增报表/跟单/输赢统计功能
- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController
- 新增 Service 层: TransactionService, ReportService, FollowPlanService
- pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll)
- 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan)
- 后台新增跟单计划管理 + 用户输赢明细统计
- 封盘状态显示倒计时 (x:xx)
- 音效仅在开奖弹窗打开时播放
- 路由按模块分组整理
- autoload 支持 App\Services 命名空间
2026-03-27 18:41:06 +08:00

345 lines
13 KiB
PHP

<?php
namespace App\Services;
use Db\Database;
class ReportService {
private $db;
public function __construct(Database $db) {
$this->db = $db;
}
/**
* 前台用户报表:按投注类型分组统计
*
* @param int $userId 用户ID
* @param string $dateFrom 开始日期 (Y-m-d)
* @param string $dateTo 结束日期 (Y-m-d)
* @return array
*/
public function getUserReport(int $userId, string $dateFrom, string $dateTo): array
{
$rangeFrom = $dateFrom . ' 00:00:00';
$rangeTo = $dateTo . ' 23:59:59';
$pdo = $this->db->medoo->pdo;
try {
// 投注统计(有效流水 = 已结算投注额,排除 pending)
$stmt = $pdo->prepare("
SELECT
bet_type as type,
COUNT(*) as bet_count,
SUM(amount) as amount,
SUM(CASE WHEN status IN ('win','lose') THEN amount ELSE 0 END) as effective_flow,
SUM(CASE
WHEN status='win' THEN win_amount
WHEN status='lose' THEN -amount
ELSE 0
END) as win_loss
FROM bets
WHERE user_id = ? AND created_at BETWEEN ? AND ?
GROUP BY bet_type
ORDER BY bet_type
");
$stmt->execute([$userId, $rangeFrom, $rangeTo]);
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// 退水(代理返佣给该用户的金额)
$rebateTotal = 0;
try {
$stmt2 = $pdo->prepare("
SELECT COALESCE(SUM(commission), 0) as total
FROM agent_commissions
WHERE from_user_id = ? AND type = 'rebate' AND created_at BETWEEN ? AND ?
");
$stmt2->execute([$userId, $rangeFrom, $rangeTo]);
$rebateTotal = floatval($stmt2->fetch(\PDO::FETCH_ASSOC)['total'] ?? 0);
} catch (\Exception $e) {
$rebateTotal = 0;
}
// 将退水按比例分配到各类型(或全部放在第一行)
$totalBet = array_sum(array_column($rows, 'amount'));
foreach ($rows as &$row) {
$row['amount'] = floatval($row['amount']);
$row['effective_flow'] = floatval($row['effective_flow']);
$row['win_loss'] = floatval($row['win_loss']);
// 按投注额占比分配退水
$ratio = $totalBet > 0 ? $row['amount'] / $totalBet : 0;
$row['rebate'] = round($rebateTotal * $ratio, 2);
}
unset($row);
return $rows;
} catch (\Exception $e) {
return [];
}
}
/**
* 后台总体统计
*
* @param string $dateFrom 开始日期 (Y-m-d)
* @param string $dateTo 结束日期 (Y-m-d)
* @return array
*/
public function getOverviewStats(string $dateFrom, string $dateTo): array
{
$range = [$dateFrom . ' 00:00:00', $dateTo . ' 23:59:59'];
$pdo = $this->db->medoo->pdo;
// 总投注(排除虚拟)
$totalBet = $this->db->sum('bets', 'amount', [
'is_virtual' => 0, 'created_at[<>]' => $range
]) ?: 0;
// 总派奖
$totalWin = $this->db->sum('bets', 'win_amount', [
'is_virtual' => 0, 'status' => 'win', 'created_at[<>]' => $range
]) ?: 0;
// 平台利润
$profit = $totalBet - $totalWin;
$profitRate = $totalBet > 0 ? round($profit / $totalBet * 100, 2) : 0;
// 总充值
$totalDeposit = $this->db->sum('transactions', 'amount', [
'is_virtual' => 0, 'type[~]' => '%deposit%', 'amount[>]' => 0,
'created_at[<>]' => $range
]) ?: 0;
// 总提现
$totalWithdraw = abs($this->db->sum('transactions', 'amount', [
'is_virtual' => 0, 'type[~]' => '%withdraw%', 'amount[<]' => 0,
'created_at[<>]' => $range
]) ?: 0);
// 代理佣金
$totalCommission = $this->db->sum('agent_commissions', 'commission', [
'created_at[<>]' => $range
]) ?: 0;
// 反水
$totalRebate = $this->db->sum('agent_commissions', 'commission', [
'type' => 'rebate', 'created_at[<>]' => $range
]) ?: 0;
// 用户统计
$totalUsers = $this->db->count('users', ['is_virtual' => 0]);
$newUsers = $this->db->count('users', [
'is_virtual' => 0, 'created_at[<>]' => $range
]);
// 活跃用户数(有投注记录的非虚拟用户)
$activeUsers = 0;
try {
$stmt = $pdo->prepare("SELECT COUNT(DISTINCT user_id) as cnt FROM bets WHERE is_virtual = 0 AND created_at BETWEEN ? AND ?");
$stmt->execute([$range[0], $range[1]]);
$activeUsers = (int)($stmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0);
} catch (\Exception $e) {
$activeUsers = 0;
}
return compact(
'totalBet', 'totalWin', 'profit', 'profitRate',
'totalDeposit', 'totalWithdraw', 'totalCommission', 'totalRebate',
'totalUsers', 'newUsers', 'activeUsers'
);
}
/**
* 日明细统计
*
* @param string $dateFrom 开始日期 (Y-m-d)
* @param string $dateTo 结束日期 (Y-m-d)
* @return array
*/
public function getDailyStats(string $dateFrom, string $dateTo): array
{
$range = [$dateFrom . ' 00:00:00', $dateTo . ' 23:59:59'];
$pdo = $this->db->medoo->pdo;
$dailyStats = [];
try {
$stmt = $pdo->prepare("
SELECT
DATE(created_at) as date,
SUM(amount) as total_bet,
SUM(CASE WHEN status='win' THEN win_amount ELSE 0 END) as total_win,
COUNT(*) as bet_count,
SUM(CASE WHEN status='win' THEN 1 ELSE 0 END) as win_count
FROM bets
WHERE is_virtual = 0 AND created_at BETWEEN ? AND ?
GROUP BY DATE(created_at)
ORDER BY date DESC
");
$stmt->execute([$range[0], $range[1]]);
$dailyStats = $stmt->fetchAll(\PDO::FETCH_ASSOC);
} catch (\Exception $e) {
return [];
}
// 补充每日充提数据
foreach ($dailyStats as &$day) {
$dr = [$day['date'] . ' 00:00:00', $day['date'] . ' 23:59:59'];
$day['deposits'] = $this->db->sum('transactions', 'amount', [
'is_virtual' => 0, 'type[~]' => '%deposit%', 'amount[>]' => 0,
'created_at[<>]' => $dr
]) ?: 0;
$day['withdraws'] = abs($this->db->sum('transactions', 'amount', [
'is_virtual' => 0, 'type[~]' => '%withdraw%', 'amount[<]' => 0,
'created_at[<>]' => $dr
]) ?: 0);
$day['total_bet'] = floatval($day['total_bet'] ?? 0);
$day['total_win'] = floatval($day['total_win'] ?? 0);
$day['bet_count'] = intval($day['bet_count'] ?? 0);
$day['win_count'] = intval($day['win_count'] ?? 0);
$day['profit'] = $day['total_bet'] - $day['total_win'];
$day['win_rate'] = $day['bet_count'] > 0 ? round($day['win_count'] / $day['bet_count'] * 100, 1) : 0;
}
unset($day);
return $dailyStats;
}
/**
* 用户输赢明细(含日维度分组 + 用户汇总)
*
* @param string $dateFrom 开始日期 (Y-m-d)
* @param string $dateTo 结束日期 (Y-m-d)
* @return array ['raw' => 原始行, 'summary' => 按用户汇总]
*/
public function getUserWinLoss(string $dateFrom, string $dateTo): array
{
$range = [$dateFrom . ' 00:00:00', $dateTo . ' 23:59:59'];
$pdo = $this->db->medoo->pdo;
$userWinLoss = [];
try {
$stmt = $pdo->prepare("
SELECT u.id as user_id, u.username, u.balance,
DATE(b.created_at) as date,
SUM(b.amount) as total_bet,
SUM(CASE WHEN b.status='win' THEN b.win_amount ELSE 0 END) as total_win,
COUNT(*) as bet_count,
SUM(CASE WHEN b.status='win' THEN 1 ELSE 0 END) as win_count
FROM bets b
JOIN users u ON b.user_id = u.id
WHERE b.is_virtual = 0 AND b.created_at BETWEEN ? AND ?
GROUP BY b.user_id, DATE(b.created_at)
ORDER BY u.username, date DESC
");
$stmt->execute([$range[0], $range[1]]);
$userWinLoss = $stmt->fetchAll(\PDO::FETCH_ASSOC);
} catch (\Exception $e) {
return ['raw' => [], 'summary' => []];
}
// 按用户汇总
$userSummary = [];
foreach ($userWinLoss as $row) {
$uid = $row['user_id'];
if (!isset($userSummary[$uid])) {
$userSummary[$uid] = [
'username' => $row['username'],
'balance' => floatval($row['balance']),
'total_bet' => 0, 'total_win' => 0,
'bet_count' => 0, 'win_count' => 0,
'days' => []
];
}
$bet = floatval($row['total_bet']);
$win = floatval($row['total_win']);
$userSummary[$uid]['total_bet'] += $bet;
$userSummary[$uid]['total_win'] += $win;
$userSummary[$uid]['bet_count'] += intval($row['bet_count']);
$userSummary[$uid]['win_count'] += intval($row['win_count']);
$userSummary[$uid]['days'][] = [
'date' => $row['date'],
'bet' => $bet, 'win' => $win,
'profit' => $bet - $win,
'count' => intval($row['bet_count']),
];
}
// 排序:按总投注额降序
uasort($userSummary, function($a, $b) { return $b['total_bet'] <=> $a['total_bet']; });
return ['raw' => $userWinLoss, 'summary' => $userSummary];
}
/**
* 用户排行(投注额 TOP N)
*
* @param string $dateFrom 开始日期 (Y-m-d)
* @param string $dateTo 结束日期 (Y-m-d)
* @param int $limit 条数限制,0=不限
* @return array
*/
public function getTopUsers(string $dateFrom, string $dateTo, int $limit = 20): array
{
$range = [$dateFrom . ' 00:00:00', $dateTo . ' 23:59:59'];
$pdo = $this->db->medoo->pdo;
$sql = "
SELECT u.username,
SUM(b.amount) as total_bet,
SUM(CASE WHEN b.status='win' THEN b.win_amount ELSE 0 END) as total_win,
SUM(b.amount) - SUM(CASE WHEN b.status='win' THEN b.win_amount ELSE 0 END) as profit,
COUNT(*) as bet_count
FROM bets b
JOIN users u ON b.user_id = u.id
WHERE b.is_virtual = 0 AND b.created_at BETWEEN ? AND ?
GROUP BY b.user_id
ORDER BY total_bet DESC
";
if ($limit > 0) {
$sql .= " LIMIT " . intval($limit);
}
try {
$stmt = $pdo->prepare($sql);
$stmt->execute([$range[0], $range[1]]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
} catch (\Exception $e) {
return [];
}
}
/**
* 代理报表
*
* @param string $dateFrom 开始日期 (Y-m-d)
* @param string $dateTo 结束日期 (Y-m-d)
* @return array
*/
public function getAgentStats(string $dateFrom, string $dateTo): array
{
$range = [$dateFrom . ' 00:00:00', $dateTo . ' 23:59:59'];
$agentStats = [];
$agents = $this->db->select('agents', '*', ['status' => 1]);
foreach ($agents as $ag) {
$playerIds = $this->db->select('users', 'id', ['agent_id' => $ag['id'], 'is_virtual' => 0]);
$agBet = 0;
$agWin = 0;
if (!empty($playerIds)) {
$agBet = $this->db->sum('bets', 'amount', ['user_id' => $playerIds, 'is_virtual' => 0, 'created_at[<>]' => $range]) ?: 0;
$agWin = $this->db->sum('bets', 'win_amount', ['user_id' => $playerIds, 'is_virtual' => 0, 'status' => 'win', 'created_at[<>]' => $range]) ?: 0;
}
$agComm = $this->db->sum('agent_commissions', 'commission', ['agent_id' => $ag['id'], 'created_at[<>]' => $range]) ?: 0;
$agentStats[] = [
'agent' => $ag,
'user' => $this->db->get('users', ['username'], ['id' => $ag['user_id']]),
'players' => count($playerIds),
'bets' => $agBet,
'wins' => $agWin,
'commission' => $agComm,
'profit' => $agBet - $agWin - $agComm,
];
}
return $agentStats;
}
}