model = new UserMoneyLog(); } /** * 验证逻辑 */ protected function validateModelData(array $data, string $scene = ''): void { if (!$this->modelValidate) return; $validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model)); if (class_exists($validateClass)) { $validate = new $validateClass(); if ($scene) $validate->scene($scene); $validate->check($data); } } /** * 查看 * @throws Throwable */ public function index(): void { if ($this->request->param('select')) { $this->select(); } list($where, $alias, $limit, $order) = $this->queryBuilder(); $res = $this->model ->withJoin($this->withJoinTable, $this->withJoinType) ->with($this->withTable) ->alias($alias) ->where($where) ->order($order) ->paginate($limit); $this->success('', [ 'list' => $res->items(), 'total' => $res->total(), 'remark' => get_route_remark(), ]); } /** * 添加 * @param int $userId * @throws Throwable */ public function add(int $userId = 0): void { $this->error(__('No rows were added')); if ($this->request->isPost()) { $data = $this->request->post(); if (!$data) { $this->error(__('Parameter %s can not be empty', [''])); } $result = false; $data = $this->excludeFields($data); if ($data['type'] == 2) { $data['money'] = -$data['money']; } $this->model->startTrans(); try { $data['created_by'] = $this->auth->getInfo()['id']; $result = $this->model->save($data); $this->model->commit(); } catch (Throwable $e) { $this->model->rollback(); $this->error($e->getMessage()); } if ($result !== false) { $this->success(__('Added successfully')); } else { $this->error(__('No rows were added')); } } $user = User::where('id', $userId)->find(); if (!$user) { $this->error(__("The user can't find it")); } $this->success('', [ 'user' => $user ]); } /** * 编辑 * @throws Throwable */ public function edit(): void { $pk = $this->model->getPk(); $id = $this->request->param($pk); $row = $this->model->find($id); if (!$row) { $this->error(__('Record not found')); } if ($this->request->isPost()) { $data = $this->request->post(); if (!$data) { $this->error(__('Parameter %s can not be empty', [''])); } $result = false; $this->model->startTrans(); try { $result = $row->save($data); $this->model->commit(); } catch (Throwable $e) { $this->model->rollback(); $this->error($e->getMessage()); } if ($result !== false) { $this->success(__('Update successful')); } else { $this->error(__('No rows updated')); } return; } $this->success('', [ 'row' => $row ]); } public function logHistory($id): void { $bindFields = [ 'admin' => ['id','username'], ]; $history = MoneyLogHistory::withJoin($bindFields, 'left') ->where('money_log_id', $id) ->select(); if (!empty($history)) { foreach ($history as &$item) { $item['admin_name'] = $item['admin']['username'] ?? ''; unset($item['admin']); } unset($item); } $this->success('', $history); } public function annualReport() { $year = $this->request->param('year/d', date('Y')); $timeExpression = "create_time"; $list = Db::table('ba_user_money_log') ->whereRaw("FROM_UNIXTIME({$timeExpression}, '%Y') = ?", [$year]) ->fieldRaw(" FROM_UNIXTIME({$timeExpression}, '%c') as month, -- 1. DEPOSIT (分转元,保留2位小数) ROUND(SUM(CASE WHEN type IN (1, 3) THEN money / 100 ELSE 0 END), 2) as deposit_amount, -- 2. WITHDRAW (从变更记录看提现通常存正数,这里直接累加,分转元) ROUND(SUM(CASE WHEN type IN (2, 4) THEN money / 100 ELSE 0 END), 2) as withdraw_amount, -- 3. TRANSACTION (DEPOSIT) COUNT(CASE WHEN type IN (1, 3) THEN id END) as deposit_count, -- 4. TRANSACTION (WITHDRAW) COUNT(CASE WHEN type IN (2, 4) THEN id END) as withdraw_count, -- 5. ACTIVE PLAYER (每月去重活跃用户) COUNT(DISTINCT user_id) as active_player, -- 6. FIRST DEPOSIT (首充去重人数) COUNT(DISTINCT CASE WHEN type = 1 AND label = 1 THEN user_id END) as first_deposit_count, -- 7. UNCLAIM RECEIPT (未领单数) COUNT(CASE WHEN type = 1 AND label = 2 THEN id END) as unclaim_count, -- 8. UNCLAIM AMOUNT (未领金额) ROUND(SUM(CASE WHEN type = 1 AND label = 2 THEN money / 100 ELSE 0 END), 2) as unclaim_amount ") ->group("month") ->select() ->toArray(); $monthsData = []; foreach ($list as $row) { $monthsData[intval($row['month'])] = $row; } $rowsSkeleton = [ 'deposit' => ['name' => 'DEPOSIT', 'is_price' => true], 'withdraw' => ['name' => 'WITHDRAW', 'is_price' => true], 'trans_deposit' => ['name' => 'TRANSACTION (DEPOSIT)', 'is_price' => false], 'trans_withdraw' => ['name' => 'TRANSACTION (WITHDRAW)', 'is_price' => false], 'active_player' => ['name' => 'ACTIVE PLAYER', 'is_price' => false], 'first_deposit' => ['name' => 'FIRST DEPOSIT', 'is_price' => false], 'unclaim_receipt' => ['name' => 'UNCLAIM RECEIPT', 'is_price' => false], 'unclaim_amount' => ['name' => 'UNCLAIM AMOUNT', 'is_price' => true], ]; $finalReport = []; foreach ($rowsSkeleton as $key => $meta) { $rowData = [ 'title' => $meta['name'] ]; $yearTotal = 0; for ($m = 1; $m <= 12; $m++) { $monthValue = 0; if (isset($monthsData[$m])) { $monthValue = match ($key) { 'deposit' => (float)$monthsData[$m]['deposit_amount'], 'withdraw' => (float)$monthsData[$m]['withdraw_amount'], 'trans_deposit' => (int)$monthsData[$m]['deposit_count'], 'trans_withdraw' => (int)$monthsData[$m]['withdraw_count'], 'active_player' => (int)$monthsData[$m]['active_player'], 'first_deposit' => (int)$monthsData[$m]['first_deposit_count'], 'unclaim_receipt' => (int)$monthsData[$m]['unclaim_count'], 'unclaim_amount' => (float)$monthsData[$m]['unclaim_amount'], }; } $rowData['month_' . $m] = $meta['is_price'] ? number_format($monthValue, 2, '.', '') : $monthValue; $yearTotal += $monthValue; } $rowData['year_total'] = $meta['is_price'] ? number_format($yearTotal, 2, '.', '') : $yearTotal; $finalReport[] = $rowData; } $this->success('', [ 'year' => $year, 'report_table' => $finalReport ]); } public function dailyReport() { // 1. 从配置中获取动态的小游戏列表 $games = config('mini_game') ?: []; // 2. 接收筛选参数,默认展示当月数据 $start = $this->request->param('start/s', date('Y-m-01')); $end = $this->request->param('end/s', date('Y-m-t')); $type = $this->request->param('type/s', 'daily'); // daily, monthly, yearly // 转换时间戳范围 $startTime = strtotime($start . ' 00:00:00'); $endTime = strtotime($end . ' 23:59:59'); // 根据 type 动态决定 MySQL 的日期分组格式 $dateFormat = '%Y-%m-%d'; if ($type === 'monthly') $dateFormat = '%Y-%m'; if ($type === 'yearly') $dateFormat = '%Y'; // 3. 🌟 动态生成小游戏的 SQL 统计子句 $promoSqlPieces = []; foreach ($games as $gameId => $gameName) { // 过滤掉不合法的非数字KEY,防止 SQL 注入风险 $gameId = (int)$gameId; // 顺便给生成的列取个易读的别名,比如:game_268_count, game_268_total $promoSqlPieces[] = "COUNT(CASE WHEN game_type = {$gameId} THEN 1 END) AS game_{$gameId}_count"; $promoSqlPieces[] = "SUM(CASE WHEN game_type = {$gameId} THEN amount ELSE 0 END) AS game_{$gameId}_total"; } // 将动态生成的片段用逗号拼接到一起 $dynamicPromoSelect = !empty($promoSqlPieces) ? ', ' . implode(', ', $promoSqlPieces) : ''; // 4. 核心聚合 SQL 1:基于余额变动表统计 $moneyLogSql = " SELECT FROM_UNIXTIME(create_time, '{$dateFormat}') AS report_date, COUNT(CASE WHEN type = 1 THEN 1 END) AS deposit_count, SUM(CASE WHEN type = 1 THEN money ELSE 0 END) / 100 AS deposit_total, COUNT(CASE WHEN type = 2 THEN 1 END) AS withdraw_count, SUM(CASE WHEN type = 2 THEN money ELSE 0 END) / 100 AS withdraw_total, (SUM(CASE WHEN type = 1 THEN money ELSE 0 END) - SUM(CASE WHEN type = 2 THEN money ELSE 0 END)) / 100 AS win_lose, COUNT(CASE WHEN label = 2 THEN 1 END) AS unclaim_count, SUM(CASE WHEN label = 2 THEN money ELSE 0 END) / 100 AS unclaim_total, COUNT(DISTINCT user_id) AS active_member, COUNT(DISTINCT CASE WHEN label = 1 THEN user_id END) AS new_deposit FROM ba_user_money_log WHERE create_time BETWEEN {$startTime} AND {$endTime} GROUP BY report_date "; $moneyData = Db::query($moneyLogSql); // 5. 核心聚合 SQL 2:👉 注入动态字段,统计小游戏奖励 $promoSql = " SELECT FROM_UNIXTIME(create_time, '{$dateFormat}') AS report_date {$dynamicPromoSelect} -- 🌟 动态生成的 COUNT 和 SUM 语句放在这里 FROM ba_promo_reward WHERE create_time BETWEEN {$startTime} AND {$endTime} GROUP BY report_date "; $promoData = Db::query($promoSql); // 6. 新注册用户统计 $userRegisterSql = " SELECT FROM_UNIXTIME(create_time, '{$dateFormat}') AS report_date, COUNT(id) AS new_register FROM ba_user WHERE create_time BETWEEN {$startTime} AND {$endTime} GROUP BY report_date "; $registerData = class_exists('\think\facade\Db') ? Db::query($userRegisterSql) : []; // 7. 数据集结构重组与动态初始化 $reportMap = []; $current = $startTime; while ($current <= $endTime) { $dateStr = date(str_replace('%', '', $dateFormat), $current); // 构建一天的基础数据结构 $baseRow = [ 'date' => $dateStr, 'deposit_count' => 0, 'deposit_total' => 0.00, 'withdraw_count' => 0, 'withdraw_total' => 0.00, 'win_lose' => 0.00, 'unclaim_count' => 0, 'unclaim_total' => 0.00, 'active_member' => 0, 'new_deposit' => 0, 'new_register' => 0, ]; // 🌟 动态初始化游戏字段的初始值(全部设为 0) foreach ($games as $gameId => $gameName) { $baseRow["game_{$gameId}_count"] = 0; $baseRow["game_{$gameId}_total"] = 0.00; } $reportMap[$dateStr] = $baseRow; $current = strtotime("+1 day", $current); } // 合并资产变动数据 foreach ($moneyData as $row) { if (isset($reportMap[$row['report_date']])) { $reportMap[$row['report_date']] = array_merge($reportMap[$row['report_date']], $row); } } // 合并动态小游戏数据 foreach ($promoData as $row) { if (isset($reportMap[$row['report_date']])) { $reportMap[$row['report_date']] = array_merge($reportMap[$row['report_date']], $row); } } // 合并注册用户数据 foreach ($registerData as $row) { if (isset($reportMap[$row['report_date']])) { $reportMap[$row['report_date']]['new_register'] = $row['new_register']; } } $list = array_values($reportMap); // 9. 返回结果给前端组件 $this->success('Success', [ 'start' => $start, 'end' => $end, 'type' => $type, 'games' => $games, // 🌟 把游戏映射送给前端,方便前端循环生成表格的 header 列 'list' => $list, ]); } public function customerReport() { // 1. 获取动态的小游戏配置 // 示例: [268 => 'plinko ball', 269 => 'smash eggs', 270 => 'spin wheel'] $games = config('mini_game') ?: []; // 2. 接收前端筛选参数 $start = $this->request->param('start/s', ''); $end = $this->request->param('end/s', ''); $username = $this->request->param('username/s', ''); $loseRebate = $this->request->param('lose_rebate/f', 0); // 输值返利百分比,如输入 5 代表 5% $limit = $this->request->param('limit/d', 10); // 🌟 每页条数,默认10条 // 3. 构建用户主表的查询条件 $userWhere = []; if (!empty($username)) { $userWhere[] = ['username', 'LIKE', '%' . (string)$username . '%']; } if (!empty($start) || !empty($end)) { $startTime = !empty($start) ? strtotime($start . ' 00:00:00') : 0; $endTime = !empty($end) ? strtotime($end . ' 23:59:59') : time(); $userWhere[] = ['create_time', 'between', [$startTime, $endTime]]; } // 4. 查询基础用户列表 (对应图片中的基本行) // 🌟 假设你的用户主表去前缀叫 'user',如果不是请更换成实际表名 $users = Db::name('user') ->where($userWhere) ->field('id, username, create_time') ->order('create_time', 'desc') // 对应图片中最新注册的用户排在最上方 ->paginate($limit); $count = $users->total(); $currentPage = $users->currentPage(); $lastPage = $users->lastPage(); // 如果没查到用户,直接返回空列表,免去后续的多表聚合 if (empty($users->items())) { $this->success('Success', [ 'start' => $start, 'end' => $end, 'username' => $username, 'games' => $games, 'list' => [], 'count' => $count, 'current_page' => $currentPage, 'last_page' => $lastPage, ]); } // 提取当前页/当前筛选出的所有用户 ID 集合 $userIds = array_column($users->items(), 'id'); $userIdsStr = implode(',', $userIds); // 5. 🌟 聚合资金流水数据 (Deposit / Withdraw / WinLose) $moneyLogSql = " SELECT user_id, COUNT(CASE WHEN type = 1 THEN 1 END) AS deposit_count, SUM(CASE WHEN type = 1 THEN money ELSE 0 END) / 100 AS deposit_total, COUNT(CASE WHEN type = 2 THEN 1 END) AS withdraw_count, SUM(CASE WHEN type = 2 THEN money ELSE 0 END) / 100 AS withdraw_total, (SUM(CASE WHEN type = 1 THEN money ELSE 0 END) - SUM(CASE WHEN type = 2 THEN money ELSE 0 END)) / 100 AS win_lose FROM ba_user_money_log WHERE user_id IN ({$userIdsStr}) GROUP BY user_id "; $moneyData = Db::query($moneyLogSql); // 重组为以 user_id 为键的关联数组,方便后续读取 $moneySummary = array_column($moneyData, null, 'user_id'); // 6. 🌟 动态拼装并聚合小游戏奖励数据 (与配置中的 gameId 挂钩) $promoSummary = []; if (!empty($games)) { $promoPieces = []; foreach ($games as $gameId => $name) { $gameId = (int)$gameId; $promoPieces[] = "COUNT(CASE WHEN game_type = {$gameId} THEN 1 END) AS game_{$gameId}_count"; $promoPieces[] = "SUM(CASE WHEN game_type = {$gameId} THEN amount ELSE 0 END) AS game_{$gameId}_total"; } $promoSql = "SELECT user_id, " . implode(', ', $promoPieces) . " FROM ba_promo_reward WHERE user_id IN ({$userIdsStr}) GROUP BY user_id"; $promoData = Db::query($promoSql); $promoSummary = array_column($promoData, null, 'user_id'); } // 7. 🌟 聚合下线推荐人数 (REFERRAL Downline) $referralSql = "SELECT parent_id, COUNT(id) AS referral_count FROM ba_user WHERE parent_id IN ({$userIdsStr}) GROUP BY parent_id"; $referralData = Db::query($referralSql); $referralSummary = array_column($referralData, null, 'parent_id'); // 8. 🌟 循环用户主表,拼装全维度报表行 $list = []; $loseRebatePercent = floatval($loseRebate) / 100; // 转换为小数进行百分比计算 foreach ($users as $user) { $uid = $user['id']; // 提取资金数据,若该用户没充提过,则给一套默认清零数据 $money = $moneySummary[$uid] ?? [ 'deposit_count' => 0, 'deposit_total' => 0.00, 'withdraw_count' => 0, 'withdraw_total' => 0.00, 'win_lose' => 0.00 ]; // 提取下线推荐人数 $referralCount = $referralSummary[$uid]['referral_count'] ?? 0; // 计算输值返利:只有当 WIN/LOSE 为负数(代表用户输钱)且前端传了返利比时才计算 $winLose = $money['win_lose']; $calculatedRebate = 0.00; if ($winLose < 0 && $loseRebatePercent > 0) { $calculatedRebate = abs($winLose) * $loseRebatePercent; } // 组装用户基础维度的列 $row = [ 'register_date' => date('Y-m-d H:i:s', $user['create_time']), 'username' => $user['username'], 'deposit_count' => $money['deposit_count'], 'deposit_total' => $money['deposit_total'], 'withdraw_count' => $money['withdraw_count'], 'withdraw_total' => $money['withdraw_total'], 'win_lose' => $winLose, 'lose_rebate' => $calculatedRebate, 'referral_count' => $referralCount, ]; // 🌟 动态注入配置文件中每一个小游戏的数据列 foreach ($games as $gameId => $name) { $row["game_{$gameId}_count"] = $promoSummary[$uid]["game_{$gameId}_count"] ?? 0; $row["game_{$gameId}_total"] = $promoSummary[$uid]["game_{$gameId}_total"] ?? 0.00; } $list[] = $row; } // 9. 返回给前端 $this->success('Success', compact( 'start', 'end', 'username', 'games', 'list', 'count', 'currentPage', 'lastPage' )); } }