- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController - 新增 Service 层: TransactionService, ReportService, FollowPlanService - pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll) - 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan) - 后台新增跟单计划管理 + 用户输赢明细统计 - 封盘状态显示倒计时 (x:xx) - 音效仅在开奖弹窗打开时播放 - 路由按模块分组整理 - autoload 支持 App\Services 命名空间
345 lines
13 KiB
PHP
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;
|
|
}
|
|
}
|