# 运营功能开发文档

> 关联文档：[运营方案](./运营方案.md)  
> 制定日期：2026-05-21  
> 当前代码分析日期：2026-05-21

---

## 一、开发任务总览

| 编号 | 任务 | 优先级 | 工时 | 依赖 | 涉及模块 |
|------|------|--------|------|------|---------|
| DEV-01 | 接入百度统计SDK | 🔴 P0 | 0.5天 | 无 | 前端 only |
| DEV-03 | 分享功能完善 | 🔴 P0 | 2天 | DEV-04 | Share + Player |
| DEV-04 | 邀请奖励机制 | 🔴 P0 | 2天 | DEV-03 | Share + Subscription |
| DEV-08 | 分享卡片美化 | 🟡 P1 | 0.5天 | DEV-03 | Share + UI |

---

## 二、DEV-01：接入百度统计SDK

### 2.1 目标
前端页面加入用户行为统计，能跟踪：PV/UV、页面停留时长、按钮点击事件、用户来源渠道。

### 2.2 技术方案

选择**百度统计**，原因：
- 免费额度够用（日均PV < 10万免费）
- 支持微信小程序 SDK
- 接入成本最低，一行代码初始化

### 2.3 具体实现

#### 2.3.1 安装与配置

```bash
cd my-uniapp-vue3
npm install @baidu/stat-mp --save  # 百度统计小程序SDK
```

#### 2.3.2 新建统计工具文件

**文件：`my-uniapp-vue3/src/utils/analytics.ts`**（新建）

```typescript
/**
 * 百度统计分析工具
 * 使用前需在百度统计后台创建应用，获取 appKey
 */

// 百度统计小程序 key（需在百度统计后台申请）
const BAIDU_APP_KEY = ''; // TODO: 替换为实际key

// H5 环境使用传统百度统计
const H5_BAIDU_ID = ''; // TODO: 替换为实际id

/**
 * 初始化统计
 */
export function initAnalytics() {
  // #ifdef MP-WEIXIN
  if (BAIDU_APP_KEY) {
    // 微信小程序接入百度统计
    const mtj = require('@baidu/stat-mp');
    mtj.default.init(BAIDU_APP_KEY, {
      autoTrack: true, // 自动采集页面浏览
    });
  }
  // #endif

  // #ifdef H5
  if (H5_BAIDU_ID) {
    // H5 接入百度统计
    const script = document.createElement('script');
    script.src = `https://hm.baidu.com/hm.js?${H5_BAIDU_ID}`;
    document.head.appendChild(script);
  }
  // #endif
}

/**
 * 自定义事件上报
 * @param eventName 事件名称
 * @param params 事件参数
 */
export function trackEvent(eventName: string, params?: Record<string, any>) {
  // #ifdef MP-WEIXIN
  try {
    const mtj = require('@baidu/stat-mp');
    mtj.default.event(eventName, params || {});
  } catch (e) {
    // 静默失败
  }
  // #endif

  // #ifdef H5
  try {
    if ((window as any)._hmt) {
      (window as any)._hmt.push(['_trackEvent', eventName, 'click', JSON.stringify(params || {})]);
    }
  } catch (e) {
    // 静默失败
  }
  // #endif
}

/**
 * 预定义事件常量（业务事件）
 */
export const AnalyticsEvents = {
  // 用户行为
  USER_REGISTER: 'user_register',
  USER_LOGIN: 'user_login',
  
  // TTS 生成
  TTS_GENERATE_START: 'tts_generate_start',
  TTS_GENERATE_SUCCESS: 'tts_generate_success',
  TTS_GENERATE_FAIL: 'tts_generate_fail',
  
  // 播放器
  PLAYER_PLAY: 'player_play',
  PLAYER_COMPLETE: 'player_complete',
  PLAYER_FAVORITE: 'player_favorite',
  PLAYER_SHARE: 'player_share',
  
  // 付费转化
  MEMBER_PAGE_VIEW: 'member_page_view',
  MEMBER_CLICK_BUY: 'member_click_buy',
  MEMBER_PAY_START: 'member_pay_start',
  MEMBER_PAY_SUCCESS: 'member_pay_success',
  
  // 签到
  SIGN_IN: 'sign_in',
  
  // 分享裂变
  SHARE_CLICK: 'share_click',
  SHARE_COMPLETE: 'share_complete',
  INVITE_ACCEPT: 'invite_accept',
} as const;

export default { init: initAnalytics, trackEvent, AnalyticsEvents };
```

#### 2.3.3 在 App.vue 中初始化

**文件：`my-uniapp-vue3/src/App.vue`**

在 `<script setup>` 的 `onLaunch` 中追加：

```typescript
import { initAnalytics } from '@/utils/analytics';

onLaunch(() => {
  // ... 现有代码 ...
  userStore.initUser();
  applyDarkMode();
  
  // 新增：初始化统计
  initAnalytics();
});
```

#### 2.3.4 关键页面埋点

在以下位置添加 `trackEvent` 调用：

| 位置 | 事件 | 代码位置 |
|------|------|---------|
| 注册成功后 | `USER_REGISTER` | `store/user.ts` login 方法 |
| 点击生成按钮 | `TTS_GENERATE_START` | `pages/create/index.vue` |
| 生成成功后 | `TTS_GENERATE_SUCCESS` | TTS回调处 |
| 播放音频 | `PLAYER_PLAY` | `pages/player/index.vue` onPlay |
| 点击付费按钮 | `MEMBER_CLICK_BUY` | `pages/member/index.vue` |

### 2.4 测试验证
- [ ] 微信小程序控制台确认 SDK 初始化成功
- [ ] 百度统计后台实时访客能看到自己
- [ ] 自定义事件能正常上报

### 2.5 百度统计后台配置
1. 访问 https://tongji.baidu.com 注册账号
2. 创建「应用」→ 选择「微信小程序」
3. 获取 AppKey，填入 `analytics.ts`

---

## 三、DEV-03：分享功能完善

### 3.1 目标
将目前的"纯前端复制链接"改造为完整的分享追踪+奖励体系。

### 3.2 当前状态分析

```
现状：
  后端：
    ✅ GET  /api/share/card/:audioId  — 生成分享卡片数据（标题、描述、封面）
    ✅ GET  /api/share/qrcode/:audioId — 生成二维码数据
    ✅ POST /api/share/track           — 「占位」console.log，无实际数据库写入
    ❌ 无 ShareRecord 数据表
    ❌ 无分享奖励逻辑

  前端（player/index.vue）：
    ✅ handleShare() — H5用navigator.share，小程序复制链接
    ❌ 不调用 /api/share/track
    ❌ 不调用 /api/share/card 获取卡片数据
    ❌ 无邀请码参数
```

### 3.3 技术方案

#### 3.3.1 数据库变更

在 `server/prisma/schema.prisma` 中新增 `ShareRecord` 模型：

```prisma
model ShareRecord {
  id         Int      @id @default(autoincrement())
  userId     Int
  audioId    Int
  platform   String   @default("wechat")  // wechat, moments, copy_link
  shareCode  String   @unique             // 分享码，用于追踪
  createdAt  DateTime @default(now())

  @@index([userId])
  @@index([shareCode])
  @@index([audioId])
  @@map("share_records")
}
```

执行迁移：
```bash
cd server
npx prisma db push
```

#### 3.3.2 后端改造

**文件：`server/src/modules/share/share.service.ts`**（重写）

```typescript
import { prisma } from '../../models';
import crypto from 'crypto';

interface ShareCardData {
  title: string;
  description: string;
  coverUrl: string;
  shareUrl: string;
  shareCode: string;
}

/**
 * 生成分享卡片（增强版：含分享码）
 */
export async function generateShareCard(chapterId: number, userId?: number): Promise<ShareCardData> {
  const chapter = await prisma.bookChapter.findUnique({
    where: { id: chapterId },
    include: { book: true },
  });

  if (!chapter) {
    throw new Error(`章节 ${chapterId} 不存在`);
  }

  // 生成唯一分享码（8位）
  const shareCode = generateShareCode();

  const shareUrl = `${getBaseUrl()}/#/pages/player/index?id=${chapterId}&sc=${shareCode}`;

  return {
    title: chapter.title || chapter.book?.title || '声工坊有声作品',
    description: `听听这篇《${chapter.book?.title || '有声作品'}》，用AI朗读超自然！`,
    coverUrl: chapter.book?.coverUrl || '',
    shareUrl,
    shareCode,
  };
}

/**
 * 记录分享行为
 */
export async function trackShare(
  userId: number,
  audioId: number,
  platform: string,
  shareCode?: string
) {
  const code = shareCode || generateShareCode();

  const record = await prisma.shareRecord.create({
    data: {
      userId,
      audioId,
      platform,
      shareCode: code,
    },
  });

  console.log(`📤 用户 ${userId} 分享了章节 ${audioId} 到 ${platform}，分享码: ${code}`);
  return record;
}

/**
 * 通过分享码追踪访问来源
 */
export async function trackShareVisit(shareCode: string, visitorId?: number) {
  const shareRecord = await prisma.shareRecord.findUnique({
    where: { shareCode },
    include: {
      user: { select: { id: true, nickname: true } },
    },
  });

  if (shareRecord) {
    console.log(`👀 分享码 ${shareCode} 被访问，分享者: ${shareRecord.userId}，访客: ${visitorId || '匿名'}`);
  }

  return shareRecord;
}

/**
 * 生成8位分享码
 */
function generateShareCode(): string {
  return crypto.randomBytes(4).toString('hex').toUpperCase();
}

/**
 * 获取基础URL
 */
function getBaseUrl(): string {
  return process.env.BASE_URL || 'https://book.rrbrr.com';
}
```

**文件：`server/src/modules/share/share.controller.ts`**（修改）

新增一个端点：

```typescript
/**
 * GET /api/share/visit?code=xxx
 * 通过分享码追踪访问，供前端页面加载时调用
 */
router.get('/visit', async (ctx) => {
  const { code } = ctx.query;
  if (!code) {
    ctx.status = 400;
    ctx.body = { code: -1, message: '缺少分享码参数' };
    return;
  }
  
  const record = await shareService.trackShareVisit(code as string);
  ctx.body = {
    code: 0,
    data: {
      valid: !!record,
      sharer: record ? { id: record.userId, nickname: record.user?.nickname } : null,
    },
  };
});
```

#### 3.3.3 前端改造

**文件：`my-uniapp-vue3/src/pages/player/index.vue`**

修改 `handleShare()` 和新增逻辑：

```typescript
import { get, post } from '@/utils/request';
import { trackEvent, AnalyticsEvents } from '@/utils/analytics';

// 分享数据
const shareData = ref<ShareCardData | null>(null);

/**
 * 获取分享卡片数据（从后端）
 */
async function fetchShareCard() {
  if (!audio.value?._id) return;
  try {
    const res = await get<ShareCardData>(`/share/card/${audio.value._id}`);
    shareData.value = res;
  } catch (e) {
    console.error('获取分享卡片失败', e);
  }
}

/**
 * 处理分享（增强版）
 */
async function handleShare() {
  trackEvent(AnalyticsEvents.SHARE_CLICK, { audioId: audio.value?._id });
  
  // 先获取最新的分享卡片数据
  await fetchShareCard();
  
  const shareTitle = shareData.value?.title || '声工坊有声作品';
  const shareDesc = shareData.value?.description || '用AI生成的高品质有声书，快来听！';
  const shareUrl = shareData.value?.shareUrl || copyShareLink();
  
  // #ifdef H5
  if (navigator.share) {
    try {
      await navigator.share({
        title: shareTitle,
        text: shareDesc,
        url: shareUrl,
      });
      await trackShareComplete('h5_share_api');
    } catch (e) {
      // 用户取消分享
      if ((e as Error).name !== 'AbortError') {
        await copyToClipboard(shareUrl);
      }
    }
    return;
  }
  // #endif
  
  // 小程序/App：复制链接
  await copyToClipboard(shareUrl);
}

/**
 * 向后端上报分享完成
 */
async function trackShareComplete(platform: string) {
  try {
    await post('/share/track', {
      audioId: audio.value?._id,
      platform,
      shareCode: shareData.value?.shareCode,
    });
    trackEvent(AnalyticsEvents.SHARE_COMPLETE, {
      audioId: audio.value?._id,
      platform,
    });
  } catch (e) {
    // 静默失败
  }
}

/**
 * 复制到剪贴板（带分享码链接）
 */
async function copyToClipboard(url: string) {
  try {
    await uni.setClipboardData({ data: url });
    uni.showToast({ title: '链接已复制，分享给朋友吧！', icon: 'success' });
    await trackShareComplete('copy_link');
  } catch (e) {
    uni.showToast({ title: '请截图分享', icon: 'none' });
  }
}

// 页面加载时，检测分享码来源
onLoad((options) => {
  const shareCode = options?.sc;
  if (shareCode) {
    // 上报分享访问
    get(`/share/visit?code=${shareCode}`).catch(() => {});
    trackEvent(AnalyticsEvents.INVITE_ACCEPT, { shareCode });
    // 存储分享码，用于后续注册绑定
    uni.setStorageSync('invite_code', shareCode);
  }
});
```

**文件：`my-uniapp-vue3/src/store/user.ts`**

注册时携带邀请码：

```typescript
async function login(phone: string, code: string) {
  const inviteCode = uni.getStorageSync('invite_code') || '';
  
  const res = await post('/auth/login', {
    phone,
    code,
    inviteCode,  // 新增：邀请码
  });
  
  // ... 原有逻辑 ...
  uni.removeStorageSync('invite_code'); // 清除已使用的邀请码
}
```

### 3.4 文件变更清单

| 操作 | 文件 | 说明 |
|------|------|------|
| 修改 | `server/prisma/schema.prisma` | 新增 ShareRecord 模型 |
| 重写 | `server/src/modules/share/share.service.ts` | 完整分享追踪逻辑 |
| 修改 | `server/src/modules/share/share.controller.ts` | 新增 /visit 端点 |
| 修改 | `my-uniapp-vue3/src/pages/player/index.vue` | 重写 handleShare |
| 修改 | `my-uniapp-vue3/src/store/user.ts` | login 携带邀请码 |
| 修改 | `my-uniapp-vue3/src/utils/analytics.ts` | 添加分享相关事件 |

### 3.5 测试验证

```bash
# 1. 获取分享卡片
curl "http://localhost:3000/api/share/card/1"
# 预期：返回 { title, description, coverUrl, shareUrl, shareCode }

# 2. 记录分享
curl -X POST http://localhost:3000/api/share/track \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{"audioId":1,"platform":"copy_link","shareCode":"AB12CD34"}'
# 预期：返回 ShareRecord 记录

# 3. 追踪访问
curl "http://localhost:3000/api/share/visit?code=AB12CD34"
# 预期：返回 { valid: true, sharer: {...} }

# 4. 前端 Playwright 测试
# - 打开播放器页面
# - 点击分享按钮
# - H5: 触发 navigator.share / 小程序: 复制链接
# - 验证链接中包含 ?sc= 参数
```

---

## 四、DEV-04：邀请奖励机制

### 4.1 目标
用户通过分享链接邀请新用户注册，双方各获得奖励（免费生成时长）。

### 4.2 技术方案

#### 4.2.1 数据库变更

在 `server/prisma/schema.prisma` 中新增 `InviteRecord` 模型：

```prisma
model InviteRecord {
  id         Int      @id @default(autoincrement())
  inviterId  Int      // 邀请人
  inviteeId  Int      @unique  // 被邀请人（一个用户只能被邀请一次）
  shareCode  String?           // 关联的分享码
  rewarded   Boolean  @default(false) // 是否已发放奖励
  createdAt  DateTime @default(now())

  @@index([inviterId])
  @@map("invite_records")
}
```

#### 4.2.2 后端改造

**文件（新建）：`server/src/modules/invite/invite.service.ts`**

```typescript
import { prisma } from '../../models';

const INVITE_REWARD = {
  inviterBonus: 10 * 60,  // 邀请人获得10分钟（600秒）
  inviteeBonus: 10 * 60,  // 被邀请人获得10分钟
};

/**
 * 处理邀请：被邀请人注册时调用
 */
export async function handleInvite(inviterId: number, inviteeId: number, shareCode?: string) {
  // 防止自邀请
  if (inviterId === inviteeId) return;

  // 防止重复被邀请
  const existing = await prisma.inviteRecord.findUnique({
    where: { inviteeId },
  });
  if (existing) return;

  // 创建邀请记录
  await prisma.inviteRecord.create({
    data: {
      inviterId,
      inviteeId,
      shareCode,
      rewarded: false,
    },
  });

  // 发放邀请人奖励
  await grantInviteReward(inviterId, INVITE_REWARD.inviterBonus);
  
  // 发放被邀请人奖励
  await grantInviteReward(inviteeId, INVITE_REWARD.inviteeBonus);

  // 标记奖励已发放
  await prisma.inviteRecord.updateMany({
    where: { inviteeId },
    data: { rewarded: true },
  });

  console.log(`🎁 邀请奖励发放: ${inviterId} → ${inviteeId}，各得${INVITE_REWARD.inviterBonus}秒`);
}

/**
 * 发放奖励（增加Token余额或costLimit）
 */
async function grantInviteReward(userId: number, seconds: number) {
  // 转换为分钟额度
  const minutes = Math.ceil(seconds / 60);
  
  // 增加 costLimit（月度费用上限）
  await prisma.user.update({
    where: { id: userId },
    data: {
      costLimit: { increment: minutes * 0.032 }, // 按入门版单价
    },
  });

  // 也增加 Token 余额
  const tokens = minutes * 1000; // 分钟→token换算
  await prisma.tokenBalance.upsert({
    where: { userId },
    create: {
      userId,
      totalTokens: tokens,
      usedTokens: 0,
    },
    update: {
      totalTokens: { increment: tokens },
    },
  });
}

/**
 * 获取用户的邀请统计
 */
export async function getInviteStats(userId: number) {
  const count = await prisma.inviteRecord.count({
    where: { inviterId: userId, rewarded: true },
  });

  return {
    totalInvites: count,
    totalReward: count * INVITE_REWARD.inviterBonus, // 秒
  };
}
```

#### 4.2.3 对接注册流程

**文件：`server/src/modules/auth/auth.service.ts`**

在 `loginWithPhone` 的注册环节追加邀请处理：

```typescript
// 在创建用户之后的代码中
if (isNewUser) {
  // 处理邀请
  const { inviteCode } = params;
  if (inviteCode) {
    // 查找邀请人
    const shareRecord = await prisma.shareRecord.findUnique({
      where: { shareCode: inviteCode },
    });
    if (shareRecord) {
      await handleInvite(shareRecord.userId, user.id, inviteCode);
    }
  }
}
```

#### 4.2.4 前端邀请统计展示

**文件：`my-uniapp-vue3/src/pages/mine/index.vue`**

新增邀请卡片入口：

```vue
<!-- 在功能入口菜单中新增 -->
<view class="menu-item" @click="goInvite">
  <text class="menu-icon">🎁</text>
  <text class="menu-label">邀请好友 · 双方得10分钟</text>
  <text class="menu-arrow">→</text>
</view>
```

### 4.3 文件变更清单

| 操作 | 文件 | 说明 |
|------|------|------|
| 修改 | `server/prisma/schema.prisma` | 新增 InviteRecord 模型 |
| 新建 | `server/src/modules/invite/invite.service.ts` | 邀请逻辑 |
| 新建 | `server/src/modules/invite/invite.controller.ts` | 邀请相关API |
| 修改 | `server/src/modules/auth/auth.service.ts` | 注册时绑定邀请关系 |
| 修改 | `server/src/app.ts` | 注册 invite 路由 |
| 修改 | `my-uniapp-vue3/src/pages/mine/index.vue` | 邀请入口 |

---

## 五、DEV-08：分享卡片美化

### 5.1 目标
优化分享卡片的视觉呈现，提升点击转化率。

### 5.2 技术方案

**文件：`server/src/modules/share/share.service.ts`**

优化 `generateShareCard` 返回的标题和描述，根据内容自动生成更有吸引力的文案：

```typescript
function generateShareTitle(chapter: any): string {
  const bookTitle = chapter.book?.title || '';
  const chapterTitle = chapter.title || '';
  
  if (bookTitle && chapterTitle) {
    return `🎧 《${bookTitle}》${chapterTitle}`;
  }
  if (bookTitle) {
    return `🎧 AI朗读《${bookTitle}》，超自然！`;
  }
  return '🎧 用AI生成的超自然有声书';
}

function generateShareDescription(chapter: any): string {
  const duration = chapter.audioDuration 
    ? `${Math.round(chapter.audioDuration / 60)}分钟` 
    : '几分钟';
  
  const templates = [
    `AI朗读效果太逼真了！${duration}的有声书，快来听听～`,
    `不用自己读，AI帮你把文字变成${duration}音频，效果惊人！`,
    `在声工坊用AI生成的${duration}有声书，分享给你！`,
  ];
  
  return templates[Math.floor(Math.random() * templates.length)];
}
```

**文件：`my-uniapp-vue3/src/pages/player/index.vue`**

分享按钮增加动画效果和引导文案：

```vue
<view class="control-btn share-btn" @click="handleShare">
  <text class="control-icon">📤</text>
  <text class="share-label">分享得时长</text>
</view>
```

---

## 六、实施计划

### 建议执行顺序

```
第一阶段（本周）P0任务 — 上线必备
┌─────────────────────────────────────┐
│ DEV-01 百度统计 (0.5天)             │ → 先接入数据，后续所有动作都能追踪
│ DEV-03 分享完善 (2天)               │ → 裂变增长的基础设施
└─────────────────────────────────────┘
           ↓
第二阶段（下周）P0任务 — 增长引擎
┌─────────────────────────────────────┐
│ DEV-04 邀请奖励 (2天)               │ → 基于DEV-03的分享码体系
└─────────────────────────────────────┘
           ↓
第三阶段（第3周）P1任务 — 体验优化
┌─────────────────────────────────────┐
│ DEV-08 分享美化 (0.5天)             │ → 锦上添花
└─────────────────────────────────────┘
```

### 总工时估算

| 阶段 | 任务 | 工时 |
|------|------|------|
| 第一阶段 | DEV-01, 03 | 2.5天 |
| 第二阶段 | DEV-04 | 2天 |
| 第三阶段 | DEV-08 | 0.5天 |
| **合计** | **4个任务** | **5天** |

---

## 七、注意事项

1. **数据统计SDK优先**：一定要在推广前接入，否则无法量化效果
2. **支付先测通**：虽然不在本文档范围内，但上线前必须确保微信支付/支付宝能正常回调
3. **小程序审核**：新增页面可能需要重新提审，预留3-5个工作日
4. **数据库迁移**：每次修改 `schema.prisma` 后执行 `npx prisma db push`
5. **分享码8位随机**：碰撞概率极低（16^8 ≈ 43亿），无需担心重复

---

**文档版本**：V1.0  
**关联文档**：[运营方案](./运营方案.md)
