Compare commits

...

2 Commits

Author SHA1 Message Date
6f56574aac 优化主页数据统计 2026-03-10 12:42:29 +08:00
fdd8f6dffa 相关记录表admin_id关联当前管理员id 2026-03-10 12:30:56 +08:00
25 changed files with 569 additions and 115 deletions

View File

@@ -1,31 +1,32 @@
import request from '@/utils/http'
/**
* 基础数据统计
* 大富翁工作台卡片统计(玩家注册、充值、提现、游玩次数,含较上周对比)
* @returns 响应
*/
export function fetchStatistics() {
return request.get<any>({
url: '/core/system/statistics'
url: '/core/dice/dashboard/statistics'
})
}
/**
* 登录统计图表数据
* 近期玩家充值统计近10天每日充值金额
* @returns 响应
*/
export function fetchLoginChart() {
export function fetchRechargeChart() {
return request.get<any>({
url: '/core/system/loginChart'
url: '/core/dice/dashboard/rechargeChart'
})
}
/**
* 登录统计图表数据
* 月度玩家充值汇总当年1-12月每月充值金额
* @returns 响应
*/
export function fetchLoginBarChart() {
export function fetchRechargeBarChart() {
return request.get<any>({
url: '/core/system/loginBarChart'
url: '/core/dice/dashboard/rechargeBarChart'
})
}

View File

@@ -28,7 +28,6 @@
</ElRow>
</template>
<AboutProject />
</div>
</template>
@@ -36,7 +35,6 @@
import CardList from './modules/card-list.vue'
import ActiveUser from './modules/active-user.vue'
import SalesOverview from './modules/sales-overview.vue'
import AboutProject from './modules/about-project.vue'
import NewUser from './modules/new-user.vue'
import Dynamic from './modules/dynamic-stats.vue'
import TodoList from './modules/todo-list.vue'

View File

@@ -1,43 +0,0 @@
<template>
<div class="art-card p-5 flex-b mb-5 max-sm:mb-4">
<div>
<h2 class="text-2xl font-medium">关于项目</h2>
<p class="text-g-700 mt-1">{{ systemName }} 是一款兼具设计美学与高效开发的后台系统</p>
<p class="text-g-700 mt-1">使用了 webman + Vue3 + Element Plus 高性能高颜值技术栈</p>
<div class="flex flex-wrap gap-3.5 max-w-150 mt-9">
<div
class="w-60 flex-cb h-12.5 px-3.5 border border-g-300 c-p rounded-lg text-sm bg-g-100 duration-300 hover:-translate-y-1 max-sm:w-full"
v-for="link in linkList"
:key="link.label"
@click="goPage(link.url)"
>
<span class="text-g-700">{{ link.label }}</span>
<ArtSvgIcon icon="ri:arrow-right-s-line" class="text-lg text-g-600" />
</div>
</div>
</div>
<img class="w-75 max-md:!hidden" src="@imgs/draw/draw1.png" alt="draw1" />
</div>
</template>
<script setup lang="ts">
import AppConfig from '@/config'
const systemName = AppConfig.systemInfo.name
const linkList = [
{ label: '项目官网', url: 'https://saithink.top/' },
{ label: '文档', url: 'https://saithink.top/documents/' },
{ label: 'Github', url: 'https://github.com/saithink/saiadmin' },
{ label: '插件市场', url: 'https://saas.saithink.top/' }
]
/**
* 在新标签页中打开指定 URL
* @param url 要打开的网页地址
*/
const goPage = (url: string): void => {
window.open(url, '_blank', 'noopener,noreferrer')
}
</script>

View File

@@ -2,7 +2,7 @@
<div class="art-card h-105 p-4 box-border mb-5 max-sm:mb-4">
<div class="art-card-header">
<div class="title">
<h4>月度登录汇总</h4>
<h4>月度玩家充值汇总</h4>
</div>
</div>
<ArtBarChart
@@ -17,22 +17,22 @@
</template>
<script setup lang="ts">
import { fetchLoginBarChart } from '@/api/dashboard'
import { fetchRechargeBarChart } from '@/api/dashboard'
/**
* 登录数据
* 充值金额数据
*/
const yData = ref<number[]>([])
/**
* 时间数据
* 月份数据
*/
const xData = ref<string[]>([])
onMounted(async () => {
fetchLoginBarChart().then((data) => {
yData.value = data.login_count
xData.value = data.login_month
fetchRechargeBarChart().then((data: any) => {
yData.value = data?.recharge_amount ?? []
xData.value = data?.recharge_month ?? []
})
})
</script>

View File

@@ -2,73 +2,95 @@
<ElRow :gutter="20" class="flex">
<ElCol :sm="12" :md="6" :lg="6">
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
<span class="text-g-700 text-sm">用户统计</span>
<ArtCountTo class="text-[26px] font-medium mt-2" :target="statData.user" :duration="1300" />
<span class="text-g-700 text-sm">玩家注册</span>
<ArtCountTo class="text-[26px] font-medium mt-2" :target="statData.player_count" :duration="1300" />
<div class="flex-c mt-1">
<span class="text-xs text-g-600">较上周</span>
<span class="ml-1 text-xs font-semibold text-success">+10%</span>
<span
class="ml-1 text-xs font-semibold"
:class="changeClass(statData.player_count_change)"
>
{{ formatChange(statData.player_count_change) }}
</span>
</div>
<div
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
>
<ArtSvgIcon icon="ri:group-line" class="text-xl text-theme" />
<ArtSvgIcon icon="ri:user-add-line" class="text-xl text-theme" />
</div>
</div>
</ElCol>
<ElCol :sm="12" :md="6" :lg="6">
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
<span class="text-g-700 text-sm">附件统计</span>
<span class="text-g-700 text-sm">玩家充值</span>
<ArtCountTo
class="text-[26px] font-medium mt-2"
:target="statData.attach"
:target="statData.charge_amount"
:duration="1300"
:decimals="2"
/>
<div class="flex-c mt-1">
<span class="text-xs text-g-600">较上周</span>
<span class="ml-1 text-xs font-semibold text-success">+10%</span>
<span
class="ml-1 text-xs font-semibold"
:class="changeClass(statData.charge_amount_change)"
>
{{ formatChange(statData.charge_amount_change) }}
</span>
</div>
<div
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
>
<ArtSvgIcon icon="ri:attachment-line" class="text-xl text-theme" />
<ArtSvgIcon icon="ri:money-dollar-circle-line" class="text-xl text-theme" />
</div>
</div>
</ElCol>
<ElCol :sm="12" :md="6" :lg="6">
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
<span class="text-g-700 text-sm">登录统计</span>
<span class="text-g-700 text-sm">玩家提现</span>
<ArtCountTo
class="text-[26px] font-medium mt-2"
:target="statData.login"
:target="statData.withdraw_amount"
:duration="1300"
:decimals="2"
/>
<div class="flex-c mt-1">
<span class="text-xs text-g-600">较上周</span>
<span class="ml-1 text-xs font-semibold text-success">+12%</span>
<span
class="ml-1 text-xs font-semibold"
:class="changeClass(statData.withdraw_amount_change)"
>
{{ formatChange(statData.withdraw_amount_change) }}
</span>
</div>
<div
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
>
<ArtSvgIcon icon="ri:fire-line" class="text-xl text-theme" />
<ArtSvgIcon icon="ri:bank-card-line" class="text-xl text-theme" />
</div>
</div>
</ElCol>
<ElCol :sm="12" :md="6" :lg="6">
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
<span class="text-g-700 text-sm">操作统计</span>
<span class="text-g-700 text-sm">玩家游玩次数</span>
<ArtCountTo
class="text-[26px] font-medium mt-2"
:target="statData.operate"
:target="statData.play_count"
:duration="1300"
/>
<div class="flex-c mt-1">
<span class="text-xs text-g-600">较上周</span>
<span class="ml-1 text-xs font-semibold text-danger">-5%</span>
<span
class="ml-1 text-xs font-semibold"
:class="changeClass(statData.play_count_change)"
>
{{ formatChange(statData.play_count_change) }}
</span>
</div>
<div
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
>
<ArtSvgIcon icon="ri:pie-chart-line" class="text-xl text-theme" />
<ArtSvgIcon icon="ri:gamepad-line" class="text-xl text-theme" />
</div>
</div>
</ElCol>
@@ -79,15 +101,40 @@
import { fetchStatistics } from '@/api/dashboard'
const statData = ref({
user: 0,
attach: 0,
login: 0,
operate: 0
player_count: 0,
player_count_change: 0,
charge_amount: 0,
charge_amount_change: 0,
withdraw_amount: 0,
withdraw_amount_change: 0,
play_count: 0,
play_count_change: 0
})
function formatChange(val: number): string {
if (val > 0) return `+${val}%`
if (val < 0) return `${val}%`
return '0%'
}
function changeClass(val: number): string {
if (val > 0) return 'text-success'
if (val < 0) return 'text-danger'
return 'text-g-600'
}
onMounted(() => {
fetchStatistics().then((data) => {
statData.value = data
fetchStatistics().then((data: any) => {
statData.value = {
player_count: data?.player_count ?? 0,
player_count_change: data?.player_count_change ?? 0,
charge_amount: data?.charge_amount ?? 0,
charge_amount_change: data?.charge_amount_change ?? 0,
withdraw_amount: data?.withdraw_amount ?? 0,
withdraw_amount_change: data?.withdraw_amount_change ?? 0,
play_count: data?.play_count ?? 0,
play_count_change: data?.play_count_change ?? 0
}
})
})
</script>

View File

@@ -2,7 +2,7 @@
<div class="art-card h-105 p-5 mb-5 max-sm:mb-4">
<div class="art-card-header">
<div class="title">
<h4>近期登录统计</h4>
<h4>近期玩家充值统计</h4>
</div>
</div>
<ArtLineChart
@@ -16,22 +16,22 @@
</template>
<script setup lang="ts">
import { fetchLoginChart } from '@/api/dashboard'
import { fetchRechargeChart } from '@/api/dashboard'
/**
* 登录数据
* 充值金额数据
*/
const yData = ref<number[]>([])
/**
* 时间数据
* 日期数据
*/
const xData = ref<string[]>([])
onMounted(async () => {
fetchLoginChart().then((data) => {
yData.value = data.login_count
xData.value = data.login_date
fetchRechargeChart().then((data: any) => {
yData.value = data?.recharge_amount ?? []
xData.value = data?.recharge_date ?? []
})
})
</script>

View File

@@ -83,5 +83,29 @@ export default {
})
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{ id: number; name: string }>
return rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
},
/**
* 获取后台管理员选项SystemUser供 admin_id 下拉使用
* @returns [ { id, username, realname, label } ]
*/
async getSystemUserOptions(): Promise<
Array<{ id: number; username: string; realname: string; label: string }>
> {
const res = await request.get<any>({
url: '/dice/player/DicePlayer/getSystemUserOptions'
})
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{
id: number
username: string
realname: string
label: string
}>
return rows.map((r) => ({
id: Number(r.id),
username: String(r.username ?? ''),
realname: String(r.realname ?? ''),
label: String(r.label ?? r.username ?? r.id ?? '')
}))
}
}

View File

@@ -34,6 +34,23 @@
<el-form-item label="状态" prop="status">
<sa-switch v-model="formData.status" />
</el-form-item>
<el-form-item label="所属管理员" prop="admin_id">
<el-select
v-model="formData.admin_id"
placeholder="选择后台管理员(可选)"
clearable
filterable
style="width: 100%"
:loading="systemUserOptionsLoading"
>
<el-option
v-for="item in systemUserOptions"
:key="item.id"
:label="item.label || item.username || `#${item.id}`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="平台币" prop="coin">
<el-input-number
v-model="formData.coin"
@@ -221,6 +238,8 @@
phone: '',
password: '',
status: 1 as number,
/** 所属后台管理员 IDSystemUser.id */
admin_id: null as number | null,
coin: 0 as number,
/** 彩金池配置 ID空 = 自定义权重,否则 = DiceLotteryConfig.id */
lottery_config_id: null as number | null,
@@ -237,6 +256,12 @@
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
/** 彩金池选项加载中 */
const lotteryConfigLoading = ref(false)
/** 后台管理员下拉选项SystemUser */
const systemUserOptions = ref<
Array<{ id: number; username: string; realname: string; label: string }>
>([])
/** 管理员选项加载中 */
const systemUserOptionsLoading = ref(false)
/** 当前选中的 DiceLotteryConfig 完整数据(用于展示) */
const currentLotteryConfig = ref<Record<string, any> | null>(null)
@@ -306,10 +331,22 @@
}
}
/** 加载后台管理员选项 */
async function loadSystemUserOptions() {
systemUserOptionsLoading.value = true
try {
systemUserOptions.value = await api.getSystemUserOptions()
} catch {
systemUserOptions.value = []
} finally {
systemUserOptionsLoading.value = false
}
}
const initPage = async () => {
currentLotteryConfig.value = null
Object.assign(formData, initialFormData)
await loadLotteryConfigOptions()
await Promise.all([loadLotteryConfigOptions(), loadSystemUserOptions()])
if (props.data) {
await nextTick()
initForm()
@@ -355,7 +392,7 @@
if (numKeys.includes(key)) {
if (key === 'id') {
;(formData as any)[key] = val != null ? Number(val) || null : null
} else if (key === 'lottery_config_id') {
} else if (key === 'lottery_config_id' || key === 'admin_id') {
const num = Number(val)
;(formData as any)[key] = val != null && !Number.isNaN(num) && num !== 0 ? num : null
} else {

View File

@@ -137,9 +137,16 @@ class GameController extends BaseController
]);
$timeoutRecord = null;
$timeout_message = '';
$adminId = null;
try {
$timeoutPlayer = DicePlayer::find($userId);
$adminId = ($timeoutPlayer && ($timeoutPlayer->admin_id ?? null)) ? (int) $timeoutPlayer->admin_id : null;
} catch (\Throwable $_) {
}
try {
$timeoutRecord = DicePlayRecord::create([
'player_id' => $userId,
'admin_id' => $adminId,
'lottery_config_id' => 0,
'lottery_type' => 0,
'is_win' => 0,

View File

@@ -6,6 +6,7 @@ namespace app\api\controller\v1;
use app\api\logic\UserLogic;
use app\api\util\ReturnCode;
use app\dice\model\player\DicePlayer;
use plugin\saiadmin\app\model\system\SystemUser;
use app\dice\model\play_record\DicePlayRecord;
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
@@ -40,9 +41,18 @@ class GameController extends BaseController
$time = (string) time();
}
$adminId = null;
$agentId = trim((string) ($request->agent_id ?? ''));
if ($agentId !== '') {
$systemUser = SystemUser::where('agent_id', $agentId)->find();
if ($systemUser) {
$adminId = (int) $systemUser->id;
}
}
try {
$logic = new UserLogic();
$result = $logic->loginByUsername($username, $password, 'chs', 0.0, $time);
$result = $logic->loginByUsername($username, $password, 'chs', 0.0, $time, $adminId);
} catch (\plugin\saiadmin\exception\ApiException $e) {
return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR);
}
@@ -264,8 +274,10 @@ class GameController extends BaseController
$player->coin = $walletAfter;
$player->save();
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
$record = DicePlayerWalletRecord::create([
'player_id' => (int) $player->id,
'admin_id' => $adminId,
'coin' => $coinVal,
'type' => $type,
'wallet_before' => $walletBefore,

View File

@@ -69,10 +69,12 @@ class GameLogic
UserCache::setUser($playerId, $updatedUserArr);
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
try {
Db::transaction(function () use (
$player,
$playerId,
$adminId,
$cost,
$coinBefore,
$coinAfter,
@@ -91,6 +93,7 @@ class GameLogic
DicePlayerWalletRecord::create([
'player_id' => $playerId,
'admin_id' => $adminId,
'coin' => -$cost,
'type' => self::WALLET_TYPE_BUY_DRAW,
'wallet_before' => $coinBefore,
@@ -103,6 +106,7 @@ class GameLogic
DicePlayerTicketRecord::create([
'player_id' => $playerId,
'admin_id' => $adminId,
'use_coins' => $cost,
'total_ticket_count' => $addTotal,
'paid_ticket_count' => $addPaid,

View File

@@ -160,9 +160,11 @@ class PlayStartLogic
$rewardId = $chosenId;
$configName = (string) ($config->name ?? '');
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
try {
Db::transaction(function () use (
$playerId,
$adminId,
$configId,
$rewardId,
$configName,
@@ -181,6 +183,7 @@ class PlayStartLogic
) {
$record = DicePlayRecord::create([
'player_id' => $playerId,
'admin_id' => $adminId,
'lottery_config_id' => $configId,
'lottery_type' => $ticketType,
'is_win' => $isWin,
@@ -218,9 +221,10 @@ class PlayStartLogic
$p->total_ticket_count = (int) $p->total_ticket_count + 1;
DicePlayerTicketRecord::create([
'player_id' => $playerId,
'player_id' => $playerId,
'admin_id' => $adminId,
'free_ticket_count' => 1,
'remark' => '中奖结果为T5',
'remark' => '中奖结果为T5',
]);
}
@@ -236,6 +240,7 @@ class PlayStartLogic
DicePlayerWalletRecord::create([
'player_id' => $playerId,
'admin_id' => $adminId,
'coin' => $winCoin,
'type' => self::WALLET_TYPE_DRAW,
'wallet_before' => $coinBefore,
@@ -248,6 +253,7 @@ class PlayStartLogic
try {
$record = DicePlayRecord::create([
'player_id' => $playerId,
'admin_id' => $adminId ?? null,
'lottery_config_id' => $configId ?? 0,
'lottery_type' => $ticketType,
'is_win' => 0,

View File

@@ -45,8 +45,10 @@ class UserLogic
* 登录JSONusername, password, lang, coin, time
* 存在则校验密码并更新 coin累加不存在则创建用户并写入 coin。
* 将会话写入 Redis返回 token 与前端连接地址。
*
* @param int|null $adminId 创建新用户时关联的后台管理员IDsa_system_user.id可选
*/
public function loginByUsername(string $username, string $password, string $lang, float $coin, string $time): array
public function loginByUsername(string $username, string $password, string $lang, float $coin, string $time, ?int $adminId = null): array
{
$username = trim($username);
if ($username === '') {
@@ -72,6 +74,9 @@ class UserLogic
$player->password = $this->hashPassword($password);
$player->status = self::STATUS_NORMAL;
$player->coin = $coin;
if ($adminId !== null && $adminId > 0) {
$player->admin_id = $adminId;
}
$player->save();
}

View File

@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace app\dice\controller;
use app\dice\helper\AdminScopeHelper;
use app\dice\model\player\DicePlayer;
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
use app\dice\model\play_record\DicePlayRecord;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\service\Permission;
use support\Response;
use support\think\Db;
/**
* 大富翁工作台数据统计
*/
class DiceDashboardController extends BaseController
{
/**
* 工作台卡片统计:玩家注册、充值、提现、游玩次数(含较上周对比)
*/
#[Permission('工作台数据统计', 'core:console:list')]
public function statistics(): Response
{
$thisWeekStart = date('Y-m-d 00:00:00', strtotime('monday this week'));
$thisWeekEnd = date('Y-m-d 23:59:59', strtotime('sunday this week'));
$lastWeekStart = date('Y-m-d 00:00:00', strtotime('monday last week'));
$lastWeekEnd = date('Y-m-d 23:59:59', strtotime('sunday last week'));
$adminInfo = $this->adminInfo ?? null;
$playerQueryThis = DicePlayer::whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
$playerQueryLast = DicePlayer::whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
AdminScopeHelper::applyAdminScope($playerQueryThis, $adminInfo);
AdminScopeHelper::applyAdminScope($playerQueryLast, $adminInfo);
$playerThis = $playerQueryThis->count();
$playerLast = $playerQueryLast->count();
$chargeQueryThis = DicePlayerWalletRecord::where('type', 0)
->where('coin', '>', 0)
->whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
$chargeQueryLast = DicePlayerWalletRecord::where('type', 0)
->where('coin', '>', 0)
->whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
AdminScopeHelper::applyAdminScope($chargeQueryThis, $adminInfo);
AdminScopeHelper::applyAdminScope($chargeQueryLast, $adminInfo);
$chargeThis = $chargeQueryThis->sum('coin');
$chargeLast = $chargeQueryLast->sum('coin');
$withdrawQueryThis = DicePlayerWalletRecord::where('type', 1)
->whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
$withdrawQueryLast = DicePlayerWalletRecord::where('type', 1)
->whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
AdminScopeHelper::applyAdminScope($withdrawQueryThis, $adminInfo);
AdminScopeHelper::applyAdminScope($withdrawQueryLast, $adminInfo);
$withdrawThis = $withdrawQueryThis->sum(Db::raw('ABS(coin)'));
$withdrawLast = $withdrawQueryLast->sum(Db::raw('ABS(coin)'));
$playQueryThis = DicePlayRecord::whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
$playQueryLast = DicePlayRecord::whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
AdminScopeHelper::applyAdminScope($playQueryThis, $adminInfo);
AdminScopeHelper::applyAdminScope($playQueryLast, $adminInfo);
$playThis = $playQueryThis->count();
$playLast = $playQueryLast->count();
$playerChange = $this->calcWeekChange($playerThis, $playerLast);
$chargeChange = $this->calcWeekChange((float) $chargeThis, (float) $chargeLast);
$withdrawChange = $this->calcWeekChange((float) $withdrawThis, (float) $withdrawLast);
$playChange = $this->calcWeekChange($playThis, $playLast);
return $this->success([
'player_count' => $playerThis,
'player_count_change' => $playerChange,
'charge_amount' => (float) $chargeThis,
'charge_amount_change' => $chargeChange,
'withdraw_amount' => (float) $withdrawThis,
'withdraw_amount_change' => $withdrawChange,
'play_count' => $playThis,
'play_count_change' => $playChange,
]);
}
/**
* 近期玩家充值统计近10天每日充值金额
*/
#[Permission('工作台数据统计', 'core:console:list')]
public function rechargeChart(): Response
{
$adminInfo = $this->adminInfo ?? null;
$allowedIds = AdminScopeHelper::getAllowedAdminIds($adminInfo);
$adminCondition = '';
if ($allowedIds !== null) {
if (empty($allowedIds)) {
$data = [];
foreach (range(0, 9) as $n) {
$data[] = ['recharge_date' => date('Y-m-d', strtotime("-{$n} days")), 'recharge_amount' => 0];
}
$data = array_reverse($data);
return $this->success([
'recharge_amount' => array_map('floatval', array_column($data, 'recharge_amount')),
'recharge_date' => array_column($data, 'recharge_date'),
]);
}
$idsStr = implode(',', array_map('intval', $allowedIds));
$adminCondition = " AND w.admin_id IN ({$idsStr})";
}
$sql = "
SELECT
d.date AS recharge_date,
IFNULL(SUM(w.coin), 0) AS recharge_amount
FROM
(SELECT CURDATE() - INTERVAL (a.N) DAY AS date
FROM (SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
) d
LEFT JOIN dice_player_wallet_record w
ON DATE(w.create_time) = d.date AND w.type = 0 AND w.coin > 0 {$adminCondition}
GROUP BY d.date
ORDER BY d.date ASC
";
$data = Db::query($sql);
return $this->success([
'recharge_amount' => array_map('floatval', array_column($data, 'recharge_amount')),
'recharge_date' => array_column($data, 'recharge_date'),
]);
}
/**
* 月度玩家充值汇总当年1-12月每月充值金额
*/
#[Permission('工作台数据统计', 'core:console:list')]
public function rechargeBarChart(): Response
{
$adminInfo = $this->adminInfo ?? null;
$allowedIds = AdminScopeHelper::getAllowedAdminIds($adminInfo);
$adminCondition = '';
if ($allowedIds !== null) {
if (empty($allowedIds)) {
$data = [];
for ($m = 1; $m <= 12; $m++) {
$data[] = ['recharge_month' => sprintf('%02d月', $m), 'recharge_amount' => 0];
}
return $this->success([
'recharge_amount' => array_map('floatval', array_column($data, 'recharge_amount')),
'recharge_month' => array_column($data, 'recharge_month'),
]);
}
$idsStr = implode(',', array_map('intval', $allowedIds));
$adminCondition = " AND w.admin_id IN ({$idsStr})";
}
$sql = "
SELECT
CONCAT(LPAD(m.month_num, 2, '0'), '月') AS recharge_month,
IFNULL(SUM(w.coin), 0) AS recharge_amount
FROM
(SELECT 1 AS month_num UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12) m
LEFT JOIN dice_player_wallet_record w
ON YEAR(w.create_time) = YEAR(CURDATE())
AND MONTH(w.create_time) = m.month_num
AND w.type = 0 AND w.coin > 0 {$adminCondition}
GROUP BY m.month_num
ORDER BY m.month_num ASC
";
$data = Db::query($sql);
return $this->success([
'recharge_amount' => array_map('floatval', array_column($data, 'recharge_amount')),
'recharge_month' => array_column($data, 'recharge_month'),
]);
}
private function calcWeekChange($current, $last): float
{
if ($last == 0) {
return $current > 0 ? 100.0 : 0.0;
}
return round((($current - $last) / $last) * 100, 1);
}
}

View File

@@ -6,6 +6,7 @@
// +----------------------------------------------------------------------
namespace app\dice\controller\play_record;
use app\dice\helper\AdminScopeHelper;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\play_record\DicePlayRecordLogic;
use app\dice\validate\play_record\DicePlayRecordValidate;
@@ -53,6 +54,7 @@ class DicePlayRecordController extends BaseController
['direction', ''],
]);
$query = $this->logic->search($where);
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
$query->with([
'dicePlayer',
'diceRewardConfig',
@@ -68,7 +70,9 @@ class DicePlayRecordController extends BaseController
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
public function getPlayerOptions(Request $request): Response
{
$list = DicePlayer::field('id,username')->select();
$query = DicePlayer::field('id,username');
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
$list = $query->select();
$data = $list->map(function ($item) {
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
})->toArray();
@@ -115,12 +119,15 @@ class DicePlayRecordController extends BaseController
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
if (!$model) {
return $this->fail('未查找到信息');
}
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限查看该记录');
}
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
}
/**

View File

@@ -6,7 +6,9 @@
// +----------------------------------------------------------------------
namespace app\dice\controller\player;
use app\dice\helper\AdminScopeHelper;
use app\dice\model\lottery_config\DiceLotteryConfig;
use plugin\saiadmin\app\model\system\SystemUser;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\player\DicePlayerLogic;
use app\dice\validate\player\DicePlayerValidate;
@@ -44,6 +46,35 @@ class DicePlayerController extends BaseController
return $this->success($data);
}
/**
* 获取后台管理员选项SystemUser.id、username、realname供 admin_id 下拉使用
* 根据当前登录用户权限过滤(超级管理员可见全部,普通管理员按部门)
* @param Request $request
* @return Response 返回 [ ['id' => int, 'username' => string, 'realname' => string], ... ]
*/
#[Permission('大富翁-玩家列表', 'dice:player:index:index')]
public function getSystemUserOptions(Request $request): Response
{
$query = SystemUser::field('id,username,realname')->where('status', 1)->order('id', 'asc');
if (isset($this->adminInfo['id']) && (int) $this->adminInfo['id'] > 1) {
$deptList = $this->adminInfo['deptList'] ?? [];
if (!empty($deptList)) {
$query->auth($deptList);
}
}
$list = $query->select();
$data = $list->map(function ($item) {
$label = trim((string) ($item['realname'] ?? '')) ?: (string) ($item['username'] ?? '');
return [
'id' => (int) $item['id'],
'username' => (string) ($item['username'] ?? ''),
'realname' => (string) ($item['realname'] ?? ''),
'label' => $label ?: (string) $item['id'],
];
})->toArray();
return $this->success($data);
}
/**
* 数据列表
* @param Request $request
@@ -61,6 +92,7 @@ class DicePlayerController extends BaseController
['lottery_config_id', ''],
]);
$query = $this->logic->search($where);
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
$query->with(['diceLotteryConfig']);
$data = $this->logic->getList($query);
return $this->success($data);
@@ -76,12 +108,15 @@ class DicePlayerController extends BaseController
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
if (!$model) {
return $this->fail('未查找到信息');
}
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限查看该玩家');
}
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
}
/**
@@ -94,6 +129,10 @@ class DicePlayerController extends BaseController
{
$data = $request->post();
$this->validate('save', $data);
// 新增时若未选择管理员,默认使用当前登录用户
if (empty($data['admin_id']) && isset($this->adminInfo['id']) && (int) $this->adminInfo['id'] > 0) {
$data['admin_id'] = (int) $this->adminInfo['id'];
}
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
@@ -112,6 +151,13 @@ class DicePlayerController extends BaseController
{
$data = $request->post();
$this->validate('update', $data);
$model = $this->logic->read($data['id'] ?? 0);
if ($model) {
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限修改该玩家');
}
}
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
@@ -136,6 +182,13 @@ class DicePlayerController extends BaseController
if ($status === null || $status === '') {
return $this->fail('缺少 status');
}
$model = $this->logic->read($id);
if ($model) {
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限修改该玩家');
}
}
$this->logic->edit($id, ['status' => (int) $status]);
return $this->success('修改成功');
}
@@ -152,6 +205,22 @@ class DicePlayerController extends BaseController
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$ids = is_array($ids) ? $ids : explode(',', (string) $ids);
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null) {
$models = $this->logic->model->whereIn('id', $ids)->column('admin_id', 'id');
$validIds = [];
foreach ($ids as $id) {
$adminId = (int) ($models[$id] ?? 0);
if (in_array($adminId, $allowedIds, true)) {
$validIds[] = $id;
}
}
$ids = $validIds;
if (empty($ids)) {
return $this->fail('无权限删除所选玩家');
}
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');

View File

@@ -6,6 +6,7 @@
// +----------------------------------------------------------------------
namespace app\dice\controller\player_ticket_record;
use app\dice\helper\AdminScopeHelper;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\player_ticket_record\DicePlayerTicketRecordLogic;
use app\dice\validate\player_ticket_record\DicePlayerTicketRecordValidate;
@@ -51,6 +52,7 @@ class DicePlayerTicketRecordController extends BaseController
['create_time_max', ''],
]);
$query = $this->logic->search($where);
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
$query->with([
'dicePlayer',
]);
@@ -66,7 +68,9 @@ class DicePlayerTicketRecordController extends BaseController
#[Permission('抽奖券获取记录列表', 'dice:player_ticket_record:index:index')]
public function getPlayerOptions(Request $request): Response
{
$list = DicePlayer::field('id,username')->select();
$query = DicePlayer::field('id,username');
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
$list = $query->select();
$data = $list->map(function ($item) {
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
})->toArray();
@@ -83,12 +87,15 @@ class DicePlayerTicketRecordController extends BaseController
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
if (!$model) {
return $this->fail('未查找到信息');
}
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限查看该记录');
}
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
}
/**

View File

@@ -6,6 +6,7 @@
// +----------------------------------------------------------------------
namespace app\dice\controller\player_wallet_record;
use app\dice\helper\AdminScopeHelper;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\player_wallet_record\DicePlayerWalletRecordLogic;
use app\dice\validate\player_wallet_record\DicePlayerWalletRecordValidate;
@@ -47,6 +48,7 @@ class DicePlayerWalletRecordController extends BaseController
['create_time_max', ''],
]);
$query = $this->logic->search($where);
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
$query->with([
'dicePlayer',
'operator',
@@ -63,7 +65,9 @@ class DicePlayerWalletRecordController extends BaseController
#[Permission('玩家钱包流水列表', 'dice:player_wallet_record:index:index')]
public function getPlayerOptions(Request $request): Response
{
$list = DicePlayer::field('id,username')->select();
$query = DicePlayer::field('id,username');
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
$list = $query->select();
$data = $list->map(function ($item) {
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
})->toArray();
@@ -83,10 +87,14 @@ class DicePlayerWalletRecordController extends BaseController
if ($playerId === null || $playerId === '') {
return $this->fail('缺少 player_id');
}
$player = DicePlayer::field('coin')->where('id', $playerId)->find();
$player = DicePlayer::field('coin,admin_id')->where('id', $playerId)->find();
if (!$player) {
return $this->fail('玩家不存在');
}
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($player->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限操作该玩家');
}
return $this->success(['wallet_before' => (float) $player['coin']]);
}
@@ -100,12 +108,15 @@ class DicePlayerWalletRecordController extends BaseController
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
if (!$model) {
return $this->fail('未查找到信息');
}
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限查看该记录');
}
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
}
/**
@@ -155,6 +166,14 @@ class DicePlayerWalletRecordController extends BaseController
return $this->fail('请先登录');
}
$player = DicePlayer::field('admin_id')->where('id', $playerId)->find();
if ($player) {
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($player->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限操作该玩家');
}
}
try {
$this->logic->adminOperate($data, $adminId);
return $this->success('操作成功');

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace app\dice\helper;
use plugin\saiadmin\app\model\system\SystemUser;
/**
* 管理员数据范围辅助类
* 用于获取当前管理员及其部门下属管理员可访问的数据范围
*/
class AdminScopeHelper
{
/**
* 获取当前管理员可访问的 admin_id 列表
* 超级管理员(id=1) 返回 null 表示不限制
* 普通管理员返回其本人及部门下属管理员的 id 列表
*
* @param array|null $adminInfo 当前登录管理员信息(含 id、deptList
* @return int[]|null null=不限制(超级管理员),否则为可访问的 admin_id 数组
*/
public static function getAllowedAdminIds(?array $adminInfo): ?array
{
if (empty($adminInfo) || !isset($adminInfo['id'])) {
return [];
}
$adminId = (int) $adminInfo['id'];
if ($adminId <= 1) {
return null;
}
$deptList = $adminInfo['deptList'] ?? [];
if (empty($deptList) || !isset($deptList['id'])) {
return [$adminId];
}
$query = SystemUser::field('id');
$query->auth($deptList);
$ids = $query->column('id');
return array_map('intval', $ids ?: []);
}
/**
* 对查询应用 admin_id 范围过滤
*
* @param object $query ThinkORM 查询对象
* @param array|null $adminInfo 当前登录管理员信息
* @return void
*/
public static function applyAdminScope($query, ?array $adminInfo): void
{
$allowedIds = self::getAllowedAdminIds($adminInfo);
if ($allowedIds === null) {
return;
}
if (empty($allowedIds)) {
$query->whereRaw('1=0');
return;
}
$query->whereIn('admin_id', $allowedIds);
}
}

View File

@@ -73,8 +73,10 @@ class DicePlayerWalletRecordLogic extends BaseLogic
DicePlayer::where('id', $playerId)->update(['coin' => $walletAfter]);
$playerAdminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
$record = [
'player_id' => $playerId,
'admin_id' => $playerAdminId,
'coin' => $type === 3 ? $coin : -$coin,
'type' => $type,
'wallet_before' => $walletBefore,

View File

@@ -19,6 +19,7 @@ use think\model\relation\BelongsTo;
*
* @property $id ID
* @property $player_id 玩家id
* @property $admin_id 关联玩家所属管理员IDDicePlayer.admin_id
* @property $lottery_config_id 彩金池配置
* @property $lottery_type 抽奖类型
* @property $is_win 是否中大奖:豹子号[1,1,1,1,1]~[6,6,6,6,6]为1否则0

View File

@@ -22,6 +22,7 @@ use app\dice\model\lottery_config\DiceLotteryConfig;
* @property $password 密码
* @property $status 状态
* @property $coin 平台币
* @property $admin_id 创建该玩家的后台管理员ID关联 sa_system_user.id
* @property $lottery_config_id 彩金池配置ID0或null时使用自定义权重*_weight
* @property $t1_weight T1池权重
* @property $t2_weight T2池权重

View File

@@ -17,6 +17,7 @@ use think\model\relation\BelongsTo;
*
* @property $id ID
* @property $player_id 玩家id
* @property $admin_id 关联玩家所属管理员IDDicePlayer.admin_id
* @property $use_coins 消耗硬币
* @property $total_ticket_count 总抽奖次数
* @property $paid_ticket_count 购买抽奖次数

View File

@@ -18,6 +18,7 @@ use think\model\relation\BelongsTo;
*
* @property $id ID
* @property $player_id 用户id
* @property $admin_id 关联玩家所属管理员IDDicePlayer.admin_id
* @property $coin 平台币变化
* @property $type 类型:0=充值 1=提现 2=购买抽奖次数
* @property $wallet_before 钱包操作前

View File

@@ -17,6 +17,10 @@ Route::group('/core', function () {
Route::get('/system/statistics', [plugin\saiadmin\app\controller\SystemController::class, 'statistics']);
Route::get('/system/loginChart', [plugin\saiadmin\app\controller\SystemController::class, 'loginChart']);
Route::get('/system/loginBarChart', [plugin\saiadmin\app\controller\SystemController::class, 'loginBarChart']);
// 大富翁工作台统计(覆盖默认统计)
Route::get('/dice/dashboard/statistics', [\app\dice\controller\DiceDashboardController::class, 'statistics']);
Route::get('/dice/dashboard/rechargeChart', [\app\dice\controller\DiceDashboardController::class, 'rechargeChart']);
Route::get('/dice/dashboard/rechargeBarChart', [\app\dice\controller\DiceDashboardController::class, 'rechargeBarChart']);
Route::get('/system/clearAllCache', [plugin\saiadmin\app\controller\SystemController::class, 'clearAllCache']);
Route::get("/system/getResourceCategory", [plugin\saiadmin\app\controller\SystemController::class, 'getResourceCategory']);
@@ -87,6 +91,7 @@ Route::group('/core', function () {
fastRoute('dice/player/DicePlayer', \app\dice\controller\player\DicePlayerController::class);
Route::put('/dice/player/DicePlayer/updateStatus', [\app\dice\controller\player\DicePlayerController::class, 'updateStatus']);
Route::get('/dice/player/DicePlayer/getLotteryConfigOptions', [\app\dice\controller\player\DicePlayerController::class, 'getLotteryConfigOptions']);
Route::get('/dice/player/DicePlayer/getSystemUserOptions', [\app\dice\controller\player\DicePlayerController::class, 'getSystemUserOptions']);
fastRoute('dice/play_record/DicePlayRecord', \app\dice\controller\play_record\DicePlayRecordController::class);
Route::get('/dice/play_record/DicePlayRecord/getPlayerOptions', [\app\dice\controller\play_record\DicePlayRecordController::class, 'getPlayerOptions']);
Route::get('/dice/play_record/DicePlayRecord/getLotteryConfigOptions', [\app\dice\controller\play_record\DicePlayRecordController::class, 'getLotteryConfigOptions']);