# ChatRoom 开发计划与注意事项

> **技术栈**：Laravel 12 · PHP 8.4 · Laravel Reverb (WebSocket) · Redis · MySQL 8.0 · Laravel Horizon
> **参照原项目**：`/Users/pllx/Web/chat/hp0709`（VBScript ASP 聊天室，仅作功能参考，不做数据迁移）
> **目标域名**：`http://chatroom.test`（Herd 自动配置）| `https://chat.ay.lc`（生产环境）

---

## 一、环境版本要求

| 组件                  | 版本                       |
| --------------------- | -------------------------- |
| **PHP**               | 8.4.5+                     |
| **Laravel Framework** | v12.x                      |
| **Laravel Reverb**    | latest（WebSocket 服务器） |
| **Laravel Horizon**   | v5（Redis 队列可视化管理） |
| **PHPUnit**           | v11（测试框架）            |
| **Node.js**           | 20.x LTS                   |
| **MySQL**             | 8.0+                       |
| **Redis**             | 7.x                        |

---

## 二、代码规范（强制执行）

### 2.1 Laravel Pint 格式化

```bash
# 提交代码前必须运行，修复格式问题
vendor/bin/pint --dirty

# 检查格式问题（不修复）
vendor/bin/pint --test

# 格式化整个项目
vendor/bin/pint
```

### 2.2 PHP 8.4 类型系统（必须遵守）

```php
// ✅ 正确：构造函数属性提升 (Constructor Property Promotion)
class ChatController extends Controller
{
    public function __construct(
        private readonly ChatStateService    $chatState,
        private readonly MessageFilterService $filter,
    ) {}
}

// ❌ 错误：不允许无参空构造函数
class SomeClass
{
    public function __construct() {} // 禁止！
}

// ✅ 正确：显式返回类型 + 参数类型提示
public function send(SendMessageRequest $request): JsonResponse
{
    // ...
}

// ✅ 正确：使用 PHP 8.4 新特性
// 联合类型
public function findUser(int|string $id): User|null {}

// readonly 属性
class MessageDto
{
    public function __construct(
        public readonly string $content,
        public readonly string $fromUser,
        public readonly int    $roomId,
    ) {}
}
```

### 2.3 Laravel 12 中间件配置（重要）

> [!IMPORTANT]
> Laravel 12 已废弃 `Kernel.php`，中间件在 `bootstrap/app.php` 中配置。

```php
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',          // API 路由
        channels: __DIR__.'/../routes/channels.php', // WebSocket 频道
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        // 注册聊天室登录验证中间件
        $middleware->alias([
            'chat.auth'       => \App\Http\Middleware\ChatAuthenticate::class,
            'chat.level'      => \App\Http\Middleware\LevelRequired::class,
            'chat.site_owner' => \App\Http\Middleware\SiteOwnerOnly::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();
```

### 2.4 中文注释规范（每个文件必须）

```php
<?php

/**
 * 文件功能：[本文件的业务职责描述]
 *
 * 对应原 ASP 文件：[原文件名.asp]
 *
 * @package  App\[命名空间]
 * @author   ChatRoom Laravel
 * @version  1.0.0
 */

namespace App\Services;

class ChatStateService
{
    /**
     * 用户进入聊天房间，将其信息写入 Redis。
     *
     * 替代原 ASP 的 Application("_user_list") 字符串拼接操作。
     *
     * @param  int    $roomId    房间 ID
     * @param  string $username  用户名
     * @param  array  $userInfo  用户信息（等级、头像、性别等）
     */
    public function userJoin(int $roomId, string $username, array $userInfo): void
    {
        // 将用户信息序列化后存入 Redis Hash，Key 为 "room:{房间ID}:users"
        $this->redis->hset("room:{$roomId}:users", $username, json_encode($userInfo));
    }
}
```

---

## 三、首次启动（必须先执行）

```bash
cd /Users/pllx/Web/Herd/chatroom

# 安装 PHP 依赖（已完成）
composer install

# 创建数据库（已完成）
mysql -u root -proot -e "CREATE DATABASE IF NOT EXISTS chatroom CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

# 运行数据库迁移 + 基础数据填充
php artisan migrate --seed

# 安装前端依赖并构建（已完成）
npm install
npm run build
```

**开发时运行的服务**：

```bash
php artisan reverb:start --debug   # WebSocket 服务器 :8080
php artisan horizon                 # 队列 Worker（含 Web 监控 /horizon）
npm run dev                        # Vite 热更新（开发阶段）
# HTTP 由 Herd 自动提供 chatroom.test
```

---

## 四、数据库迁移对照表

**原 Access 表** → **Laravel Migration** 对应关系：

| 原 ASP 表   | Laravel 迁移文件                   | Model 类            | 说明                               |
| ----------- | ---------------------------------- | ------------------- | ---------------------------------- |
| `user`      | `create_users_table`               | `User`              | 主用户表（含 jjb 金币 / exp 经验） |
| `room`      | `create_rooms_table`               | `Room`              | 聊天房间                           |
| _(内存)_    | `create_messages_table`            | `Message`           | 消息归档（实时用 Redis）           |
| `sysparam`  | `create_sys_params_table`          | `Sysparam`          | 系统参数（alias/guidetxt/body）    |
| `ip_lock`   | `create_ip_locks_table`            | `IpLock`            | IP 封锁                            |
| `record`    | `create_audit_logs_table`          | `AuditLog`          | 管理操作日志                       |
| `guestbook` | `create_guestbooks_table`          | `Guestbook`         | 留言板                             |
| `calls`     | `create_friend_calls_table`        | `FriendCall`        | 好友呼叫                           |
| `friendrq`  | `create_friend_requests_table`     | `FriendRequest`     | 好友申请                           |
| `action`    | `create_actions_table`             | `Action`            | 表情/动作定义                      |
| `admincz`   | `create_admin_logs_table`          | `AdminLog`          | 管理员操作统计                     |
| `gg`        | `create_user_items_table`          | `UserItem`          | 道具（封口令等）                   |
| `scrollad`  | `create_scroll_ads_table`          | `ScrollAd`          | 滚动公告                           |
| `hy` / `lh` | `create_marriages_table`           | `Marriage`          | 婚姻关系                           |
| `ip`        | `create_ip_logs_table`             | `IpLog`             | IP 登录日志                        |
| `room_des`  | `create_room_descriptions_table`   | `RoomDescription`   | 房间描述模板                       |
| `autoact`   | `create_autoacts_table`            | `Autoact`           | 机器人随机事件                     |
| _(新增)_    | `create_gifts_table`               | `Gift`              | 礼物/鲜花定义（名称/图标/金币）    |
| _(新增)_    | `create_shop_items_table`          | `ShopItem`          | 商店商品（特效卡/改名卡等）        |
| _(新增)_    | `create_user_purchases_table`      | `UserPurchase`      | 用户购买记录                       |
| _(新增)_    | `create_vip_levels_table`          | `VipLevel`          | VIP 会员等级（倍率/进入模板）      |
| _(新增)_    | `create_ai_provider_configs_table` | `AiProviderConfig`  | AI 服务商配置（多厂商支持）        |
| _(新增)_    | `create_ai_usage_logs_table`       | `AiUsageLog`        | AI 使用日志（Token 用量记录）      |
| _(新增)_    | `create_username_blacklist_table`  | `UsernameBlacklist` | 用户名黑名单                       |
| _(新增)_    | `create_user_currency_logs_table`  | `UserCurrencyLog`   | 积分流水（经验/金币/魅力变动记录） |

---

## 五、推荐目录结构

```
app/
├── Events/                         # WebSocket 广播事件（ShouldBroadcast）
│   ├── MessageSent.php             # 消息发送（替代 NEWSAY.ASP）
│   ├── UserJoined.php              # 用户进房（替代 INIT.ASP）
│   ├── UserLeft.php                # 用户离开（替代 LEAVE.ASP）
│   ├── UserKicked.php              # 踢人
│   ├── UserMuted.php               # 封口
│   ├── UserLevelUp.php             # 用户升级通知
│   ├── RoomTitleUpdated.php        # 房间标题更新
│   ├── ScreenCleared.php           # 管理员清屏
│   └── EffectBroadcast.php         # 特效广播（烟花/下雨等）
│
├── Services/                       # 业务逻辑服务层（纯 PHP，不含 HTTP 逻辑）
│   ├── ChatStateService.php        # Redis 全局状态（替代 Application 对象）
│   ├── MessageFilterService.php    # 敏感词/HTML 过滤
│   ├── UserLevelService.php        # 等级权限判断（替代 getLevel()）
│   ├── AiChatService.php           # AI 聊天服务（多厂商适配：OpenAI/DeepSeek 等）
│   ├── VipService.php              # VIP 开通/到期检查
│   ├── ShopService.php             # 商店购买 + 道具激活逻辑
│   └── UserCurrencyService.php     # ⭐ 积分统一操作服务（所有经验/金币/魅力变更必须经此）
│
├── Http/
│   ├── Controllers/
│   │   ├── AuthController.php      # DEFAULT.asp + CHECK.asp + CLOSE.ASP
│   │   ├── ChatController.php      # NEWSAY.ASP + INIT.ASP + LEAVE.ASP + 心跳
│   │   ├── RoomController.php      # ROOM*.ASP 系列（大厅/创建/编辑/删除/转让）
│   │   ├── UserController.php      # USERSET + DOUSER + KILLUSER + 封禁/解封
│   │   ├── GuestbookController.php # GUEST.ASP（留言板）
│   │   ├── LeaderboardController.php # TOP10.ASP（排行榜）
│   │   ├── ShopController.php      # 商店购买 + 改名
│   │   ├── ChatBotController.php   # AI 聊天机器人接口
│   │   ├── FishingController.php   # 钓鱼小游戏（复刻原版 diaoyu/ 功能）
│   │   ├── AdminCommandController.php # 聊天室内管理员快捷命令
│   │   └── Admin/
│   │       ├── DashboardController.php    # 后台首页总览
│   │       ├── SystemController.php       # VIEWSYS.ASP 系统参数配置
│   │       ├── UserManagerController.php  # 用户大盘管理
│   │       ├── RoomManagerController.php  # 房间大盘管理
│   │       ├── AutoactController.php      # 随机事件管理
│   │       ├── VipController.php          # VIP 等级 CRUD
│   │       ├── AiProviderController.php   # AI 厂商配置管理（含启用/禁用/设为默认）
│   │       ├── CurrencyStatsController.php # ⭐ 积分流水活动统计（每日来源产出分析）
│   │       └── SmtpController.php         # 邮件发信配置
│   │
│   ├── Middleware/
│   │   ├── ChatAuthenticate.php    # 聊天室登录验证（检查 Session）
│   │   ├── LevelRequired.php       # 等级权限中间件（chat.level:super）
│   │   └── SiteOwnerOnly.php       # 超级站长专属（chat.site_owner）
│   │
│   └── Requests/                   # Form Request 验证
│       ├── LoginRequest.php
│       ├── SendMessageRequest.php
│       └── CreateRoomRequest.php
│
├── Models/
│   ├── User.php          ├── Room.php          ├── Message.php
│   ├── Sysparam.php      ├── IpLock.php        ├── IpLog.php
│   ├── AuditLog.php      ├── AdminLog.php      ├── Guestbook.php
│   ├── FriendCall.php    ├── FriendRequest.php ├── Marriage.php
│   ├── Action.php        ├── Autoact.php       ├── ScrollAd.php
│   ├── RoomDescription.php ├── UserItem.php   ├── Gift.php
│   ├── ShopItem.php      ├── UserPurchase.php  ├── VipLevel.php
│   ├── AiProviderConfig.php ├── AiUsageLog.php ├── UsernameBlacklist.php
│   ├── UserCurrencyLog.php # ⭐ 积分流水记录
│   └── System/
│       └── PlatformSmtpAccount.php
│
├── Enums/
│   └── CurrencySource.php          # ⭐ 积分来源枚举（auto_save/fishing_gain/admin_adjust 等）
│
└── Jobs/
    └── SaveMessageJob.php          # 异步将消息持久化到 MySQL

bootstrap/
└── app.php                         # ⚠ Laravel 12：中间件/路由在此配置（无 Kernel.php）

routes/
├── web.php                         # 所有 HTTP 路由
├── api.php                         # API 路由（验证码等）
└── channels.php                    # WebSocket Presence Channel 鉴权
```

---

## 六、具体开发任务计划

> **图例**：`[x]` 已完成 · `[/]` 进行中 · `[ ]` 待开发

---

### ✅ 第一阶段：数据库层

- [x] 修改默认 `users` 迁移（username/jjb/exp/sex/headface/user_level/email 等）
- [x] 创建 `rooms` 迁移（room_name/owner/auto/des/title/permit_level/door_open）
- [x] 创建 `messages` 迁移（room_id/from_user/to_user/content/is_secret/font_color/action/sent_at）
- [x] 创建 `sys_params` 迁移（alias/guidetxt/body）
- [x] 创建 `ip_locks` 迁移
- [x] 创建 `audit_logs` 迁移
- [x] 创建 `guestbooks` 迁移
- [x] 创建 `friend_calls` 迁移
- [x] 创建 `friend_requests` 迁移
- [x] 创建 `actions` 迁移
- [x] 创建 `admin_logs` 迁移
- [x] 创建 `user_items` 迁移（封口令道具）
- [x] 创建 `scroll_ads` 迁移
- [x] 创建 `marriages` 迁移
- [x] 创建 `ip_logs` 迁移
- [x] 创建 `room_descriptions` 迁移
- [x] 创建 `autoacts` 迁移（随机事件/机器人自动发言）
- [x] 创建 `gifts` 迁移（礼物定义：emoji/cost/charm）
- [x] 创建 `shop_items` 迁移（商店商品：特效卡/改名卡）
- [x] 创建 `user_purchases` 迁移（购买记录）
- [x] 创建 `vip_levels` 迁移（VIP 等级：倍率/进入模板/价格）
- [x] 创建 `ai_provider_configs` 迁移（AI 服务商配置）
- [x] 创建 `ai_usage_logs` 迁移（AI 用量日志）
- [x] 创建 `username_blacklist` 迁移（用户名黑名单）
- [x] 创建所有 Model 文件（含 `$fillable`、`$casts`、关联关系、中文 DocBlock）
- [x] 创建 `DatabaseSeeder`（默认房间「公共大厅」、系统参数）
- [x] 运行 `php artisan migrate --seed` 验证建表成功

---

### ✅ 第二阶段：Auth 认证

- [x] `AuthController::login()` — 登录（含 IP 封锁检查 + 密码 bcrypt）
- [x] `AuthController::logout()` — 退出并清理 Redis 用户状态
- [x] `ChatAuthenticate` 中间件 — 检查 Session 是否有效
- [x] `LevelRequired` 中间件 — 权限等级检查（`chat.level:super`）
- [x] `SiteOwnerOnly` 中间件 — 超级站长专属（最高权限区块）
- [x] 登录 Blade 视图 `resources/views/index.blade.php`
- [x] 测试：登录 → 退出完整流程

---

### ✅ 第三阶段：Redis 状态层

- [x] `ChatStateService` — 完整实现（userJoin/userLeave/getRoomUsers/pushMessage/nextMsgId/withLock/getSysParam）
- [x] `MessageFilterService` — 敏感词替换 + HTML 过滤
- [x] `UserLevelService` — 等级权限判断

---

### ✅ 第四阶段：WebSocket 广播

- [x] `MessageSent` Event（广播到 `room.{id}` Presence Channel）
- [x] `UserJoined` Event — 用户进入广播
- [x] `UserLeft` Event — 用户离开广播
- [x] `UserKicked` Event — 踢人广播（私有频道）
- [x] `UserMuted` Event — 封口广播
- [x] `UserLevelUp` Event — 升级通知广播
- [x] `RoomTitleUpdated` Event — 标题更新广播
- [x] `ScreenCleared` Event — 管理员清屏广播
- [x] `EffectBroadcast` Event — 特效广播（烟花/下雨/闪电/彩带）
- [x] `routes/channels.php` — Presence Channel 鉴权
- [x] `resources/js/chat.js` — Laravel Echo 接入
- [x] Reverb 连通性测试通过

---

### ✅ 第五阶段：聊天核心

- [x] `ChatController::init()` — 进入房间（历史消息 + 在线用户）
- [x] `ChatController::send()` — 发言（过滤 → Redis → SaveMessageJob → 广播）
- [x] `ChatController::heartbeat()` — 挂机心跳（限流：每分钟6次）
- [x] `ChatController::leave()` — 离开房间
- [x] `ChatController::headfaceList()` — 头像列表
- [x] `ChatController::changeAvatar()` — 修改头像
- [x] `ChatController::setAnnouncement()` — 设置房间公告
- [x] `ChatController::sendFlower()` — 送花/礼物（扣金币 + 增魅力）
- [x] `SaveMessageJob` — 异步消息持久化到 MySQL
- [x] 聊天主界面 Blade 视图 `resources/views/chat/frame.blade.php`
- [x] 测试：进房 → 发言 → 实时收到消息

---

### ✅ 第六阶段：房间管理

- [x] `RoomController::index()` — 大厅房间列表
- [x] `RoomController::store()` — 创建房间
- [x] `RoomController::update()` — 修改设置
- [x] `RoomController::destroy()` — 删除/解散房间
- [x] `RoomController::transfer()` — 转让房主
- [x] 对应 Blade 视图（大厅列表、引导页）

---

### ✅ 第七阶段：用户管理

- [x] `UserController::show()` — 用户名片/资料页
- [x] `UserController::updateProfile()` — 修改个人资料
- [x] `UserController::changePassword()` — 改密码
- [x] `UserController::kick()` — 踢人
- [x] `UserController::mute()` — 封口
- [x] `UserController::ban()` — 封号
- [x] `UserController::banIp()` — 封 IP
- [x] 邮箱验证码发送（`Api\VerificationController`）

---

### ✅ 第八阶段：管理后台

- [x] `Admin\DashboardController` — 后台首页总览
- [x] `Admin\SystemController` — 系统参数配置
- [x] `Admin\SmtpController` — 邮件发信配置
- [x] `Admin\UserManagerController` — 用户大盘管理（编辑/删除）
- [x] `Admin\RoomManagerController` — 房间大盘管理
- [x] `Admin\AutoactController` — 随机事件 CRUD 管理
- [x] `Admin\VipController` — VIP 等级 CRUD 管理
- [x] `Admin\AiProviderController` — AI 厂商配置管理（含启用/禁用/设为默认）
- [x] Horizon 面板 `/horizon`（队列监控）
- [x] 对应 Blade 视图

---

### ✅ 第九阶段：外围功能

- [x] `LeaderboardController` — 风云排行榜（经验/金币/魅力榜）
- [x] `GuestbookController` — 留言板（发表/删除）

---

### ✅ 第十阶段：扩展玩法

- [x] **钓鱼小游戏** `FishingController` — 复刻原版 `diaoyu/` 功能（投竿/收竿/随机收益）
- [x] **AI 聊天机器人** `ChatBotController` + `AiChatService`（多厂商：OpenAI/DeepSeek/Gemini 等）
- [x] **礼物/送花系统** — `Gift` 模型 + `ChatController::sendFlower()`（金币消耗 + 魅力增量）
- [x] **商店系统** `ShopController` + `ShopService`（购买特效卡/改名卡，`ShopItem` + `UserPurchase`）
- [x] **VIP 会员系统** `VipService` + `VipLevel`（进入/离开专属模板、倍率加成）
- [x] **管理员快捷命令** `AdminCommandController`（警告/踢人/封口/冻结/清屏/全服广播/特效）
- [x] **特效系统** `EffectBroadcast` Event（烟花/下雨/闪电/彩带，支持单次卡/周卡）

---

### ✅ 第十一阶段：积分流水审计系统

- [x] `App\Enums\CurrencySource` — 所有来源枚举（可扩展）
- [x] `App\Models\UserCurrencyLog` — 流水 Model（含查询 Scope）
- [x] `App\Services\UserCurrencyService` — 统一积分变更服务（**所有经验/金币/魅力修改必须走此服务**）
- [x] 接入 `AutoSaveExp`（auto_save 来源）
- [x] 接入 `FishingController`（fishing_cost / fishing_gain 来源）
- [x] 接入 `ChatController::init()` 新人礼包（newbie_bonus 来源）
- [x] 接入 `Admin\UserManagerController`（admin_adjust 来源）
- [x] `LeaderboardController::todayIndex()` — 今日风云榜独立页（`/leaderboard/today`）
- [x] `LeaderboardController::myLogs()` — 用户个人流水日志（`/my/currency-logs`）
- [x] `Admin\CurrencyStatsController` — 后台积分活动统计（`/admin/currency-stats`）
- [x] 导航栏新增「📅 今日榜」按钮，后台侧边栏新增「📈 积分流水统计」入口

---

### 🔲 待完善事项

- [x] **用户名禁词管理**（已完成）
    - `username_blacklist` 表新增 `type`（`temp`/`permanent`）和 `reason` 字段
    - `temp` = 改名后旧名30天保留；`permanent` = 管理员永久禁用词
    - `UsernameBlacklist::isBlocked()` 精确匹配两种类型，`isReserved()` 保留兼容
    - 注册（`AuthController`）和改名（`ShopService::useRenameCard`）均经过检测
    - 后台「🚫 禁用用户名」页面（站长专用）：
        - 单个添加 / 批量添加（多行/逗号/空格分隔，自动去重）
        - 关键词搜索 + 分页列表
        - 行内编辑原因备注、删除
    - 已预置：系统保留词、国家领导人名称、侮辱性词汇
- [ ] **滚动公告管理**（后台 CRUD，`ScrollAd` 模型已建，界面待完成）
- [x] **好友系统**（已完成）
    - 好友面板独立 partial：`resources/views/chat/partials/friend-panel.blade.php`
    - 工具栏「好友」按钮，点击弹出浮窗
    - 「我关注的好友」列表：显示互相状态（💚互相/👤单向）、添加时间、删除按钮
    - 「对方已加我」列表：显示对方添加时间、➕ 回加按钮
    - 搜索栏：输入用户名 Enter/点击直接添加好友
    - 所有操作调用与双击用户卡片相同的后端接口
    - `FriendController::index()` 返回 `friends`（含 `sub_time`）和 `pending` 两个列表
- [ ] **婚姻系统**（`Marriage` 模型已建，界面待完成）
- [ ] **单元测试**（核心 Service 层测试覆盖率）
- [ ] **Flash 游戏替代**（`game/`、`pig/` 等 .swf 文件，考虑 HTML5/Canvas 重写）

---

## 七、注意事项

### 7.1 密码策略

- 所有用户密码使用 `bcrypt`（`Hash::make()`）
- 登录时使用 `Hash::check()` 验证

### 7.2 实时推送机制

**聊天室所有状态变更均通过 Reverb WebSocket 广播**，无需轮询刷新。

> [!NOTE]
> 历史上基于 `<meta http-equiv=refresh>` 的 6秒刷新方案已被完全取代，任何涉及聊天室状态更新的功能都应通过 Event → Broadcasting 实现，**禁止在实现中引入轮询逻辑**。

### 7.3 Redis 状态层设计

| 数据         | Redis Key 格式            | 说明                       |
| ------------ | ------------------------- | -------------------------- |
| 在线用户列表 | `room:{id}:users` Hash    | 值为用户信息 JSON          |
| 消息环形缓冲 | `room:{id}:messages` List | 最多保留 200 条            |
| 房间基本信息 | `room:{id}:info` String   | JSON 快照                  |
| 并发锁       | `Cache::lock("key", 10)`  | 使用 `->block(5, fn)` 实现 |

### 7.4 Flash 游戏（暂不处理）

`game/`、`pig/`、`Gupiao/` 等目录内的 `.swf` Flash 文件现代浏览器已不支持，**本期不做转换**。

### 7.5 AI 机器人多厂商支持

`AiChatService` 支持以下 Provider，通过 `ai_provider_configs` 后台配置切换：

| Provider   | 推荐模型           | 说明                         |
| ---------- | ------------------ | ---------------------------- |
| `openai`   | `gpt-4o-mini`      | OpenAI 官方 API              |
| `deepseek` | `deepseek-chat`    | 国产低成本，兼容 OpenAI 接口 |
| `gemini`   | `gemini-1.5-flash` | Google Gemini                |

### 7.6 商店商品类型说明

| type       | slug 示例        | 说明                   |
| ---------- | ---------------- | ---------------------- |
| `instant`  | `once_fireworks` | 单次特效卡（即时生效） |
| `duration` | `week_fireworks` | 周卡（7天内持续生效）  |
| `rename`   | `rename`         | 改名卡（改变显示名称） |

---

### 7.7 积分流水系统 — 开发者使用指南 ⭐

> [!IMPORTANT]
> **铁律**：所有涉及修改用户 `exp_num`（经验）、`jjb`（金币）、`meili`（魅力）的操作，
> **必须通过 `UserCurrencyService::change()` 执行**，禁止直接操作 `$user->increment()` 或 `User::update()`。

#### 8.1 注入服务

```php
use App\Enums\CurrencySource;
use App\Services\UserCurrencyService;

public function __construct(
    private readonly UserCurrencyService $currencyService,
) {}
```

#### 8.2 单次变更

```php
// 给用户增加经验（正数=增加，负数=扣除）
$this->currencyService->change(
    user: $user,
    currency: 'exp',           // 'exp' | 'gold' | 'charm'
    amount: 10,                // 正数=增加，负数=消耗（不会低于0）
    source: CurrencySource::AUTO_SAVE,  // 来源（必须用 Enum，不得写裸字符串）
    remark: '挂机获得经验',    // 可选备注
    roomId: 1,                 // 可选房间ID
);

// 扣金币（钓鱼消耗）
$this->currencyService->change($user, 'gold', -5, CurrencySource::FISHING_COST, '抛竿消耗');
```

#### 8.3 批量变更（自动存点场景）

```php
// 对多个用户批量发放，内部逐条原子执行
$this->currencyService->batchChange(
    users: $onlineUsers,     // Collection<User>
    currency: 'exp',
    amount: 1,
    source: CurrencySource::AUTO_SAVE,
    remark: '定时存点',
);
```

#### 8.4 添加新的积分来源（扩展 Enum）

在 `App\Enums\CurrencySource` 枚举中新增一个常量即可，**无需修改数据库**：

```php
// app/Enums/CurrencySource.php
enum CurrencySource: string
{
    case AUTO_SAVE      = 'auto_save';
    case FISHING_COST   = 'fishing_cost';
    case FISHING_GAIN   = 'fishing_gain';
    case NEWBIE_BONUS   = 'newbie_bonus';
    case ADMIN_ADJUST   = 'admin_adjust';
    // ↓ 新增活动时在此追加一行即可
    case DAILY_SIGN_IN  = 'daily_sign_in';   // 示例：每日签到

    /** 返回可读名称（用于后台统计展示） */
    public function label(): string
    {
        return match($this) {
            self::AUTO_SAVE     => '⏰ 自动存点',
            self::FISHING_COST  => '🎣 钓鱼消耗',
            self::FISHING_GAIN  => '🐟 钓鱼收获',
            self::NEWBIE_BONUS  => '🎁 新人礼包',
            self::ADMIN_ADJUST  => '🛠️ 管理员调整',
            self::DAILY_SIGN_IN => '📅 每日签到',  // 对应新增
        };
    }
}
```

#### 8.5 流水查询 Scope

```php
use App\Models\UserCurrencyLog;

// 查询某用户最近7天的经验记录
$logs = UserCurrencyLog::where('user_id', $user->id)
    ->currency('exp')
    ->whereDate('created_at', '>=', now()->subDays(7))
    ->orderByDesc('created_at')
    ->get();

// 今日金币排行（今日获得量前20名）
$ranking = UserCurrencyLog::whereDate('created_at', today())
    ->where('currency', 'gold')
    ->where('amount', '>', 0)   // 只算增加，不算消耗
    ->selectRaw('user_id, username, SUM(amount) as total')
    ->groupBy('user_id', 'username')
    ->orderByDesc('total')
    ->limit(20)
    ->get();
```

#### 8.6 相关页面路由速查

| 页面         | 路由名                       | URI                                     |
| ------------ | ---------------------------- | --------------------------------------- |
| 个人积分日志 | `currency.my-logs`           | `/my/currency-logs?currency=exp&days=7` |
| 今日风云榜   | `leaderboard.today`          | `/leaderboard/today`                    |
| 累计风云榜   | `leaderboard.index`          | `/leaderboard`                          |
| 后台积分统计 | `admin.currency-stats.index` | `/admin/currency-stats?date=2026-02-28` |

---

### 7.8 全局弹窗系统 `window.chatDialog` ⭐

> [!IMPORTANT]
> 聊天室内**禁止使用**浏览器原生的 `alert()` / `confirm()` / `prompt()`。
> 原因：原生弹窗在 Chrome 中会与 `beforeunload` / `pagehide` 事件产生冲突，导致弹窗闪烁消失。
> **所有需要提示或确认的场景，必须使用 `window.chatDialog`。**

#### 文件位置

```
resources/views/chat/partials/global-dialog.blade.php   ← 弹窗 HTML + JS
resources/views/chat/frame.blade.php                    ← 已 @include，全页面可用
```

#### API 说明

| 方法                                      | 模式              | 返回值             | 说明             |
| ----------------------------------------- | ----------------- | ------------------ | ---------------- |
| `chatDialog.alert(msg, title?, color?)`   | 仅「确定」按钮    | `Promise<void>`    | 替代 `alert()`   |
| `chatDialog.confirm(msg, title?, color?)` | 「取消」+「确定」 | `Promise<boolean>` | 替代 `confirm()` |

参数说明：

- `msg`：弹窗正文内容（字符串）
- `title`：标题栏文字，默认 `'提示'` / `'请确认'`
- `color`：标题栏背景色（CSS 颜色值），默认蓝色 `#336699` / 红色 `#cc4444`

#### 使用示例

```javascript
// ① alert 模式（提示成功 — 绿色）
await window.chatDialog.alert("操作成功！", "提示", "#16a34a");

// ② alert 模式（提示失败 — 红色）
await window.chatDialog.alert("网络异常，请稍后重试", "错误", "#cc4444");

// ③ confirm 模式（执行危险操作前确认）
const yes = await window.chatDialog.confirm(
    "确定要撤销该用户的职务吗？撤销后将不再拥有相关权限。",
    "撤销职务", // 标题默认红色
);
if (yes) {
    // 用户点了「确定」
}

// ④ confirm 模式 — 自定义颜色
const leave = await window.chatDialog.confirm(
    "确定要离开聊天室吗？",
    "离开聊天室",
    "#cc4444",
);
if (leave) {
    leaveRoom();
}
```

#### 在 Alpine.js 组件内使用

`userCardComponent` 组件已通过代理方法封装：

```javascript
// 在 Alpine.js 组件方法中，可以直接用 this.$alert / this.$confirm
await this.$alert("任命成功！", "任命成功 ✨", "#16a34a");

const ok = await this.$confirm("确定撤销职务？", "撤销确认");
if (ok) {
    /* ... */
}
```

这两个方法内部等价于 `window.chatDialog.alert()` / `window.chatDialog.confirm()`。

#### 颜色速查

| 场景            | 推荐颜色                |
| --------------- | ----------------------- |
| 成功 / 正向操作 | `#16a34a`（绿色）       |
| 错误 / 警告     | `#cc4444`（红色）       |
| 一般提示        | `#336699`（蓝色，默认） |
| 撤销 / 中性操作 | `#6b7280`（灰色）       |
| 管理 / 特权操作 | `#7c3aed`（紫色）       |

---

### 7.9 全局右下角 Toast 通知卡片 `window.chatToast` ⭐

> [!NOTE]
> `chatToast` 是固定在**右下角**的轻量通知卡片，适用于实时事件通知（奖励到账、好友动态等）。
> 与 `chatDialog` 不同，它不阻断操作流程，自动消失，可堆叠多条。

#### 文件位置

```
resources/views/chat/partials/toast-notification.blade.php  ← Toast 组件 HTML + JS
resources/views/chat/frame.blade.php                        ← 已 @include，全页面可用
```

#### API 说明

```javascript
window.chatToast.show({
    title: "标题文字", // 必填
    message: "正文内容", // 必填，支持 HTML
    icon: "🪙", // 可选，左侧 Emoji，默认 💬
    color: "#f59e0b", // 可选，强调色，默认 #336699
    duration: 6000, // 可选，自动消失毫秒，0 = 不自动消失，默认 6000
    action: {
        // 可选，操作按钮
        label: "操作文字",
        onClick: () => {
            /* ... */
        },
    },
});
```

#### 通过消息字段自动触发（后端控制）

后端 broadcast 的消息中包含 `toast_notification` 字段，且接收方是当前用户时，前端脚本会自动弹出 Toast：

```php
// AdminCommandController::reward() 示例
$msg['toast_notification'] = [
    'title'    => '🪙 奖励金币到账',
    'message'  => "<b>{$admin->username}</b> 向你发放了 <b>{$amount}</b> 枚金币！",
    'icon'     => '🪙',
    'color'    => '#f59e0b',
    'duration' => 8000,
];
```

#### 使用场景

| 场景             | 颜色            | 图标 |
| ---------------- | --------------- | ---- |
| 奖励金币到账     | `#f59e0b`（橙） | 🪙   |
| 好友动态通知     | `#6b7280`（灰） | 👥   |
| 礼物收到         | `#e11d48`（玫） | 🎁   |
| 系统提示（普通） | `#336699`（蓝） | 💬   |
| 等级晋升         | `#7c3aed`（紫） | 🌟   |

#### 原 `showFriendToast` 迁移说明

旧函数 `showFriendToast()` 已被 `window.chatToast.show()` 替代，好友删除通知已改用新 API。
新增功能只需调用 `window.chatToast.show()`，**勿新增** `showFriendToast` 调用。

---

### 7.10 全局大卡片通知 `window.chatBanner` ⭐

> [!NOTE]
> `chatBanner` 是居中弹出的沉浸式大卡片通知组件，适用于好友通知、任命公告、系统广播等重要事件。与 `chatDialog` 不同，它**不阻断操作流程**，支持自动消失和自定义按钮。

#### 文件位置

```
resources/views/chat/partials/scripts.blade.php   ← chatBanner 定义（window.chatBanner）
app/Events/BannerNotification.php                 ← 广播事件（后端推送）
app/Http/Controllers/Admin/BannerBroadcastController.php  ← 管理员推送接口
```

#### 前端 API

```javascript
window.chatBanner.show(options); // 显示大卡片
window.chatBanner.close(id); // 关闭指定 banner
```

#### `show(options)` 参数说明

| 参数         | 类型       | 说明                                        | 默认值                            |
| ------------ | ---------- | ------------------------------------------- | --------------------------------- |
| `id`         | `string`   | 可选，banner DOM ID，同 ID 自动覆盖防重叠   | `'chat-banner-default'`           |
| `icon`       | `string`   | 顶部 Emoji 图标                             | 无                                |
| `title`      | `string`   | 小标题（显示为 `══ 标题 ══`）               | 无                                |
| `name`       | `string`   | 大名字/主角行（自动 HTML 转义）             | 无                                |
| `body`       | `string`   | 主内容，支持有限 HTML（`<b><span><br>` 等） | 无                                |
| `sub`        | `string`   | 副说明（小字，支持有限 HTML）               | 无                                |
| `gradient`   | `string[]` | 渐变色数组，如 `['#4f46e5', '#db2777']`     | `['#4f46e5','#7c3aed','#db2777']` |
| `titleColor` | `string`   | 小标题字体颜色                              | `'#fde68a'`                       |
| `autoClose`  | `number`   | 自动关闭时间（ms），`0` = 不自动关闭        | `5000`                            |
| `buttons`    | `Button[]` | 按钮列表（见下）                            | 无（无按钮时不可点击）            |

`buttons` 数组元素：

```typescript
{
  label:   string,                          // 按钮文字
  color:   string,                          // 背景色（CSS 颜色值）
  onClick: (btn: HTMLElement, close: () => void) => void,  // 点击回调
}
```

#### 使用示例

```javascript
// ① 简单通知（5秒自动消失）
window.chatBanner.show({
    icon: "🎉💚🎉",
    title: "好友通知",
    name: "lkddi1",
    body: "将你加为好友了！",
    sub: '<strong style="color:#a7f3d0;">你们现在互为好友 🎊</strong>',
    gradient: ["#065f46", "#059669", "#10b981"],
    titleColor: "#a7f3d0",
    autoClose: 5000,
});

// ② 带操作按钮（不自动关闭）
window.chatBanner.show({
    id: "friend-add-banner",
    icon: "💚📩",
    title: "好友申请",
    name: "lkddi1",
    body: "将你加为好友了！",
    sub: "但你还没有回加对方为好友",
    gradient: ["#1e3a5f", "#1d4ed8", "#0891b2"],
    titleColor: "#bae6fd",
    autoClose: 0, // 不自动关闭，等待用户操作
    buttons: [
        {
            label: "➕ 回加好友",
            color: "#10b981",
            onClick: (btn, close) => {
                btn.textContent = "处理中…";
                // ... 调用 API ...
                close(); // 完成后手动关闭
            },
        },
        {
            label: "稍后再说",
            color: "rgba(255,255,255,0.15)",
            onClick: (btn, close) => close(),
        },
    ],
});

// ③ 手动关闭指定 banner
window.chatBanner.close("friend-add-banner");
```

#### 后端推送（管理员）

通过 `BannerNotification` 事件向**单个用户**或**整个房间**广播大卡片：

```php
use App\Events\BannerNotification;

// 推给单个用户（私有频道 user.{username}）
broadcast(new BannerNotification(
    target:   'user',
    targetId: 'lkddi',
    options:  [
        'icon'      => '📢',
        'title'     => '系统通知',
        'body'      => '你有一条新消息',
        'gradient'  => ['#4f46e5', '#7c3aed'],
        'autoClose' => 6000,
    ]
));

// 推给整个房间（Presence 频道 room.{id}）
broadcast(new BannerNotification(
    target:   'room',
    targetId: 1,
    options:  [
        'icon'      => '🎊',
        'title'     => '活动公告',
        'body'      => '双倍积分活动已开始！',
        'gradient'  => ['#065f46', '#059669'],
        'autoClose' => 8000,
    ]
));
```

也可通过管理员 HTTP 接口（仅超级管理员）：

```
POST /admin/banner/broadcast
Content-Type: application/json

{
  "target":    "room",
  "target_id": 1,
  "options": {
    "icon":      "📢",
    "title":     "公告",
    "body":      "服务器将于 10 分钟后重启",
    "gradient":  ["#374151", "#4b5563"],
    "autoClose": 10000
  }
}
```

#### 渐变配色速查

| 场景        | 渐变数组                                      |
| ----------- | --------------------------------------------- |
| 好友 / 成功 | `['#065f46', '#059669', '#10b981']`（绿色）   |
| 申请 / 信息 | `['#1e3a5f', '#1d4ed8', '#0891b2']`（蓝色）   |
| 任命 / 荣耀 | `['#4f46e5', '#7c3aed', '#db2777']`（紫粉）   |
| 撤销 / 中性 | `['#374151', '#4b5563', '#6b7280']`（灰色）   |
| 警告 / 紧急 | `['#7f1d1d', '#991b1b', '#dc2626']`（红色）   |
| 系统 / 特权 | `['#1e1b4b', '#312e81', '#4338ca']`（深蓝紫） |

#### 安全说明

> [!IMPORTANT]
>
> - **前端控制台调用** `window.chatBanner.show()` 只影响**用户自己的浏览器**，无法推送给他人。
> - **HTTP 推送接口** `POST /admin/banner/broadcast` 受三层中间件保护（`chat.auth` + `chat.has_position` + `chat.level:super`），普通用户调用返回 403。
> - **内容净化**：`body`、`sub`、`title` 字段经过 `strip_tags` 白名单处理（允许 `<b><strong><em><span><br>`），防止 XSS。
> - **按钮 action 白名单**：仅允许 `close | add_friend | remove_friend | link`，禁止任意 JS 注入。

---

## 八、常用命令速查

```bash
# 创建 Model + Migration（-m 同时生成迁移）
php artisan make:model Room -m

# 创建 Controller（-r 生成 RESTful 方法）
php artisan make:controller ChatController

# 创建广播 Event
php artisan make:event MessageSent

# 创建队列 Job
php artisan make:job SaveMessageJob

# 创建 Middleware
php artisan make:middleware ChatAuthenticate

# 创建 Form Request
php artisan make:request SendMessageRequest

# 重置迁移（开发阶段）
php artisan migrate:fresh --seed

# 查看路由列表
php artisan route:list --columns=method,uri,name,action

# 代码格式化（提交前必须运行）
vendor/bin/pint --dirty

# Horizon 队列监控（生产环境）
php artisan horizon

# 重启 Horizon（更新代码后）
php artisan horizon:terminate

# 清理所有缓存
php artisan optimize:clear
```

---

> **参照原项目**：`/Users/pllx/Web/chat/hp0709/`（VBScript ASP 源码，仅作功能参考和备查，不做数据迁移）
