Files
chatroom/docs/phase1-decoration-plan.md
T
2026-04-27 11:23:08 +08:00

449 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第一阶段:消息装扮 & 头像框消费系统 — 实施方案
> 目标:为聊天室新增持续性的金币消费出口,通过「消息装扮」和「头像框」两个高社交可见度的功能,让金币有处可花、花得有面子。
---
## 一、功能范围
### 1.1 消息气泡样式(msg_bubble
用户购买后,发出的消息气泡带特殊边框/背景样式,房间内所有人可见。
| 商品 | 价格 | 时长 | 效果描述 |
|------|------|------|----------|
| 金色气泡 | 300 金币 | 1 天 | 消息框带金色渐变边框 + 微光 |
| 樱花气泡 | 500 金币 | 3 天 | 粉白边框 + 飘落花瓣 |
| 星际气泡 | 800 金币 | 7 天 | 深蓝渐变边框 + 星光 |
| 彩虹气泡 | 1500 金币 | 7 天 | 流动彩虹边框 |
| 皇冠气泡 | 3000 金币 | 30 天 | 皇家风格金边 + 皇冠徽标 |
### 1.2 昵称颜色效果(msg_name_color
购买后,自己的昵称在聊天消息和用户列表中显示为特殊颜色。
| 商品 | 价格 | 时长 | 效果描述 |
|------|------|------|----------|
| 金色昵称 | 200 金币 | 1 天 | 昵称文字变为金色 #fbbf24 |
| 渐变色昵称 | 500 金币 | 3 天 | 彩虹渐变色(CSS gradient text |
| 发光昵称 | 800 金币 | 7 天 | 文字发光效果(text-shadow glow |
| 火焰昵称 | 1500 金币 | 7 天 | 红橙火焰色 + 脉动动画 |
### 1.3 头像框(avatar_frame
购买后,用户头像外围显示装饰边框,在用户列表中展示。
| 商品 | 价格 | 时长 | 效果描述 |
|------|------|------|----------|
| 银色边框 | 500 金币 | 7 天 | 银色圆环边框 |
| 金色边框 | 1000 金币 | 7 天 | 金色圆环边框 |
| 星光边框 | 2000 金币 | 14 天 | 星光闪烁旋转边框 |
| 龙纹边框 | 5000 金币 | 30 天 | 龙纹缠绕高级边框 |
---
## 二、架构设计
### 2.1 整体思路
遵循项目现有的 `ShopService → UserCurrencyService` 消费模式,最小化架构改动:
```
用户点击购买 → ShopController::buy()
→ ShopService::buyItem() [新增 msg_bubble / msg_name_color / avatar_frame 分支]
→ DB::transaction:
① UserCurrencyService::change() 扣金币 + 写流水
② UserPurchase::create() 写购买记录
③ 更新 users.active_decorations JSON 字段
→ 返回购买结果 + 新余额
发送消息时 → ChatController::send()
→ 读取 users.active_decorations
→ 写入 messageData 广播 payload
用户列表 → ChatUserPresenceService::build()
→ 读取 users.active_decorations.avatar_frame
→ 写入 presence payload
```
### 2.2 数据存储方案
**不使用新表**,在现有两张表上扩展:
**`users` 表** — 新增 1 个字段用于缓存当前激活的装扮(避免每次发消息都 JOIN user_purchases):
```sql
ALTER TABLE users ADD COLUMN active_decorations JSON NULL
COMMENT '当前激活的装扮,格式: {"bubble":{"style":"golden","expires_at":"..."},"name_color":{...},"avatar_frame":{...}}';
```
示例数据:
```json
{
"bubble": {"style": "golden", "expires_at": "2026-05-04T12:00:00+08:00"},
"name_color": {"style": "rainbow", "expires_at": "2026-05-01T12:00:00+08:00"},
"avatar_frame": {"style": "dragon", "expires_at": "2026-05-27T12:00:00+08:00"}
}
```
**`shop_items` 表** — 新增商品行,利用现有的 `type``duration_days` 字段(无需改表结构)。
**`user_purchases` 表** — 利用现有表记录所有购买历史(无需改表结构)。
### 2.3 装扮过期策略
**懒过期(Lazy Expiration**:每次读取 `active_decorations` 时检查过期时间,自动清理。
在以下时机触发检查和清理:
- 用户发送消息时(`ChatController::send()`
- 构建在线用户 payload 时(`ChatUserPresenceService::build()`
- 查询商店数据时(`ShopController::items()`
过期清理逻辑在一个统一的 helper 方法中:
```php
// DecorationService::getActiveDecorations(User $user): array
// 读取 users.active_decorations JSON
// 过滤掉已过期的条目
// 如果有变化,写回 users.active_decorations
// 返回干净的激活装扮列表
```
---
## 三、后端改动清单
### 3.1 数据库 Migration
**新文件**`database/migrations/xxxx_xx_xx_add_active_decorations_to_users_table.php`
```php
Schema::table('users', function (Blueprint $table) {
$table->json('active_decorations')->nullable()
->comment('当前激活的装扮: bubble/name_color/avatar_frame');
});
```
### 3.2 CurrencySource 枚举
**文件**`app/Enums/CurrencySource.php` — 新增两个 case
```php
/** 购买消息装扮(气泡/昵称颜色等) */
case MSG_DECORATION_BUY = 'msg_decoration_buy';
/** 购买头像框 */
case AVATAR_FRAME_BUY = 'avatar_frame_buy';
```
同时在 `label()` 方法中添加对应的中文映射。
### 3.3 DecorationService(新服务)
**文件**`app/Services/DecorationService.php`
```php
class DecorationService
{
/**
* 购买装扮:扣金币 + 写记录 + 更新 active_decorations
*/
public function purchase(User $user, ShopItem $item): array;
/**
* 获取用户当前激活的装扮(自动清理过期项)
* 返回: ['bubble' => [...], 'name_color' => [...], 'avatar_frame' => [...]]
*/
public function getActiveDecorations(User $user): array;
/**
* 判断装扮类型之间是否互斥(同类型新购买覆盖旧的)
*/
public function isSameCategory(string $typeA, string $typeB): bool;
}
```
**`purchase()` 核心逻辑**
1. 计算总价(`$item->price`
2. 检查金币余额
3. `DB::transaction`
- 通过 `UserCurrencyService` 扣金币
- 创建 `UserPurchase` 记录(status=active, expires_at=now+days
- 读取当前 `active_decorations`,同类型覆盖写入,不同类型合并
- `$user->active_decorations = $merged; $user->save();`
4. 返回结果
**`getActiveDecorations()` 核心逻辑**
1. 读取 `$user->active_decorations`JSON → array
2. 遍历每个 slot,检查 `expires_at` 是否过期
3. 如果有过期项被清理,写回数据库
4. 返回干净的数组
### 3.4 ShopService 扩展
**文件**`app/Services/ShopService.php`
`buyItem()``match ($item->type)` 中新增三个分支:
```php
'msg_bubble' => $this->decorationService->purchase($user, $item),
'msg_name_color' => $this->decorationService->purchase($user, $item),
'avatar_frame' => $this->decorationService->purchase($user, $item),
```
注入 `DecorationService` 依赖。
### 3.5 ShopController 扩展
**文件**`app/Http/Controllers/ShopController.php`
`items()` 方法的返回数据中增加:
```php
'active_decorations' => $this->decorationService->getActiveDecorations($user),
```
### 3.6 消息广播增强
**文件**`app/Http/Controllers/ChatController.php`
`send()` 方法中,构造 `$messageData` 时增加装饰字段:
```php
$decorations = app(DecorationService::class)->getActiveDecorations($user);
// 在 $messageData 中追加
if (!empty($decorations['bubble'])) {
$messageData['msg_bubble'] = $decorations['bubble']['style'];
}
if (!empty($decorations['name_color'])) {
$messageData['msg_name_color'] = $decorations['name_color']['style'];
}
```
这样每条消息的 WebSocket 广播 payload 中就带上了装扮信息。
### 3.7 在线用户展示增强
**文件**`app/Services/ChatUserPresenceService.php`
`build()` 方法的 $payload 数组中增加:
```php
$decorations = app(DecorationService::class)->getActiveDecorations($user);
if (!empty($decorations['avatar_frame'])) {
$payload['avatar_frame'] = $decorations['avatar_frame']['style'];
}
if (!empty($decorations['name_color'])) {
$payload['name_color'] = $decorations['name_color']['style'];
}
```
### 3.8 Shop Seeder 扩展
**文件**`database/seeders/ShopItemSeeder.php`
新增装扮商品数据(sort_order 50-80 范围,排在改名卡之后)。
### 3.9 User Model
**文件**`app/Models/User.php`
`$fillable``$casts` 中增加:
```php
'active_decorations' => 'array', // 已在 $casts 中
```
---
## 四、前端改动清单
### 4.1 商店面板改造
**文件**`resources/js/chat-room/shop-controls.js`
1.`SHOP_GROUPS` 数组中新增三个分组:
```js
{ label: "💬 消息气泡", type: "msg_bubble" },
{ label: "🎨 昵称颜色", type: "msg_name_color" },
{ label: "🖼️ 头像框", type: "avatar_frame" },
```
2. 渲染装扮卡片时显示:
- 当前是否已激活同类装扮
- 剩余有效时间
- 购买/续费按钮
3. 购买成功后更新本地装扮状态,无需刷新整个列表。
**文件**`resources/views/chat/partials/shop-panel.blade.php`
可能需要微调样式以适配新卡片。
### 4.2 消息渲染改造
**文件**`resources/views/chat/partials/scripts.blade.php`
在 `appendMessage()` 函数中(约第 2105 行附近),根据消息 payload 中的装扮字段应用样式:
```javascript
// 在构建消息 HTML 时
let bubbleClass = '';
let nameColorStyle = '';
if (msg.msg_bubble) {
bubbleClass = `msg-bubble--${msg.msg_bubble}`;
}
if (msg.msg_name_color) {
nameColorStyle = `style="color: var(--name-${msg.msg_name_color})"`;
}
// 消息容器加上 bubbleClass
// 发送者名字处加上 nameColorStyle
```
**新增 CSS 样式**(可放在 `scripts.blade.php` 的 `<style>` 块中,或独立 CSS 文件):
```css
/* ── 消息气泡样式 ────────────────────── */
.msg-bubble--golden { border: 2px solid #fbbf24; box-shadow: 0 0 8px rgba(251,191,36,.4); }
.msg-bubble--sakura { border: 2px solid #f9a8d4; background: linear-gradient(135deg, #fce7f3, #fff1f2); }
.msg-bubble--star { border: 2px solid #6366f1; background: linear-gradient(135deg, #1e1b4b, #312e81); }
.msg-bubble--rainbow { border: 2px solid transparent; background-clip: padding-box;
border-image: linear-gradient(90deg, red, orange, yellow, green, blue, purple) 1; }
.msg-bubble--crown { border: 3px solid #fbbf24; box-shadow: 0 0 12px rgba(251,191,36,.6); }
/* ── 昵称颜色 ────────────────────────── */
.msg-name--golden { color: #fbbf24 !important; }
.msg-name--rainbow { background: linear-gradient(90deg, #ef4444, #f59e0b, #22c55e, #3b82f6, #a855f7);
-webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.msg-name--glow { color: #e2e8f0; text-shadow: 0 0 6px #6366f1, 0 0 12px #818cf8; }
.msg-name--flame { color: #f97316; text-shadow: 0 0 4px #ef4444; animation: name-flame 1.5s infinite; }
@keyframes name-flame {
0%, 100% { text-shadow: 0 0 4px #ef4444; }
50% { text-shadow: 0 0 8px #fbbf24, 0 0 12px #ef4444; }
}
```
### 4.3 用户列表渲染改造
**文件**`resources/views/chat/partials/scripts.blade.php`
在 `_renderUserListToContainer()` 函数中(约第 1777 行附近),为有头像框的用户增加装饰层:
```javascript
// 构建用户条目 HTML 时
let avatarFrameHtml = '';
if (user.avatar_frame) {
avatarFrameHtml = `<span class="avatar-frame avatar-frame--${user.avatar_frame}"></span>`;
}
// 将 avatarFrameHtml 放在头像 img 的外层
```
**新增 CSS**
```css
/* ── 头像框样式 ──────────────────────── */
.avatar-frame-wrapper { position: relative; display: inline-block; }
.avatar-frame {
position: absolute; top: -3px; left: -3px; right: -3px; bottom: -3px;
border-radius: 50%; pointer-events: none; z-index: 5;
}
.avatar-frame--silver { border: 2px solid #9ca3af; }
.avatar-frame--gold { border: 2px solid #fbbf24; box-shadow: 0 0 4px rgba(251,191,36,.5); }
.avatar-frame--star { border: 2px solid #fbbf24; animation: frame-spin 3s linear infinite; }
.avatar-frame--dragon { border: 2px solid #dc2626; box-shadow: 0 0 6px rgba(220,38,38,.6); }
@keyframes frame-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
```
### 4.4 移动端适配
**文件**`resources/js/chat-room/mobile-drawer.js`
移动端用户列表渲染调用 `window._renderUserListToContainer()`,头像框改动自动生效,无需额外修改。
---
## 五、任务拆分与实施顺序
| 序号 | 任务 | 涉及文件 | 预估工作量 |
|------|------|----------|-----------|
| 1 | 创建 `active_decorations` migration | 1 个新 migration 文件 | 0.5h |
| 2 | 新增 `CurrencySource` 枚举值 | `CurrencySource.php` | 0.2h |
| 3 | 创建 `DecorationService` | 1 个新 Service 文件 | 2h |
| 4 | 扩展 `ShopService::buyItem()` | `ShopService.php` | 0.5h |
| 5 | 扩展 `ShopController::items()` | `ShopController.php` | 0.3h |
| 6 | 增强消息广播 payload | `ChatController.php` | 0.5h |
| 7 | 增强在线用户 presence | `ChatUserPresenceService.php` | 0.3h |
| 8 | 扩展 Seeder 添加装扮商品 | `ShopItemSeeder.php` | 0.5h |
| 9 | 前端商店面板改造 | `shop-controls.js` + blade | 2h |
| 10 | 前端消息渲染改造 | `scripts.blade.php` | 3h |
| 11 | 前端用户列表头像框 | `scripts.blade.php` | 1.5h |
| 12 | CSS 样式编写 | `scripts.blade.php` 或独立 CSS | 1.5h |
| 13 | 联调测试 | — | 2h |
**建议分两轮交付**
**第一轮(任务 1-8,后端先行)**:后端全部完成,可通过 API / 数据库直接验证购买、扣金币、流水记录、过期清理逻辑。
**第二轮(任务 9-13,前端展示)**:前端商店界面、消息渲染、用户列表展示。
---
## 六、价格平衡分析
以活跃用户的日均金币收入(心跳 + 签到 + 视频 ≈ 3000-15000 金币/天)为基准:
| 装扮类型 | 最低价 | 日均摊成本 | 占日收入比例 |
|----------|--------|-----------|-------------|
| 金色气泡(1天) | 300 | 300/天 | 10%-20%(轻度用户) |
| 樱花气泡(3天) | 500 | 167/天 | 5%-10% |
| 彩虹气泡(7天) | 1500 | 214/天 | 7%-15% |
| 金色昵称(1天) | 200 | 200/天 | 7%-13% |
| 发光昵称(7天) | 800 | 114/天 | 4%-8% |
| 银色头像框(7天) | 500 | 71/天 | 2%-5% |
| 龙纹头像框(30天) | 5000 | 167/天 | 5%-10% |
如果同时购买气泡+昵称+头像框(全部最低档):300 + 200 + 71 ≈ 571 金币/天,约占轻度用户日收入的 20-40%,**合理且有适度压力**。
---
## 七、后续扩展预留
当前设计中已预留扩展空间:
1. **装扮互斥覆盖**`DecorationService::purchase()` 中同类型新购买会自动覆盖旧的(旧装扮不退款),这是有意为之的消耗设计。
2. **新装扮类型**:只需在 `DecorationService` 中增加新的 slot key(如 `text_effect`、`enter_effect`),无需改数据库。
3. **稀有/限定装扮**:可在 seeder 中添加 `is_active=false` 的商品,通过活动/任务发放。
4. **装扮赠送**:未来可在 `ShopController::buy()` 中扩展 recipient 参数,允许购买装扮送给他人(复用现有礼物赠送的消息广播模式)。
---
## 八、验收标准
1. ✅ 用户可在商店面板看到装扮分类和商品列表
2. ✅ 购买装扮扣金币正确,流水记录 source 为 `msg_decoration_buy` / `avatar_frame_buy`
3. ✅ 购买成功后 `users.active_decorations` JSON 字段正确更新
4. ✅ 同类型新装扮覆盖旧装扮
5. ✅ 过期装扮在下次读取时自动清理
6. ✅ 消息广播 payload 包含 `msg_bubble` / `msg_name_color` 字段
7. ✅ 消息气泡正确渲染对应样式
8. ✅ 昵称颜色正确渲染
9. ✅ 用户列表头像正确渲染头像框
10. ✅ 移动端展示正常
11. ✅ 金币不足时提示错误,不会扣成负数
php artisan migrate # 添加 active_decorations 列
php artisan db:seed --class=ShopItemSeeder # 导入装扮商品数据
npm run build # 重新编译前端资源