Files
jk8_admin/app/admin/controller/user/MoneyLog.php

551 lines
21 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
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'
));
}
}