551 lines
21 KiB
PHP
551 lines
21 KiB
PHP
<?php
|
||
|
||
namespace app\admin\controller\user;
|
||
|
||
use app\admin\model\MoneyLogHistory;
|
||
use Throwable;
|
||
use app\admin\model\User;
|
||
use app\admin\model\UserMoneyLog;
|
||
use app\common\controller\Backend;
|
||
use think\facade\Db;
|
||
|
||
class MoneyLog extends Backend
|
||
{
|
||
/**
|
||
* @var object
|
||
* @phpstan-var UserMoneyLog
|
||
*/
|
||
protected object $model;
|
||
|
||
protected array $withJoinTable = ['user', 'admin', 'bank'];
|
||
protected array $withTable = ['scoreLog'];
|
||
|
||
// 排除字段
|
||
protected string|array $preExcludeFields = ['create_time'];
|
||
|
||
protected string|array $quickSearchField = ['user.username', 'user.nickname'];
|
||
|
||
public function initialize(): void
|
||
{
|
||
parent::initialize();
|
||
$this->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'
|
||
));
|
||
}
|
||
} |