diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 738dd1e..1e67679 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -2,7 +2,7 @@ > **技术栈**:Laravel 12 · PHP 8.4 · Laravel Reverb (WebSocket) · Redis · MySQL 8.0 · Laravel Horizon > **原项目**:`/Users/pllx/Web/chat/hp0709`(VBScript ASP + MS Access 聊天室) -> **目标域名**:`http://chatroom.test`(Herd 自动配置) +> **目标域名**:`http://chatroom.test`(Herd 自动配置)| `https://chat.ay.lc`(生产环境) --- @@ -93,13 +93,9 @@ return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { // 注册聊天室登录验证中间件 $middleware->alias([ - 'chat.auth' => \App\Http\Middleware\ChatAuthenticate::class, - 'chat.level' => \App\Http\Middleware\LevelRequired::class, - ]); - - // Session 中间件(Web 路由自动携带) - $middleware->web(append: [ - \App\Http\Middleware\HandleInertiaRequests::class, + '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 { @@ -150,26 +146,18 @@ class ChatStateService ```bash cd /Users/pllx/Web/Herd/chatroom -# 安装 Reverb WebSocket 服务器(已经完成) -composer require laravel/reverb predis/predis +# 安装 PHP 依赖(已完成) +composer install -# 安装 Horizon 队列管理(替代 queue:work,提供 Web 监控面板) -composer require laravel/horizon - -# 发布配置文件 -php artisan reverb:install -php artisan horizon:install - -# 创建数据库(已经完成) +# 创建数据库(已完成) mysql -u root -proot -e "CREATE DATABASE IF NOT EXISTS chatroom CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" -# 运行数据库迁移(迁移前检查原有迁移文件是否有用) -php artisan migrate +# 运行数据库迁移 + 基础数据填充 +php artisan migrate --seed -# 安装前端依赖 -npm install (已经完成) -npm install laravel-echo pusher-js(已经完成) -npm run dev +# 安装前端依赖并构建(已完成) +npm install +npm run build ``` **开发时运行的服务**: @@ -177,7 +165,7 @@ npm run dev ```bash php artisan reverb:start --debug # WebSocket 服务器 :8080 php artisan horizon # 队列 Worker(含 Web 监控 /horizon) -npm run dev # Vite 热更新 +npm run dev # Vite 热更新(开发阶段) # HTTP 由 Herd 自动提供 chatroom.test ``` @@ -187,44 +175,32 @@ npm run dev # Vite 热更新 **原 Access 表** → **Laravel Migration** 对应关系: -| 原 ASP 表 | Laravel 迁移文件 | Model 类 | 说明 | -| ----------- | -------------------------------- | ----------------- | --------------------------------- | -| `user` | `create_users_table` | `User` | 主用户表(默认迁移文件,需修改) | -| `room` | `create_rooms_table` | `Room` | 聊天房间 | -| _(内存)_ | `create_messages_table` | `Message` | 消息归档(原用 Application 内存) | -| `sysparam` | `create_sys_params_table` | `SysParam` | 系统参数 | -| `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` | 房间描述模板 | - -**批量生成迁移命令**: - -```bash -php artisan make:migration create_rooms_table -php artisan make:migration create_messages_table -php artisan make:migration create_sys_params_table -php artisan make:migration create_ip_locks_table -php artisan make:migration create_audit_logs_table -php artisan make:migration create_guestbooks_table -php artisan make:migration create_friend_calls_table -php artisan make:migration create_friend_requests_table -php artisan make:migration create_actions_table -php artisan make:migration create_admin_logs_table -php artisan make:migration create_user_items_table -php artisan make:migration create_scroll_ads_table -php artisan make:migration create_marriages_table -php artisan make:migration create_ip_logs_table -php artisan make:migration create_room_descriptions_table -``` +| 原 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` | 用户名黑名单 | --- @@ -238,31 +214,45 @@ app/ │ ├── UserLeft.php # 用户离开(替代 LEAVE.ASP) │ ├── UserKicked.php # 踢人 │ ├── UserMuted.php # 封口 -│ └── RoomTitleUpdated.php # 房间标题更新 +│ ├── UserLevelUp.php # 用户升级通知 +│ ├── RoomTitleUpdated.php # 房间标题更新 +│ ├── ScreenCleared.php # 管理员清屏 +│ └── EffectBroadcast.php # 特效广播(烟花/下雨等) │ ├── Services/ # 业务逻辑服务层(纯 PHP,不含 HTTP 逻辑) │ ├── ChatStateService.php # Redis 全局状态(替代 Application 对象) │ ├── MessageFilterService.php # 敏感词/HTML 过滤 -│ ├── AuthService.php # 登录验证逻辑 -│ └── UserLevelService.php # 等级权限判断(替代 getLevel()) +│ ├── UserLevelService.php # 等级权限判断(替代 getLevel()) +│ ├── AiChatService.php # AI 聊天服务(多厂商适配:OpenAI/DeepSeek 等) +│ ├── VipService.php # VIP 开通/到期检查 +│ └── ShopService.php # 商店购买 + 道具激活逻辑 │ ├── Http/ │ ├── Controllers/ │ │ ├── AuthController.php # DEFAULT.asp + CHECK.asp + CLOSE.ASP -│ │ ├── RegisterController.php # Reg.asp + addnewuser.asp -│ │ ├── ChatController.php # NEWSAY.ASP + INIT.ASP + LEAVE.ASP -│ │ ├── RoomController.php # ROOM*.ASP 系列 -│ │ ├── UserController.php # USERSET + DOUSER + KILLUSER -│ │ ├── FriendController.php # addfriend + agreefriend 等 -│ │ ├── GuestbookController.php # GUEST.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/ -│ │ ├── AdminController.php -│ │ ├── UserManagerController.php -│ │ └── SystemController.php # VIEWSYS.ASP +│ │ ├── DashboardController.php # 后台首页总览 +│ │ ├── SystemController.php # VIEWSYS.ASP 系统参数配置 +│ │ ├── UserManagerController.php # 用户大盘管理 +│ │ ├── RoomManagerController.php # 房间大盘管理 +│ │ ├── AutoactController.php # 随机事件管理 +│ │ ├── VipController.php # VIP 等级 CRUD +│ │ ├── AiProviderController.php # AI 厂商配置管理 +│ │ └── SmtpController.php # 邮件发信配置 │ │ │ ├── Middleware/ -│ │ ├── ChatAuthenticate.php # 聊天室登录验证 -│ │ └── LevelRequired.php # 等级权限中间件(用法:chat.level:5) +│ │ ├── ChatAuthenticate.php # 聊天室登录验证(检查 Session) +│ │ ├── LevelRequired.php # 等级权限中间件(chat.level:super) +│ │ └── SiteOwnerOnly.php # 超级站长专属(chat.site_owner) │ │ │ └── Requests/ # Form Request 验证 │ ├── LoginRequest.php @@ -270,12 +260,16 @@ app/ │ └── CreateRoomRequest.php │ ├── Models/ -│ ├── User.php -│ ├── Room.php -│ ├── Message.php -│ ├── SysParam.php -│ ├── IpLock.php -│ └── ... +│ ├── 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 +│ └── System/ +│ └── PlatformSmtpAccount.php │ └── Jobs/ └── SaveMessageJob.php # 异步将消息持久化到 MySQL @@ -285,162 +279,171 @@ bootstrap/ routes/ ├── web.php # 所有 HTTP 路由 -├── api.php # API 路由(Horizon 监控等) +├── api.php # API 路由(验证码等) └── channels.php # WebSocket Presence Channel 鉴权 - -resources/ -├── views/ -│ ├── index.blade.php # 登录页(DEFAULT.asp) -│ ├── chat/ -│ │ ├── frame.blade.php # 聊天主框架(CHAT.ASP) -│ │ └── room.blade.php # 消息区 -│ └── ... -└── js/ - ├── app.js - └── chat.js # Laravel Echo 替代 newChat.js ``` --- ## 六、具体开发任务计划 -### ✅ 已完成 - -- [x] 创建 Laravel 12 项目 -- [x] 安装 `laravel/reverb`、`predis/predis` -- [x] 创建 MySQL 数据库 `chatroom` -- [x] 配置 `.env`(MySQL root/root · Redis · Reverb · 时区 Asia/Shanghai) +> **图例**:`[x]` 已完成 · `[/]` 进行中 · `[ ]` 待开发 --- -### 🔲 第一阶段:数据库层(预计 1-2 天) +### ✅ 第一阶段:数据库层 -**目标**:所有表创建完毕,并完成基础 Seeder。 - -- [ ] **修改默认 `users` 迁移**,对应 ASP `user` 表字段(username/passwd/sex/user_level/exp_num/friends/headface/等) -- [ ] **创建 `rooms` 迁移**(room_name/owner/auto/des/title/permit_level/door_open) -- [ ] **创建 `messages` 迁移**(room_id/from_user/to_user/content/is_secret/font_color/action/sent_at) -- [ ] **创建 `sys_params` 迁移**(alias/guidetxt/body) -- [ ] **创建 `ip_locks` 迁移**(ip/end_time/act_level) -- [ ] **创建 `audit_logs` 迁移**(occ_time/occ_env/stype) -- [ ] **创建 `guestbooks` 迁移**(who/towho/secret/ip/post_time/text_title/text_body) -- [ ] **创建 `friend_calls` 迁移**(who/towho/callmess/calltime/read) -- [ ] **创建 `friend_requests` 迁移**(who/towho/sub_time) -- [ ] **创建 `actions` 迁移**(act_name/alias/toall/toself/toother) -- [ ] **创建 `user_items` 迁移**(name/gg/times/dayy/lx — 对应道具/封口令等) -- [ ] **创建 `scroll_ads` 迁移**(ad_title/ad_link/ad_new_flag) -- [ ] **创建 `marriages` 迁移**(hyname/hyname1/hytime/hygb/hyjb) -- [ ] **创建 `ip_logs` 迁移**(ip/sdate/uuname) -- [ ] **创建所有 Model 文件**(含 `$fillable`、`$casts`、关联关系、中文 DocBlock) -- [ ] **创建 `SysParamSeeder`**(写入系统默认参数:maxpeople/namelength/maxsayslength 等) -- [ ] 运行 `php artisan migrate --seed`,验证建表成功 +- [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 认证(预计 1-2 天) +### ✅ 第二阶段:Auth 认证 -**目标**:用户能够登录、注册、退出。 - -- [ ] **`LoginRequest`**(验证:username/password/captcha 验证码) -- [ ] **`AuthController::index()`** — 登录页(含验证码生成,替代 `session("regjm")`) -- [ ] **`AuthController::check()`** — 登录验证(含 IP 封锁检查 + 密码 MD5/bcrypt 双模式) -- [ ] **`AuthController::logout()`** — 退出并清理 Redis 用户状态 -- [ ] **`RegisterController::show()`** — 注册页 -- [ ] **`RegisterController::store()`** — 注册逻辑(含 IP 注册频率限制) -- [ ] **`ChatAuthenticate` 中间件** — 检查 Session 是否有效,无则跳转登录页 -- [ ] **`LevelRequired` 中间件** — 检查用户等级,不足则拒绝(`chat.level:5`) -- [ ] 在 **`bootstrap/app.php`** 注册以上中间件别名 -- [ ] **登录 Blade 视图** `resources/views/index.blade.php`(仿原 DEFAULT.asp 样式) -- [ ] 测试:注册 → 登录 → 退出完整流程 +- [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 状态层(预计 1 天) +### ✅ 第三阶段:Redis 状态层 -**目标**:`ChatStateService` 完整实现,替代原 Application 对象。 - -- [ ] **`ChatStateService`** 实现以下方法(全部带中文注释): - - `userJoin(int $roomId, string $username, array $info): void` - - `userLeave(int $roomId, string $username): void` - - `getRoomUsers(int $roomId): array` - - `pushMessage(int $roomId, array $message): void` - - `getNewMessages(int $roomId, int $lastId): array` - - `nextMessageId(int $roomId): int`(Redis 自增计数器) - - `withLock(string $key, callable $callback): mixed`(分布式锁) - - `getSysParam(string $alias): string`(读取系统参数,缓存1分钟) -- [ ] **`MessageFilterService`** — 敏感词替换 + HTML 过滤(替代 `TrStr()` / `SHTM()` 函数) -- [ ] **`UserLevelService`** — 从 Redis 读取当前用户等级 +- [x] `ChatStateService` — 完整实现(userJoin/userLeave/getRoomUsers/pushMessage/nextMsgId/withLock/getSysParam) +- [x] `MessageFilterService` — 敏感词替换 + HTML 过滤 +- [x] `UserLevelService` — 等级权限判断 --- -### 🔲 第四阶段:WebSocket 广播(预计 1 天) +### ✅ 第四阶段:WebSocket 广播 -**目标**:Reverb 正常运行,前端能收到实时消息。 - -- [ ] **`MessageSent` Event**(implement `ShouldBroadcast`)— 广播到 `room.{id}` Presence Channel -- [ ] **`UserJoined` Event** — 用户进入广播 -- [ ] **`UserLeft` Event** — 用户离开广播 -- [ ] **`UserKicked` Event** — 踢人广播(私有频道,只发给被踢人) -- [ ] **`UserMuted` Event** — 封口广播 -- [ ] **`RoomTitleUpdated` Event** — 标题更新广播 -- [ ] **`routes/channels.php`** — Presence Channel 鉴权(验证等级 + 返回用户信息) -- [ ] **`resources/js/chat.js`** — Laravel Echo 接入(`Echo.join('room.X').here().joining().leaving().listen()`) -- [ ] 运行 `php artisan reverb:start --debug`,测试 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 连通性测试通过 --- -### 🔲 第五阶段:聊天核心(预计 2-3 天) +### ✅ 第五阶段:聊天核心 -**目标**:进房、发言、离开完整流程可用。 - -- [ ] **`ChatController::init()`** — 进入房间(读取 Redis 用户列表 + 历史消息,替代 INIT.ASP) -- [ ] **`ChatController::send()`** — 发言(过滤 → 推 Redis → `SaveMessageJob` → 广播 Event) -- [ ] **`ChatController::leave()`** — 离开房间(清 Redis → 广播 `UserLeft`) -- [ ] **`SaveMessageJob`** — 实现 `ShouldQueue`,异步写消息到 `messages` 表 -- [ ] **聊天 Blade 视图** `resources/views/chat/frame.blade.php`(主框架,含 Vite 引入) -- [ ] 测试:登录 → 进房 → 发言 → 另一浏览器实时收到消息 +- [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] 测试:进房 → 发言 → 实时收到消息 --- -### 🔲 第六阶段:房间管理(预计 2 天) +### ✅ 第六阶段:房间管理 -- [ ] **`RoomController::list()`** — 房间列表(替代 ROOMLIST.ASP) -- [ ] **`RoomController::create()`** / `store()` — 创建房间(替代 NEWROOM.ASP) -- [ ] **`RoomController::edit()`** / `update()` — 修改设置(替代 ROOMSET.ASP) -- [ ] **`RoomController::destroy()`** — 删除/解散房间(替代 CUTROOM.ASP) -- [ ] **`RoomController::transfer()`** — 转让房主(替代 OVERROOM.ASP) -- [ ] 对应 Blade 视图 +- [x] `RoomController::index()` — 大厅房间列表 +- [x] `RoomController::store()` — 创建房间 +- [x] `RoomController::update()` — 修改设置 +- [x] `RoomController::destroy()` — 删除/解散房间 +- [x] `RoomController::transfer()` — 转让房主 +- [x] 对应 Blade 视图(大厅列表、引导页) --- -### 🔲 第七阶段:用户管理(预计 2 天) +### ✅ 第七阶段:用户管理 -- [ ] **`UserController::info()`** — 用户信息(替代 USERinfo.ASP) -- [ ] **`UserController::update()`** — 修改个人资料(替代 USERSET.ASP) -- [ ] **`UserController::kick()`** — 踢人(替代 KILLUSER.ASP,广播 `UserKicked`) -- [ ] **`UserController::mute()`** — 封口(道具 `user_items` 表操作) -- [ ] **`UserController::changePassword()`** — 改密码(替代 chpasswd.asp) +- [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`) --- -### 🔲 第八阶段:管理后台(预计 3-5 天) +### ✅ 第八阶段:管理后台 -- [ ] **`LevelRequired` 中间件** 保护 `/admin` 路由(需 level=15) -- [ ] **`Admin\SystemController`** — 系统参数配置(替代 VIEWSYS.ASP) -- [ ] **`Admin\UserManagerController`** — 用户管理列表(替代 `gl/` 目录) -- [ ] **`Admin\SqlController`** — 后台 SQL 执行(替代 SQL.asp,⚠ 仅 SELECT) -- [ ] **Horizon 面板** `/horizon`(队列监控,替代后台日志查看) -- [ ] 对应 Blade 视图 +- [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 视图 --- -### 🔲 第九阶段:附加功能(按需) +### ✅ 第九阶段:外围功能 -- [ ] 好友系统(FriendController) -- [ ] 留言板(GuestbookController) -- [ ] 排行榜(RankController) -- [ ] 会员系统(`huiyuan/` 对应功能) -- [ ] 滚动公告(ScrollAd 管理) +- [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(烟花/下雨/闪电/彩带,支持单次卡/周卡) + +--- + +### 🔲 待完善事项 + +- [ ] **用户名黑名单管理**(后台 CRUD,`UsernameBlacklist` 模型已建,路由/界面待完成) +- [ ] **滚动公告管理**(后台 CRUD,`ScrollAd` 模型已建,界面待完成) +- [ ] **好友系统**(`FriendCall` / `FriendRequest` 模型已建,Controller 待完成) +- [ ] **婚姻系统**(`Marriage` 模型已建,界面待完成) +- [ ] **历史数据迁移**(从 Access 导入旧用户数据:GBK→UTF-8 转换) +- [ ] **密码兼容过渡**(旧 MD5 用户登录时自动升级为 bcrypt) +- [ ] **单元测试**(核心 Service 层测试覆盖率) +- [ ] **Flash 游戏替代**(`game/`、`pig/` 等 .swf 文件,考虑 HTML5/Canvas 重写) --- @@ -451,7 +454,6 @@ resources/ - 导入旧数据时,`password` 字段存原始 MD5 值(32位字符串) - 登录时双模式验证:`md5($input) === $storedPass` 成功后升级为 `bcrypt` - 新注册用户直接用 `bcrypt`(`Hash::make()`) -- 约 3 个月后移除 MD5 兼容分支 ### 7.2 字符编码 @@ -477,7 +479,25 @@ resources/ ### 7.5 Flash 游戏(暂不处理) -`game/`、`pig/`、`Gupiao/` 等目录内的 `.swf` Flash 文件现代浏览器已不支持,**本期不做转换**,后续单独用 HTML5/Canvas 重写。 +`game/`、`pig/`、`Gupiao/` 等目录内的 `.swf` Flash 文件现代浏览器已不支持,**本期不做转换**。 + +### 7.6 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.7 商店商品类型说明 + +| type | slug 示例 | 说明 | +| ---------- | ---------------- | ---------------------- | +| `instant` | `once_fireworks` | 单次特效卡(即时生效) | +| `duration` | `week_fireworks` | 周卡(7天内持续生效) | +| `rename` | `rename` | 改名卡(改变显示名称) | --- diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php index f6646f2..a89d003 100644 --- a/app/Console/Commands/AutoSaveExp.php +++ b/app/Console/Commands/AutoSaveExp.php @@ -17,10 +17,12 @@ namespace App\Console\Commands; use App\Events\MessageSent; +use App\Enums\CurrencySource; use App\Jobs\SaveMessageJob; use App\Models\Sysparam; use App\Models\User; use App\Services\ChatStateService; +use App\Services\UserCurrencyService; use App\Services\VipService; use Illuminate\Console\Command; use Illuminate\Support\Facades\Redis; @@ -41,8 +43,9 @@ class AutoSaveExp extends Command * 注入依赖服务 */ public function __construct( - private readonly ChatStateService $chatState, - private readonly VipService $vipService, + private readonly ChatStateService $chatState, + private readonly VipService $vipService, + private readonly UserCurrencyService $currencyService, ) { parent::__construct(); } @@ -134,21 +137,31 @@ class AutoSaveExp extends Command return; } - // 1. 发放经验奖励(支持 VIP 倍率) + // 1. 计算奖励量(经验/金币 均支持 VIP 倍率) $expGain = $this->parseRewardValue($expGainRaw); $expMultiplier = $this->vipService->getExpMultiplier($user); $actualExpGain = (int) round($expGain * $expMultiplier); - $user->exp_num += $actualExpGain; - // 2. 发放金币奖励(支持 VIP 倍率) $jjbGain = $this->parseRewardValue($jjbGainRaw); $actualJjbGain = 0; if ($jjbGain > 0) { $jjbMultiplier = $this->vipService->getJjbMultiplier($user); $actualJjbGain = (int) round($jjbGain * $jjbMultiplier); - $user->jjb = ($user->jjb ?? 0) + $actualJjbGain; } + // 2. 通过统一积分服务发放奖励(原子写入 + 流水记录) + if ($actualExpGain > 0) { + $this->currencyService->change( + $user, 'exp', $actualExpGain, CurrencySource::AUTO_SAVE, '自动存点', $roomId, + ); + } + if ($actualJjbGain > 0) { + $this->currencyService->change( + $user, 'gold', $actualJjbGain, CurrencySource::AUTO_SAVE, '自动存点', $roomId, + ); + } + $user->refresh(); // 刷新获取最新属性(service 已原子更新) + // 3. 自动升降级(管理员不参与) $oldLevel = $user->user_level; $leveledUp = false; diff --git a/app/Enums/CurrencySource.php b/app/Enums/CurrencySource.php new file mode 100644 index 0000000..66af706 --- /dev/null +++ b/app/Enums/CurrencySource.php @@ -0,0 +1,61 @@ + '自动存点', + self::FISHING_GAIN => '钓鱼奖励', + self::FISHING_COST => '钓鱼消耗', + self::SEND_GIFT => '送出礼物', + self::RECV_GIFT => '收到礼物', + self::NEWBIE_BONUS => '新人礼包', + self::SHOP_BUY => '商城购买', + self::ADMIN_ADJUST => '管理员调整', + }; + } +} diff --git a/app/Http/Controllers/Admin/CurrencyStatsController.php b/app/Http/Controllers/Admin/CurrencyStatsController.php new file mode 100644 index 0000000..9628b80 --- /dev/null +++ b/app/Http/Controllers/Admin/CurrencyStatsController.php @@ -0,0 +1,69 @@ +input('date', today()->toDateString()); + + // 各来源活动产出统计(按 source + currency 分组汇总) + $stats = $this->currencyService->activityStats($date); + + // 按货币类型分组,方便视图展示 + $statsByType = $stats->groupBy('currency')->map( + fn ($rows) => $rows->keyBy('source') + ); + + // 今日净流通量(正向增加 - 负向消耗),可判断通货膨胀 + $netFlow = []; + foreach (['exp', 'gold', 'charm'] as $currency) { + $totalIn = UserCurrencyLog::whereDate('created_at', $date) + ->where('currency', $currency)->where('amount', '>', 0) + ->sum('amount'); + $totalOut = UserCurrencyLog::whereDate('created_at', $date) + ->where('currency', $currency)->where('amount', '<', 0) + ->sum('amount'); + $netFlow[$currency] = [ + 'in' => $totalIn, + 'out' => abs($totalOut), + 'net' => $totalIn + $totalOut, // 净增量 + ]; + } + + // 所有已知来源(供视图展示缺失来源的空行) + $allSources = CurrencySource::cases(); + + return view('admin.currency-stats.index', compact( + 'date', 'stats', 'statsByType', 'netFlow', 'allSources', + )); + } +} diff --git a/app/Http/Controllers/Admin/UserManagerController.php b/app/Http/Controllers/Admin/UserManagerController.php index 9c58142..c3b69ac 100644 --- a/app/Http/Controllers/Admin/UserManagerController.php +++ b/app/Http/Controllers/Admin/UserManagerController.php @@ -12,7 +12,9 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Enums\CurrencySource; use App\Models\User; +use App\Services\UserCurrencyService; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -22,6 +24,12 @@ use Illuminate\View\View; class UserManagerController extends Controller { + /** + * 注入统一积分服务(用于管理员调整经验/金币/魅力时记录流水) + */ + public function __construct( + private readonly UserCurrencyService $currencyService, + ) {} /** * 显示用户列表及搜索(支持按等级/经验/金币/魅力排序) */ @@ -90,13 +98,35 @@ class UserManagerController extends Controller $targetUser->sex = $validated['sex']; } if (isset($validated['exp_num'])) { - $targetUser->exp_num = $validated['exp_num']; + // 计算差值并通过统一服务记录流水(管理员手动调整) + $expDiff = $validated['exp_num'] - ($targetUser->exp_num ?? 0); + if ($expDiff !== 0) { + $this->currencyService->change( + $targetUser, 'exp', $expDiff, CurrencySource::ADMIN_ADJUST, + "管理员 {$currentUser->username} 手动调整经验", + ); + $targetUser->refresh(); + } } if (isset($validated['jjb'])) { - $targetUser->jjb = $validated['jjb']; + $jjbDiff = $validated['jjb'] - ($targetUser->jjb ?? 0); + if ($jjbDiff !== 0) { + $this->currencyService->change( + $targetUser, 'gold', $jjbDiff, CurrencySource::ADMIN_ADJUST, + "管理员 {$currentUser->username} 手动调整金币", + ); + $targetUser->refresh(); + } } if (isset($validated['meili'])) { - $targetUser->meili = $validated['meili']; + $meiliDiff = $validated['meili'] - ($targetUser->meili ?? 0); + if ($meiliDiff !== 0) { + $this->currencyService->change( + $targetUser, 'charm', $meiliDiff, CurrencySource::ADMIN_ADJUST, + "管理员 {$currentUser->username} 手动调整魅力", + ); + $targetUser->refresh(); + } } if (array_key_exists('qianming', $validated)) { $targetUser->qianming = $validated['qianming']; diff --git a/app/Http/Controllers/FishingController.php b/app/Http/Controllers/FishingController.php index 4313cd7..727464a 100644 --- a/app/Http/Controllers/FishingController.php +++ b/app/Http/Controllers/FishingController.php @@ -13,8 +13,10 @@ namespace App\Http\Controllers; use App\Events\MessageSent; +use App\Enums\CurrencySource; use App\Models\Sysparam; use App\Services\ChatStateService; +use App\Services\UserCurrencyService; use App\Services\VipService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -24,8 +26,9 @@ use Illuminate\Support\Facades\Redis; class FishingController extends Controller { public function __construct( - private readonly ChatStateService $chatState, - private readonly VipService $vipService, + private readonly ChatStateService $chatState, + private readonly VipService $vipService, + private readonly UserCurrencyService $currencyService, ) {} /** @@ -61,9 +64,16 @@ class FishingController extends Controller ], 422); } - // 3. 扣除金币 - $user->jjb = max(0, ($user->jjb ?? 0) - $cost); - $user->save(); + // 3. 扣除金币(通过统一积分服务记录流水) + $this->currencyService->change( + $user, + 'gold', + -$cost, + CurrencySource::FISHING_COST, + "钓鱼抛竿消耗 {$cost} 金币", + $id, + ); + $user->refresh(); // 刷新本地模型(service 已原子更新) // 4. 设置"正在钓鱼"标记(防止重复抛竿,30秒后自动过期) Redis::setex("fishing:active:{$user->id}", 30, time()); @@ -113,18 +123,24 @@ class FishingController extends Controller // 3. 随机决定钓鱼结果 $result = $this->randomFishResult(); - // 4. 更新用户经验和金币(正向奖励按 VIP 倍率加成,负面惩罚不变) + // 4. 通过统一积分服务更新经验和金币,写入流水 $expMul = $this->vipService->getExpMultiplier($user); $jjbMul = $this->vipService->getJjbMultiplier($user); if ($result['exp'] !== 0) { $finalExp = $result['exp'] > 0 ? (int) round($result['exp'] * $expMul) : $result['exp']; - $user->exp_num = max(0, ($user->exp_num ?? 0) + $finalExp); + $this->currencyService->change( + $user, 'exp', $finalExp, CurrencySource::FISHING_GAIN, + "钓鱼收竿:{$result['message']}", $id, + ); } if ($result['jjb'] !== 0) { $finalJjb = $result['jjb'] > 0 ? (int) round($result['jjb'] * $jjbMul) : $result['jjb']; - $user->jjb = max(0, ($user->jjb ?? 0) + $finalJjb); + $this->currencyService->change( + $user, 'gold', $finalJjb, CurrencySource::FISHING_GAIN, + "钓鱼收竿:{$result['message']}", $id, + ); } - $user->save(); + $user->refresh(); // 刷新获取最新余额 // 5. 广播钓鱼结果到聊天室 $sysMsg = [ diff --git a/app/Http/Controllers/LeaderboardController.php b/app/Http/Controllers/LeaderboardController.php index 5924815..207cda4 100644 --- a/app/Http/Controllers/LeaderboardController.php +++ b/app/Http/Controllers/LeaderboardController.php @@ -3,6 +3,7 @@ /** * 文件功能:全局风云排行榜控制器 * 各种维度(等级、经验、交友币、魅力)的前20名抓取与缓存展示。 + * 新增今日榜:显示今天经验成长、今日金币获得、今日魅力增长最多的用户。 * * @author ChatRoom Laravel * @@ -12,26 +13,33 @@ namespace App\Http\Controllers; use App\Models\User; +use App\Services\UserCurrencyService; use Illuminate\Support\Facades\Cache; use Illuminate\View\View; class LeaderboardController extends Controller { /** - * 渲染排行榜主视角 + * 注入积分统计服务(用于今日榜单数据查询) + */ + public function __construct( + private readonly UserCurrencyService $currencyService, + ) {} + + /** + * 渲染排行榜主视角(包含累计榜 + 今日榜) */ public function index(): View { - // 缓存 15 分钟,防止每秒几百个人看排行榜把数据库扫死 - // 选用 remember 则在过期时自动执行闭包查询并重置缓存 - $ttl = 60 * 15; - // 管理员等级阈值,排行榜中隐藏管理员 $superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100'); // 排行榜显示人数(后台可配置) $topN = (int) \App\Models\Sysparam::getValue('leaderboard_limit', '20'); + // ── 累计榜(15分钟缓存)────────────────────────────── + $ttl = 60 * 15; + // 1. 境界榜 (以 user_level 为尊) $topLevels = Cache::remember('leaderboard:top_levels', $ttl, function () use ($superLevel, $topN) { return User::select('id', 'username', 'usersf', 'user_level', 'sex') @@ -76,6 +84,36 @@ class LeaderboardController extends Controller ->get(); }); - return view('leaderboard.index', compact('topLevels', 'topExp', 'topWealth', 'topCharm')); + // ── 今日榜(5分钟缓存,数据来自 user_currency_logs 流水表)── + $todayTtl = 60 * 5; + $today = today()->toDateString(); + + $todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl, + fn () => $this->currencyService->todayLeaderboard('exp', $topN, $today) + ); + $todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl, + fn () => $this->currencyService->todayLeaderboard('gold', $topN, $today) + ); + $todayCharm = Cache::remember("leaderboard:today_charm:{$today}", $todayTtl, + fn () => $this->currencyService->todayLeaderboard('charm', $topN, $today) + ); + + return view('leaderboard.index', compact( + 'topLevels', 'topExp', 'topWealth', 'topCharm', + 'todayExp', 'todayGold', 'todayCharm', + )); + } + + /** + * 用户个人流水日志页(查询自己的经验/金币/魅力操作历史) + */ + public function myLogs(): View + { + $user = auth()->user(); + $currency = request('currency'); + $days = (int) request('days', 7); + $logs = $this->currencyService->userLogs($user->id, $currency ?: null, $days); + + return view('leaderboard.my-logs', compact('logs', 'user', 'currency', 'days')); } } diff --git a/app/Models/UserCurrencyLog.php b/app/Models/UserCurrencyLog.php new file mode 100644 index 0000000..fd94efa --- /dev/null +++ b/app/Models/UserCurrencyLog.php @@ -0,0 +1,100 @@ + 'integer', + 'balance_after'=> 'integer', + 'room_id' => 'integer', + 'created_at' => 'datetime', + ]; + + // ─── 关联 ───────────────────────────────────────────────── + + /** + * 关联用户(流水属于哪个用户) + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + // ─── 作用域(Scope)快捷查询 ────────────────────────────── + + /** + * 按货币类型过滤 + */ + public function scopeCurrency(Builder $query, string $currency): Builder + { + return $query->where('currency', $currency); + } + + /** + * 今日数据 + */ + public function scopeToday(Builder $query): Builder + { + return $query->whereDate('created_at', today()); + } + + /** + * 指定日期数据 + */ + public function scopeOnDate(Builder $query, string $date): Builder + { + return $query->whereDate('created_at', $date); + } + + /** + * 仅正向增加(用于排行榜,不算消耗) + */ + public function scopeGain(Builder $query): Builder + { + return $query->where('amount', '>', 0); + } + + /** + * 按来源过滤 + */ + public function scopeSource(Builder $query, string $source): Builder + { + return $query->where('source', $source); + } +} diff --git a/app/Services/UserCurrencyService.php b/app/Services/UserCurrencyService.php new file mode 100644 index 0000000..12841e2 --- /dev/null +++ b/app/Services/UserCurrencyService.php @@ -0,0 +1,192 @@ + 'exp_num', + 'gold' => 'jjb', + 'charm' => 'meili', + ]; + + /** + * 统一变更用户货币属性并写入流水记录。 + * 使用数据库事务保证原子性:用户属性更新 + 流水写入同时成功或同时回滚。 + * + * @param User $user 目标用户 + * @param string $currency 货币类型('exp' / 'gold' / 'charm') + * @param int $amount 变更量,正数增加,负数扣除 + * @param CurrencySource $source 来源活动枚举 + * @param string $remark 备注说明 + * @param int|null $roomId 所在房间 ID(可选) + */ + public function change( + User $user, + string $currency, + int $amount, + CurrencySource $source, + string $remark = '', + ?int $roomId = null, + ): void { + if ($amount === 0) { + return; // 变更量为 0 不写记录 + } + + $field = self::FIELD_MAP[$currency] ?? null; + if (! $field) { + return; // 未知货币类型,静默忽略(不抛异常,避免影响主流程) + } + + DB::transaction(function () use ($user, $currency, $amount, $source, $remark, $roomId, $field) { + // 原子性更新用户属性(用 increment/decrement 防并发竞态) + if ($amount > 0) { + $user->increment($field, $amount); + } else { + // 扣除时不让余额低于 0 + $user->decrement($field, min(abs($amount), $user->$field ?? 0)); + } + + // 重新读取最新余额(避免缓存脏数据) + $balanceAfter = (int) $user->fresh()->$field; + + // 写入流水记录(快照当前用户名,排行 JOIN 时再取最新名) + UserCurrencyLog::create([ + 'user_id' => $user->id, + 'username' => $user->username, + 'currency' => $currency, + 'amount' => $amount, + 'balance_after'=> $balanceAfter, + 'source' => $source->value, + 'remark' => $remark, + 'room_id' => $roomId, + ]); + }); + } + + /** + * 批量变更多个用户的货币属性(适用于自动存点:一次操作多人)。 + * 每位用户仍独立走事务,单人失败不影响其他人。 + * + * @param array $items [['user' => User, 'changes' => ['exp'=>1,'gold'=>2]], ...] + * @param CurrencySource $source + * @param int|null $roomId + */ + public function batchChange(array $items, CurrencySource $source, ?int $roomId = null): void + { + foreach ($items as $item) { + $user = $item['user']; + $changes = $item['changes'] ?? []; + foreach ($changes as $currency => $amount) { + $this->change($user, $currency, (int) $amount, $source, '', $roomId); + } + } + } + + /** + * 查询某日各来源活动的产出统计(后台统计页面使用)。 + * 返回格式:Collection of stdClass { source, currency, total_amount, participant_count } + * + * @param string|null $date 日期字符串如 '2026-02-28',默认今日 + */ + public function activityStats(?string $date = null): Collection + { + $date = $date ?? today()->toDateString(); + + return UserCurrencyLog::query() + ->whereDate('created_at', $date) + ->selectRaw('source, currency, SUM(amount) as total_amount, COUNT(DISTINCT user_id) as participant_count') + ->groupBy('source', 'currency') + ->orderBy('currency') + ->orderByRaw('ABS(SUM(amount)) DESC') + ->get(); + } + + /** + * 今日排行榜(按 user_id 聚合,展示最新用户名)。 + * 只统计正向变更(amount > 0),不因消耗而扣分。 + * + * @param string $currency 'exp' | 'gold' | 'charm' + * @param int $limit 返回条数 + * @param string|null $date 日期,默认今日 + */ + public function todayLeaderboard(string $currency, int $limit = 20, ?string $date = null): Collection + { + $date = $date ?? today()->toDateString(); + + return UserCurrencyLog::query() + ->whereDate('created_at', $date) + ->where('currency', $currency) + ->where('amount', '>', 0) // 只统计正向 + ->selectRaw('user_id, SUM(amount) as total') + ->groupBy('user_id') + ->orderByRaw('SUM(amount) DESC') + ->limit($limit) + ->get() + ->map(function ($row) { + // JOIN 取最新用户名(避免改名后显示旧名) + $user = User::select('id', 'username', 'user_level', 'sex', 'headface') + ->find($row->user_id); + + return (object) [ + 'user_id' => $row->user_id, + 'username' => $user?->username ?? '未知用户', + 'level' => $user?->user_level ?? 0, + 'sex' => $user?->sex ?? 1, + 'headface' => $user?->headface ?? '1.gif', + 'total' => $row->total, + ]; + }); + } + + /** + * 用户个人流水明细(用户查询自己的日志)。 + * + * @param int $userId 用户 ID + * @param string|null $currency 为 null 时返回所有货币类型 + * @param int $days 查询最近多少天 + */ + public function userLogs(int $userId, ?string $currency = null, int $days = 7): Collection + { + return UserCurrencyLog::query() + ->where('user_id', $userId) + ->when($currency, fn ($q) => $q->where('currency', $currency)) + ->where('created_at', '>=', now()->subDays($days)) + ->orderByDesc('created_at') + ->limit(200) + ->get(); + } + + /** + * 货币类型中文名映射(用于视图展示)。 + */ + public static function currencyLabel(string $currency): string + { + return match ($currency) { + 'exp' => '经验', + 'gold' => '金币', + 'charm' => '魅力', + default => $currency, + }; + } +} diff --git a/config/app.php b/config/app.php index 423eed5..9b24e59 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => 'Asia/Shanghai', /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2026_02_28_123839_create_user_currency_logs_table.php b/database/migrations/2026_02_28_123839_create_user_currency_logs_table.php new file mode 100644 index 0000000..98a19bb --- /dev/null +++ b/database/migrations/2026_02_28_123839_create_user_currency_logs_table.php @@ -0,0 +1,72 @@ +id(); + + // 真实身份锚点:使用 user_id,即使用户改名历史记录仍可准确归属 + $table->unsignedBigInteger('user_id')->index()->comment('用户 ID(身份锚点,不随改名变化)'); + + // 写入时的用户名快照:仅用于日志可读性,排行榜展示取 users.username 当前值 + $table->string('username', 50)->comment('操作时刻用户名快照'); + + // 货币类型(对应 CurrencySource 枚举,varchar 允许未来无缝新增) + $table->string('currency', 20)->comment('货币类型: exp / gold / charm / 以后可自由扩展'); + + // 变更量(正数增加,负数扣除) + $table->integer('amount')->comment('变更量,正数为增加,负数为扣除'); + + // 变更后余额(方便对账,无需重算历史) + $table->integer('balance_after')->comment('变更后余额快照'); + + // 来源活动(varchar 非 ENUM,新增活动无需改表,只在 CurrencySource 枚举加一行) + $table->string('source', 30)->comment('来源标识: auto_save / fishing_gain / send_gift / ...'); + + // 备注(补充说明,如"送花给流星") + $table->string('remark', 255)->default('')->comment('操作备注'); + + // 发生所在房间(部分操作无房间) + $table->unsignedInteger('room_id')->nullable()->comment('所在房间 ID,无则为 null'); + + // 只记录创建时间,流水不可修改 + $table->timestamp('created_at')->useCurrent()->comment('记录时间'); + + // ── 索引(按高频查询场景设计)──────────────────────────── + // 今日排行快速聚合:按货币类型 + 日期 + 金额 + $table->index(['currency', 'created_at', 'amount'], 'idx_daily_board'); + + // 用户个人流水查询:按 user_id + 货币类型 + 时间 + $table->index(['user_id', 'currency', 'created_at'], 'idx_user_log'); + + // 活动统计聚合:按来源 + 货币类型 + 日期 + $table->index(['source', 'currency', 'created_at'], 'idx_source_stat'); + }); + } + + /** + * 回滚:删除流水表。 + */ + public function down(): void + { + Schema::dropIfExists('user_currency_logs'); + } +}; diff --git a/resources/views/admin/currency-stats/index.blade.php b/resources/views/admin/currency-stats/index.blade.php new file mode 100644 index 0000000..bfe7636 --- /dev/null +++ b/resources/views/admin/currency-stats/index.blade.php @@ -0,0 +1,102 @@ +{{-- + 文件功能:后台积分活动统计页面 + 显示指定日期下各来源活动(钓鱼、存点等)产出的经验/金币/魅力统计,以及今日净流通量 + + @extends admin/layouts +--}} +@extends('layouts.admin') + +@section('title', '积分流水统计') + +@section('content') +
+ + {{-- 页头 --}} +
+

📊 积分流水活动统计

+ {{-- 日期选择器 --}} +
+ + + +
+
+ + {{-- 净流通量摘要卡片 --}} +
+ @foreach (['exp' => ['label' => '经验', 'icon' => '⚡', 'color' => 'amber'], 'gold' => ['label' => '金币', 'icon' => '💰', 'color' => 'yellow'], 'charm' => ['label' => '魅力', 'icon' => '🌸', 'color' => 'pink']] as $cur => $info) + @php $flow = $netFlow[$cur] ?? ['in'=>0,'out'=>0,'net'=>0]; @endphp +
+
+ {{ $info['icon'] }} + {{ $info['label'] }} 流通 +
+
+ +{{ number_format($flow['in']) }} 流入 + -{{ number_format($flow['out']) }} 消耗 +
+
+ 净增:{{ $flow['net'] >= 0 ? '+' : '' }}{{ number_format($flow['net']) }} +
+
+ @endforeach +
+ + {{-- 来源活动详细统计表 --}} +
+
+

各活动来源产出明细

+

日期:{{ $date }}(仅统计正向增加,不含消耗)

+
+ + + + + + + + + + + + @foreach ($allSources as $source) + @php + $expRow = $statsByType['exp'][$source->value] ?? null; + $goldRow = $statsByType['gold'][$source->value] ?? null; + $charmRow = $statsByType['charm'][$source->value] ?? null; + $hasData = $expRow || $goldRow || $charmRow; + $maxParticipants = max( + $expRow?->participant_count ?? 0, + $goldRow?->participant_count ?? 0, + $charmRow?->participant_count ?? 0, + ); + @endphp + + + + + + + + @endforeach + +
来源活动⚡ 经验产出💰 金币产出🌸 魅力产出参与人次
{{ $source->label() }} + {{ $expRow ? number_format($expRow->total_amount) : '—' }} + + {{ $goldRow ? number_format($goldRow->total_amount) : '—' }} + + {{ $charmRow ? number_format($charmRow->total_amount) : '—' }} + + {{ $maxParticipants > 0 ? $maxParticipants . ' 人' : '—' }} +
+
+ +
+@endsection diff --git a/resources/views/leaderboard/my-logs.blade.php b/resources/views/leaderboard/my-logs.blade.php new file mode 100644 index 0000000..f664844 --- /dev/null +++ b/resources/views/leaderboard/my-logs.blade.php @@ -0,0 +1,103 @@ +{{-- + 文件功能:用户个人积分流水日志页面 + 用户可筛选查看自己的经验、金币、魅力变动历史,按日期倒序排列 + + @extends layouts.app +--}} +@extends('layouts.app') + +@section('title', '我的积分记录 - 飘落流星') +@section('nav-icon', '📊') +@section('nav-title', '我的积分记录') + +@section('content') +
+
+ + {{-- 筛选栏 --}} +
+ 筛选: + + {{-- 货币类型 --}} +
+ @foreach(['' => '全部', 'exp' => '⚡ 经验', 'gold' => '💰 金币', 'charm' => '🌸 魅力'] as $val => $label) + + {{ $label }} + + @endforeach +
+ + {{-- 日期范围 --}} +
+ @foreach([7 => '7 天', 14 => '14 天', 30 => '30 天'] as $d => $label) + + {{ $label }} + + @endforeach +
+
+ + {{-- 流水列表 --}} +
+ @if($logs->isEmpty()) +
+
📭
+

暂无积分记录,快去钓鱼或挂机吧!

+
+ @else + + + + + + + + + + + + + @foreach($logs as $log) + @php + $currencyIcons = ['exp' => '⚡', 'gold' => '💰', 'charm' => '🌸']; + $icon = $currencyIcons[$log->currency] ?? '📌'; + $isPositive = $log->amount >= 0; + @endphp + + + + + + + + + @endforeach + +
时间类型来源变动变动后余额备注
+ {{ \Carbon\Carbon::parse($log->created_at)->format('m-d H:i') }} + + {{ $icon }} {{ \App\Services\UserCurrencyService::currencyLabel($log->currency) }} + + @php + $sourceLabel = ''; + try { $sourceLabel = \App\Enums\CurrencySource::from($log->source)->label(); } catch (\Throwable) { $sourceLabel = $log->source; } + @endphp + {{ $sourceLabel }} + + {{ $isPositive ? '+' : '' }}{{ number_format($log->amount) }} + + {{ number_format($log->balance_after) }} + + {{ $log->remark ?: '—' }} +
+ @endif +
+ +

最多显示最近 200 条记录

+
+
+@endsection diff --git a/routes/web.php b/routes/web.php index dbacd4f..1bdae9e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -41,6 +41,9 @@ Route::middleware(['chat.auth'])->group(function () { // ---- 第九阶段:外围矩阵 - 风云排行榜 ---- Route::get('/leaderboard', [\App\Http\Controllers\LeaderboardController::class, 'index'])->name('leaderboard.index'); + // 用户个人积分流水日志(查询自己的经验/金币/魅力历史) + Route::get('/my/currency-logs', [\App\Http\Controllers\LeaderboardController::class, 'myLogs'])->name('currency.my-logs'); + // ---- 第十阶段:站内信与留言板系统 ---- Route::get('/guestbook', [\App\Http\Controllers\GuestbookController::class, 'index'])->name('guestbook.index'); Route::post('/guestbook', [\App\Http\Controllers\GuestbookController::class, 'store'])->middleware('throttle:10,1')->name('guestbook.store'); @@ -146,6 +149,9 @@ Route::middleware(['chat.auth', 'chat.level:super'])->prefix('admin')->name('adm Route::put('/vip/{id}', [\App\Http\Controllers\Admin\VipController::class, 'update'])->name('vip.update'); Route::delete('/vip/{id}', [\App\Http\Controllers\Admin\VipController::class, 'destroy'])->name('vip.destroy'); + // 积分流水活动统计(管理员查看每日经验/金币产出概况) + Route::get('/currency-stats', [\App\Http\Controllers\Admin\CurrencyStatsController::class, 'index'])->name('currency-stats.index'); + // AI 厂商配置管理 Route::middleware(['chat.site_owner'])->group(function () { Route::get('/ai-providers', [\App\Http\Controllers\Admin\AiProviderController::class, 'index'])->name('ai-providers.index');