新增职务权限管理与聊天室管理权限控制

This commit is contained in:
2026-04-21 16:43:17 +08:00
parent cfdbf387af
commit 281315d1cf
19 changed files with 1243 additions and 87 deletions
@@ -13,9 +13,14 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\StoreBaccaratLossCoverEventRequest;
use App\Models\BaccaratLossCoverEvent;
use App\Services\BaccaratLossCoverService;
use App\Services\PositionPermissionService;
use App\Support\PositionPermissionRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* 类功能:处理聊天室顶部快捷入口创建与结束百家乐买单活动。
*/
class BaccaratLossCoverEventController extends Controller
{
/**
@@ -23,6 +28,7 @@ class BaccaratLossCoverEventController extends Controller
*/
public function __construct(
private readonly BaccaratLossCoverService $lossCoverService,
private readonly PositionPermissionService $positionPermissionService,
) {}
/**
@@ -30,6 +36,13 @@ class BaccaratLossCoverEventController extends Controller
*/
public function store(StoreBaccaratLossCoverEventRequest $request): JsonResponse
{
if (! $this->positionPermissionService->hasPermission($request->user(), PositionPermissionRegistry::ROOM_BACCARAT_LOSS_COVER)) {
return response()->json([
'ok' => false,
'message' => '当前职务无权创建买单活动。',
], 403);
}
try {
$event = $this->lossCoverService->createEvent($request->user(), $request->validated());
} catch (\RuntimeException $exception) {
@@ -51,6 +64,13 @@ class BaccaratLossCoverEventController extends Controller
*/
public function close(Request $request, BaccaratLossCoverEvent $event): JsonResponse
{
if (! $this->positionPermissionService->hasPermission($request->user(), PositionPermissionRegistry::ROOM_BACCARAT_LOSS_COVER)) {
return response()->json([
'ok' => false,
'message' => '当前职务无权结束买单活动。',
], 403);
}
$event = $this->lossCoverService->forceCloseEvent($event, $request->user());
return response()->json([
@@ -16,10 +16,15 @@ use App\Http\Controllers\Controller;
use App\Models\Department;
use App\Models\Position;
use App\Models\Sysparam;
use App\Support\PositionPermissionRegistry;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
/**
* 类功能:负责后台职务资料、任命白名单与聊天室权限配置的维护。
*/
class PositionController extends Controller
{
/**
@@ -29,16 +34,25 @@ class PositionController extends Controller
{
// 按部门分组展示
$departments = Department::with([
'positions' => fn ($q) => $q->withCount(['activeUserPositions'])->ordered(),
'positions' => fn ($q) => $q->withCount(['activeUserPositions'])->with('appointablePositions')->ordered(),
])->ordered()->get();
// 全部职务(供任命白名单多选框使用)
$allPositions = Position::with('department')->orderByDesc('rank')->get();
$allPositions = Position::with('department')->ordered()->get();
// 全局奖励接收次数上限(0 = 不限)
$globalRecipientDailyMax = (int) Sysparam::getValue('reward_recipient_daily_max', '0');
return view('admin.positions.index', compact('departments', 'allPositions', 'globalRecipientDailyMax'));
$positionPermissions = PositionPermissionRegistry::groupedDefinitions();
$permissionLabels = PositionPermissionRegistry::labelMap();
return view('admin.positions.index', compact(
'departments',
'allPositions',
'globalRecipientDailyMax',
'positionPermissions',
'permissionLabels',
));
}
/**
@@ -59,10 +73,13 @@ class PositionController extends Controller
'sort_order' => 'required|integer|min:0',
'appointable_ids' => 'nullable|array',
'appointable_ids.*' => 'exists:positions,id',
'permissions' => 'nullable|array',
'permissions.*' => ['string', Rule::in(PositionPermissionRegistry::codes())],
]);
$appointableIds = $data['appointable_ids'] ?? [];
unset($data['appointable_ids']);
$data['permissions'] = array_values(array_unique($data['permissions'] ?? []));
$position = Position::create($data);
@@ -147,10 +164,13 @@ class PositionController extends Controller
'sort_order' => 'required|integer|min:0',
'appointable_ids' => 'nullable|array',
'appointable_ids.*' => 'exists:positions,id',
'permissions' => 'nullable|array',
'permissions.*' => ['string', Rule::in(PositionPermissionRegistry::codes())],
]);
$appointableIds = $data['appointable_ids'] ?? [];
unset($data['appointable_ids']);
$data['permissions'] = array_values(array_unique($data['permissions'] ?? []));
$position->update($data);
$position->appointablePositions()->sync($appointableIds);
+43 -20
View File
@@ -4,7 +4,7 @@
* 文件功能:管理员聊天室实时命令控制器
*
* 提供管理员在聊天室内对用户执行的管理操作:
* 警告(=J)、踢出(=T)、禁言(=B)、冻结(=Y)、查看私信(=S)站长公屏讲话。
* 警告(=J)、踢出(=T)、禁言(=B)、冻结(=Y)、查看私信(=S)职务公屏讲话。
*
* 对应原 ASP 文件:DOUSER.ASP / KILLUSER.ASP / LOCKIP.ASP / NEWSAY.ASP
*
@@ -24,13 +24,18 @@ use App\Models\PositionAuthorityLog;
use App\Models\Sysparam;
use App\Models\User;
use App\Services\ChatStateService;
use App\Services\PositionPermissionService;
use App\Services\UserCurrencyService;
use App\Support\PositionPermissionRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redis;
use Illuminate\Validation\Rule;
/**
* 类功能:处理聊天室内的实时管理命令与部分职务奖励操作。
*/
class AdminCommandController extends Controller
{
/**
@@ -39,6 +44,7 @@ class AdminCommandController extends Controller
public function __construct(
private readonly ChatStateService $chatState,
private readonly UserCurrencyService $currencyService,
private readonly PositionPermissionService $positionPermissionService,
) {}
/**
@@ -347,9 +353,10 @@ class AdminCommandController extends Controller
}
/**
* 站长公屏讲话
* 聊天室公屏讲话
*
* 站长发送全聊天室公告,以特殊样式显示。
* 拥有 room.public_broadcast 权限的职务可以发送全聊天室公告,
* id=1 站长仍然拥有完整兜底权限。
*
* @param Request $request 请求对象,需包含 content, room_id
* @return JsonResponse 操作结果
@@ -362,22 +369,21 @@ class AdminCommandController extends Controller
]);
$admin = Auth::user();
$superLevel = (int) Sysparam::getValue('superlevel', '100');
if ($admin->user_level < $superLevel) {
return response()->json(['status' => 'error', 'message' => '仅站长可发布公屏讲话'], 403);
if (! $this->positionPermissionService->hasPermission($admin, PositionPermissionRegistry::ROOM_PUBLIC_BROADCAST)) {
return response()->json(['status' => 'error', 'message' => '当前职务无权发布公屏讲话'], 403);
}
$roomId = $request->input('room_id');
$content = $request->input('content');
// 广播站长公告
// 按当前在职职务拼装发布者身份,避免继续显示为固定“站长公告
$publisherLabel = $this->buildAnnouncementPublisherLabel($admin);
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统公告',
'to_user' => '大家',
'content' => "📢 站长 <b>{$admin->username}</b> 讲话{$content}",
'content' => "📢 <b>{$publisherLabel}</b> <b>{$admin->username}</b> 发布公告{$content}",
'is_secret' => false,
'font_color' => '#b91c1c',
'action' => '',
@@ -390,6 +396,28 @@ class AdminCommandController extends Controller
return response()->json(['status' => 'success', 'message' => '公告已发送']);
}
/**
* 生成公屏公告发布者身份标签。
*
* 普通在职用户按“部门+职务”显示;站长无在职职务时保持“站长”标识兜底。
*/
private function buildAnnouncementPublisherLabel(User $user): string
{
$position = $user->activePosition?->position;
if ($position) {
$departmentName = (string) ($position->department?->name ?? '');
return $departmentName.$position->name;
}
if ($user->id === 1) {
return '站长';
}
return '管理员';
}
/**
* 管理员全员清屏
*
@@ -407,11 +435,9 @@ class AdminCommandController extends Controller
$admin = Auth::user();
$roomId = $request->input('room_id');
$superLevel = (int) Sysparam::getValue('superlevel', '100');
// 需要站长权限才能全员清屏
if ($admin->user_level < $superLevel) {
return response()->json(['status' => 'error', 'message' => '仅站长可执行全员清屏'], 403);
// 改为按职务权限控制聊天室顶部“清屏”按钮。
if (! $this->positionPermissionService->hasPermission($admin, PositionPermissionRegistry::ROOM_CLEAR_SCREEN)) {
return response()->json(['status' => 'error', 'message' => '当前职务无权执行全员清屏'], 403);
}
// 清除 Redis 中该房间的消息缓存
@@ -427,7 +453,7 @@ class AdminCommandController extends Controller
* 管理员触发全屏特效。
*
* 向房间内所有用户广播 EffectBroadcast 事件,前端收到后播放对应 Canvas 动画。
* superlevel 等级管理员可触发。
* 拥有 room.fullscreen_effect 权限的职务可触发。
*
* @param Request $request 请求对象,需包含 room_id, type
* @return JsonResponse 操作结果
@@ -442,11 +468,8 @@ class AdminCommandController extends Controller
$admin = Auth::user();
$roomId = $request->input('room_id');
$type = $request->input('type');
$superLevel = (int) Sysparam::getValue('superlevel', '100');
// 仅 superlevel 等级可触发特效
if ($admin->user_level < $superLevel) {
return response()->json(['status' => 'error', 'message' => '仅站长可触发特效'], 403);
if (! $this->positionPermissionService->hasPermission($admin, PositionPermissionRegistry::ROOM_FULLSCREEN_EFFECT)) {
return response()->json(['status' => 'error', 'message' => '当前职务无权触发特效'], 403);
}
// 广播特效事件给房间内所有在线用户
+12 -5
View File
@@ -26,9 +26,11 @@ use App\Models\User;
use App\Services\AppointmentService;
use App\Services\ChatStateService;
use App\Services\MessageFilterService;
use App\Services\PositionPermissionService;
use App\Services\RoomBroadcastService;
use App\Services\UserCurrencyService;
use App\Services\VipService;
use App\Support\PositionPermissionRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
@@ -58,6 +60,7 @@ class ChatController extends Controller
private readonly UserCurrencyService $currencyService,
private readonly AppointmentService $appointmentService,
private readonly RoomBroadcastService $broadcast,
private readonly PositionPermissionService $positionPermissionService,
) {}
/**
@@ -278,7 +281,9 @@ class ChatController extends Controller
];
}
// 渲染主聊天框架视图
// 渲染主聊天框架视图前,先计算当前用户的聊天室顶部管理权限。
$roomPermissionMap = $this->positionPermissionService->permissionMapForUser($user);
return view('chat.frame', [
'room' => $room,
'user' => $user,
@@ -289,6 +294,8 @@ class ChatController extends Controller
'historyMessages' => $historyMessages,
'pendingProposal' => $pendingProposalData,
'pendingDivorce' => $pendingDivorceData,
'roomPermissionMap' => $roomPermissionMap,
'hasRoomManagementPermission' => in_array(true, $roomPermissionMap, true),
]);
}
@@ -892,7 +899,8 @@ class ChatController extends Controller
/**
* 设置房间公告/祝福语(滚动显示在聊天室顶部)
* 需要房间主人或等级达到 level_announcement 配置值
* 需要当前在职职务拥有 room.announcement 权限,
* id=1 站长始终允许操作。
*
* @param int $id 房间ID
*/
@@ -901,9 +909,8 @@ class ChatController extends Controller
$user = Auth::user();
$room = Room::findOrFail($id);
// 权限检查:房间主人 或 等级 >= level_announcement
$requiredLevel = (int) Sysparam::getValue('level_announcement', '10');
if ($user->username !== $room->master && $user->user_level < $requiredLevel) {
// 改为统一走职务权限判断,不再给房主单独保留公告特权。
if (! $this->positionPermissionService->hasPermission($user, PositionPermissionRegistry::ROOM_ANNOUNCEMENT)) {
return response()->json(['status' => 'error', 'message' => '权限不足,无法修改公告'], 403);
}
+8 -7
View File
@@ -4,7 +4,7 @@
* 文件功能:聊天室礼包(红包)控制器
*
* 提供两个核心接口:
* - send() superlevel 站长凭空发出 888 数量 10 份礼包(金币 or 经验)
* - send() 拥有权限的职务用户凭空发出 8888 数量 10 份礼包(金币 or 经验)
* - claim() :在线用户抢礼包(先到先得,每人一份)
*
* 接入 UserCurrencyService 记录所有货币变动流水。
@@ -23,9 +23,10 @@ use App\Events\RedPacketSent;
use App\Jobs\SaveMessageJob;
use App\Models\RedPacketClaim;
use App\Models\RedPacketEnvelope;
use App\Models\Sysparam;
use App\Services\ChatStateService;
use App\Services\PositionPermissionService;
use App\Services\UserCurrencyService;
use App\Support\PositionPermissionRegistry;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -54,10 +55,11 @@ class RedPacketController extends Controller
public function __construct(
private readonly ChatStateService $chatState,
private readonly UserCurrencyService $currencyService,
private readonly PositionPermissionService $positionPermissionService,
) {}
/**
* superlevel 站长凭空发出礼包。
* 拥有权限的职务用户凭空发出礼包。
*
* 不扣发包人自身货币,888 数量凭空发出分 10 份。
* type 参数决定本次发出的是金币(gold)还是经验(exp)。
@@ -75,10 +77,9 @@ class RedPacketController extends Controller
$roomId = (int) $request->input('room_id');
$type = $request->input('type'); // 'gold' 或 'exp'
// 权限校验:仅 superlevel 可发礼包
$superLevel = (int) Sysparam::getValue('superlevel', '100');
if ($user->user_level < $superLevel) {
return response()->json(['status' => 'error', 'message' => '仅站长可发礼包红包'], 403);
// 改为按职务权限码控制礼包发放。
if (! $this->positionPermissionService->hasPermission($user, PositionPermissionRegistry::ROOM_RED_PACKET)) {
return response()->json(['status' => 'error', 'message' => '当前职务无权发礼包红包'], 403);
}
// 检查该用户在此房间是否有进行中的红包(防止刷包)
+22 -1
View File
@@ -3,7 +3,8 @@
/**
* 文件功能:职务模型
* 对应 positions 表,职务属于某个部门,包含等级、图标、人数上限和奖励上限
* 任命权限通过 position_appoint_limits 中间表多对多关联定义
* 任命权限通过 position_appoint_limits 中间表多对多关联定义
* 聊天室顶部管理权限通过 permissions JSON 字段配置
*
* @author ChatRoom Laravel
*
@@ -35,6 +36,7 @@ class Position extends Model
'daily_reward_limit',
'recipient_daily_limit',
'sort_order',
'permissions',
];
/**
@@ -50,6 +52,7 @@ class Position extends Model
'daily_reward_limit' => 'integer',
'recipient_daily_limit' => 'integer',
'sort_order' => 'integer',
'permissions' => 'array',
];
}
@@ -123,6 +126,24 @@ class Position extends Model
return $this->currentCount() >= $this->max_persons;
}
/**
* 判断当前职务是否拥有指定权限码。
*/
public function hasPermission(string $permission): bool
{
return in_array($permission, $this->permissions ?? [], true);
}
/**
* 返回当前职务的权限码列表。
*
* @return list<string>
*/
public function permissionCodes(): array
{
return array_values($this->permissions ?? []);
}
/**
* 查询范围:按位阶降序
*/
@@ -0,0 +1,92 @@
<?php
/**
* 文件功能:职务权限判断服务
* 负责把当前用户的在职职务权限转换为统一布尔判断,
* 供控制器和 Blade 视图复用。
*/
namespace App\Services;
use App\Models\User;
use App\Support\PositionPermissionRegistry;
/**
* 类功能:统一提供用户职务权限读取与校验方法。
*/
class PositionPermissionService
{
/**
* 返回当前用户拥有的全部聊天室权限码。
*
* 规则:
* - id=1 站长始终拥有全部权限
* - 其他用户仅按当前在职职务的 permissions 生效
*
* @return list<string>
*/
public function permissionsForUser(?User $user): array
{
if (! $user) {
return [];
}
if ($user->id === 1) {
return PositionPermissionRegistry::codes();
}
$position = $user->activePosition?->position;
if (! $position) {
return [];
}
return array_values(array_intersect(
PositionPermissionRegistry::codes(),
$position->permissions ?? [],
));
}
/**
* 返回当前用户全部权限的布尔映射表。
*
* @return array<string, bool>
*/
public function permissionMapForUser(?User $user): array
{
$permissionMap = array_fill_keys(PositionPermissionRegistry::codes(), false);
foreach ($this->permissionsForUser($user) as $permission) {
$permissionMap[$permission] = true;
}
return $permissionMap;
}
/**
* 判断用户是否拥有指定权限码。
*/
public function hasPermission(?User $user, string $permission): bool
{
if (! in_array($permission, PositionPermissionRegistry::codes(), true)) {
return false;
}
return in_array($permission, $this->permissionsForUser($user), true);
}
/**
* 判断用户是否至少拥有一项指定权限。
*
* @param list<string> $permissions
*/
public function hasAnyPermission(?User $user, array $permissions): bool
{
foreach ($permissions as $permission) {
if ($this->hasPermission($user, $permission)) {
return true;
}
}
return false;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
/**
* 文件功能:职务权限码注册表
* 统一维护聊天室顶部管理菜单可配置的全部权限码、
* 中文标题、说明文案与默认分配规则,便于后续继续扩展。
*/
namespace App\Support;
/**
* 类功能:集中定义职务权限元数据与默认权限策略。
*/
class PositionPermissionRegistry
{
/**
* 房间公告权限。
*/
public const ROOM_ANNOUNCEMENT = 'room.announcement';
/**
* 房间公屏讲话权限。
*/
public const ROOM_PUBLIC_BROADCAST = 'room.public_broadcast';
/**
* 房间全员清屏权限。
*/
public const ROOM_CLEAR_SCREEN = 'room.clear_screen';
/**
* 礼包红包权限。
*/
public const ROOM_RED_PACKET = 'room.red_packet';
/**
* 百家乐买单活动权限。
*/
public const ROOM_BACCARAT_LOSS_COVER = 'room.baccarat_loss_cover';
/**
* 全屏特效权限。
*/
public const ROOM_FULLSCREEN_EFFECT = 'room.fullscreen_effect';
/**
* 返回全部权限定义。
*
* @return array<string, array{group: string, label: string, description: string}>
*/
public static function definitions(): array
{
return [
self::ROOM_ANNOUNCEMENT => [
'group' => '聊天室管理',
'label' => '设置公告',
'description' => '允许修改聊天室顶部滚动公告。',
],
self::ROOM_PUBLIC_BROADCAST => [
'group' => '聊天室管理',
'label' => '公屏讲话',
'description' => '允许在聊天室内发送管理员公屏讲话。',
],
self::ROOM_CLEAR_SCREEN => [
'group' => '聊天室管理',
'label' => '全员清屏',
'description' => '允许清除当前房间所有人的普通聊天记录。',
],
self::ROOM_RED_PACKET => [
'group' => '活动管理',
'label' => '礼包红包',
'description' => '允许在聊天室内发出金币或经验礼包。',
],
self::ROOM_BACCARAT_LOSS_COVER => [
'group' => '活动管理',
'label' => '买单活动',
'description' => '允许创建和结束百家乐买单活动。',
],
self::ROOM_FULLSCREEN_EFFECT => [
'group' => '全屏特效',
'label' => '全屏特效',
'description' => '允许触发聊天室内全部全屏动画特效。',
],
];
}
/**
* 返回全部权限码列表。
*
* @return list<string>
*/
public static function codes(): array
{
return array_keys(self::definitions());
}
/**
* 返回权限码到中文标题的映射。
*
* @return array<string, string>
*/
public static function labelMap(): array
{
$labels = [];
foreach (self::definitions() as $code => $definition) {
$labels[$code] = $definition['label'];
}
return $labels;
}
/**
* 按分组返回权限定义,供后台表单渲染。
*
* @return array<string, array<string, array{label: string, description: string}>>
*/
public static function groupedDefinitions(): array
{
$grouped = [];
foreach (self::definitions() as $code => $definition) {
$grouped[$definition['group']][$code] = [
'label' => $definition['label'],
'description' => $definition['description'],
];
}
return $grouped;
}
/**
* 将权限码数组转换为中文标题列表。
*
* @param list<string> $codes
* @return list<string>
*/
public static function summaryLabels(array $codes): array
{
$labels = self::labelMap();
return array_values(array_map(
fn (string $code): string => $labels[$code] ?? $code,
array_values(array_intersect(self::codes(), $codes))
));
}
/**
* 根据职务等级返回默认权限。
*
* 默认策略:
* - Lv.60 及以上默认拥有「设置公告」
* - Lv.97 及以上默认拥有顶部管理菜单全部权限
*
* @return list<string>
*/
public static function defaultPermissionsForLevel(int $level): array
{
if ($level >= 97) {
return self::codes();
}
if ($level >= 60) {
return [self::ROOM_ANNOUNCEMENT];
}
return [];
}
}