---
name: chatroom-ride-development
description: "开发 /Users/pllx/Web/Herd/chatroom 的聊天室座驾。适用于新增 ride_<key> 独立座驾、全屏 Canvas 特效、座驾音效、后台配置、前台座驾页面和进房欢迎语。"
---

# Chatroom Ride Development

## 适用场景

- 新增或修改聊天室独立座驾。
- 新增 `resources/js/effects/<key>.js` 全屏座驾特效。
- 调整座驾购买、续期、替换、入场欢迎语或后台价格/天数配置。
- 排查座驾进房后特效不播放、欢迎语不显示、购买记录不正确的问题。
- 排查座驾购买没有金币流水、后台座驾列表为空、前台座驾弹窗为空的问题。

## 必须遵守

- 座驾是独立模块：必须使用 `rides` 和 `user_ride_purchases`，不要写入 `shop_items` 或 `user_purchases`。
- 商店模块只管理普通商店商品；不要给 `ShopItem` 增加 `TYPE_RIDE`、`isRide()`、`rideKey()` 或座驾欢迎语字段。
- 座驾 slug 固定为 `ride_<effect_key>`。
- `<effect_key>` 必须同时出现在：
  - `rides.slug`
  - `rides.effect_key`
  - `Ride::rideKey()` 可解析结果
  - `EffectBroadcast::TYPES`
  - `resources/js/effects/effect-manager.js`
  - `resources/js/effects/effect-sounds.js`
  - `resources/js/effects/<effect_key>.js`
- 新增座驾默认通过迁移或 Seeder 写入 `rides`，字段至少包含名称、slug、`effect_key`、图标、价格、`duration_days`、排序和 `welcome_message`。
- `welcome_message` 支持 `{name}` 和 `{ride}`，输出前必须转义，不能直接信任后台输入。
- 当前版本只允许用户同时拥有一个 active 座驾；同款续购叠加有效期，不同款替换旧座驾并把旧记录置为 `cancelled`。
- 用户购买记录必须写入 `user_ride_purchases`，不要复用商店购买记录。
- 座驾购买扣金币必须走 `UserCurrencyService::deductGoldIfEnough()`，来源使用 `CurrencySource::RIDE_BUY`，并写入 `user_currency_logs`。
- 座驾购买接口必须携带房间 ID，并把 `room_id` 传给金币流水，方便后台按房间追溯。
- 测试中禁止使用 `Redis::flushall()` 清理聊天室状态；只允许清理 `room:*` 相关 key，避免把本机登录 session 一起删掉。

## 模块边界

- 前台接口：`GET /rides/items`、`POST /rides/buy`。
- 后台入口：`GET /admin/rides`，页面文件为 `resources/views/admin/rides/index.blade.php`。
- 前台弹窗脚本：`resources/js/chat-room/ride-controls.js`。
- 业务服务：`app/Services/RideService.php`。
- 数据模型：`app/Models/Ride.php`、`app/Models/UserRidePurchase.php`。
- 请求验证：`BuyRideRequest`、`StoreRideRequest`、`UpdateRideRequest`。

## 已验收的进房展示规则

- 用户有有效座驾时，座驾优先级最高：
  - 只发送一条 `座驾播报` 文字消息。
  - 不再发送普通 `进出播报`。
  - 不再播放会员进场横幅或会员全屏特效。
- 座驾全屏特效播放范围是当前房间内所有在线用户：
  - 进房用户本人通过 `initialRideEffect` 本地播放。
  - 其他在线用户通过 `EffectBroadcast` 的 `room.{roomId}` PresenceChannel 播放。
  - 不设置 `target_username`，表示当前房间全员可见。
- 座驾文字播报口径：
  - 显示 `部门 <部门> · 职务 <图标 职务> · 会员 <图标 会员>`。
  - 不在身份行前面显示 `用户 <用户名>`，因为后面的欢迎语模板已经包含 `{name}`。
  - 示例：`🚀 部门 办公厅 · 职务 🎖️ 技术总监 · 会员 👑 至尊会员 · 【流星】乘【东风-5C战略导弹】点火升空...`
- 座驾动画 HUD 标题口径：
  - 第一行用户身份信息显示 `用户 <用户名> · 部门 <部门> · 职务 <图标 职务> · 会员 <图标 会员>`。
  - 第二行标题只显示 `乘坐【<座驾名称>】闪亮登场`，不要重复用户名。
  - `effect_user_info` 供动画 HUD 第一行使用，`effect_title` 供动画 HUD 第二行使用，`identity_text` 只供文字播报使用。
- 座驾购买成功必须向当前房间广播文字通知：
  - 通知使用 `系统传音`，`action = ride_purchase`。
  - 文案包含 `【座驾】`，前端会按百家乐同款紧凑卡片样式渲染。
  - 通知末尾必须带 `购买座驾` 按钮，点击调用 `openRideModal()`，方便其他人快速购买。
  - 购买通知必须写入 `ChatStateService`、广播 `MessageSent`，并通过 `SaveMessageJob` 落库。
- 前端改动后如果浏览器仍显示旧动画标题，必须运行 `npm run build` 或确认 Vite dev server 已刷新，避免加载旧的 `public/build` 资源。

## 新增座驾步骤

1. 新增全屏特效文件：`resources/js/effects/<effect_key>.js`。
2. 在 `effect-manager.js` 注册模块加载和启动分支。
3. 在 `effect-sounds.js` 注册音效启动分支。
4. 在 `EffectBroadcast::TYPES` 加入 `<effect_key>`。
5. 在迁移或 Seeder 中新增 `rides` 记录，slug 使用 `ride_<effect_key>`，`effect_key` 使用 `<effect_key>`。
6. 确认后台 `座驾管理` 可以配置价格、使用天数、欢迎语、上下架状态和排序。
7. 确认 `RideService::buy()` 购买时写入 `user_ride_purchases` 和 `user_currency_logs`。
8. 更新座驾相关 PHPUnit 测试，至少覆盖列表、余额不足、购买成功、金币流水、续期、替换和进房触发。

## 金币流水要求

- 购买成功必须在 `user_currency_logs` 中写入：
  - `currency = gold`
  - `amount = -座驾价格`
  - `source = ride_buy`
  - `remark = 购买聊天室座驾：<座驾名称>`
  - `room_id = 当前房间 ID`
- 余额不足时不能写 `user_ride_purchases`，也不能写 `user_currency_logs`。
- 不要用 `$user->decrement('jjb', ...)` 直接扣金币。

## 旧方案清理要求

- 不要恢复 `add_ride_fields_to_shop_items` 这类把座驾挂到商店表的迁移。
- 允许保留清理迁移，用于删除旧环境中 `shop_items.slug like ride_%`、`shop_items.type = ride` 或 `shop_items.welcome_message` 残留。
- 修改座驾模块后，检查 `rg 'TYPE_RIDE|ShopItem::rideKey|RideItemController|复用商店购买记录'`，确认没有旧方案引用。

## 验证清单

- `node --check resources/js/effects/<effect_key>.js`
- `node --check resources/js/effects/effect-manager.js`
- `node --check resources/js/effects/effect-sounds.js`
- `php artisan test --compact tests/Feature/RideControllerTest.php`
- 有进房逻辑变更时运行相关 `ChatControllerTest` 过滤用例。
- 涉及商店边界时运行 `php artisan test --compact tests/Feature/ShopControllerTest.php`。
- 修改 PHP 后运行 `vendor/bin/pint --dirty --format=agent`。

## 特别注意

- 如果从 stash 恢复昨天的座驾特效，必须确认 untracked 父提交中的新特效文件也已恢复，不能只恢复已跟踪文件。
- `99a` 这种以数字开头的 key 在 JS 对象字面量里必须加引号。
- 新座驾的展示名可以是中文，但 effect key 必须保持小写短横线/数字/字母风格，避免前后端匹配失败。
- 本项目 PHP 文件要求类和方法上方有中文 DocBlock；新增座驾相关 PHP 文件时必须补齐。
