# 第一阶段：消息装扮 & 头像框消费系统 — 实施方案

> 目标：为聊天室新增持续性的金币消费出口，通过「消息装扮」和「头像框」两个高社交可见度的功能，让金币有处可花、花得有面子。

---

## 一、功能范围

### 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                           # 重新编译前端资源
