feat: 付费体系重构 P0 - 配额独立化/简历付费下载/PDF生成

This commit is contained in:
yuzhiran
2026-06-12 09:31:11 +08:00
parent 5d407b4f79
commit 065fe7a186
23 changed files with 965 additions and 106 deletions
+299
View File
@@ -0,0 +1,299 @@
# 产品改造方案:付费体系重构 + 数字人面试
> 2026-06-11 · v1.0
## 概述
将现有一刀切的 `remaining` 免费额度模式,改为**按产品线独立计费 + 会员套餐含额度 + 数据库配置化**的灵活付费体系。
## 数据库模型变更
### User Schema 新增字段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `interviewCredits` | number | 1 | AI 数字人面试可用次数(含首次免费) |
| `resumeOptimizeCredits` | number | 0 | 简历优化可用次数 |
| `resumeDownloadCredits` | number | 0 | 简历下载可用次数 |
| `freeOptimizeUsed` | number | 0 | 已使用免费优化次数(上限 3) |
旧的 `remaining` 字段保留,仅限老用户兼容,新逻辑不再使用。
### Resume Schema 新增字段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `version` | number | 1 | 版本号,每次 AI 改写 +1 |
| `contentHash` | string | '' | 内容 MD5,用于判断是否改版 |
| `paidDownload` | boolean | false | 是否已购买该版本的下载权 |
`contentHash` 在 create 和 optimize 后自动更新。
### PaymentOrder Schema 新增字段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `type` | string | 'membership' | `membership` / `interview` / `optimize` / `download` |
| `metadata` | Mixed | {} | 产品粒度信息(如 `{ resumeId, version }` |
### 定价配置(SiteConfig key=`pricing`
单一配置源,所有定价和套餐内容后台可编辑:
```json
{
"interview": {
"firstFree": true,
"pricePerSession": 500,
"creditsPerPurchase": 1
},
"resumeOptimize": {
"freeLimit": 3,
"pricePerOptimize": 300,
"creditsPerPurchase": 1
},
"resumeDownload": {
"pricePerDownload": 200,
"creditsPerPurchase": 1
},
"plans": {
"growth": {
"price": 1990,
"durationDays": 30,
"credits": {
"interview": 999,
"resumeOptimize": 20,
"resumeDownload": 10
},
"features": [
"免费版全部权益",
"AI 数字人面试无限次",
"详细面试报告(四维评分)",
"进步轨迹雷达图 + 打卡",
"每日一题推送",
"参考回答思路",
"公司真题库"
]
},
"sprint": {
"price": 4990,
"durationDays": 30,
"credits": {
"interview": 999,
"resumeOptimize": 50,
"resumeDownload": 30
},
"features": [
"成长版全部权益",
"AI 语音分析(语气词/语速检测)",
"技能缺口分析报告",
"学习路径推荐",
"真人导师 1v1 点评(每月 1 次)",
"简历精修(每月 1 次)",
"内推优先"
]
}
}
}
```
## 配额检查逻辑
所有配额检查收敛到统一的 `QuotaGuard``QuotaService`,不再散落在各处。
```typescript
// 配额服务
class QuotaService {
checkInterview(user): // user.interviewCredits >= 1
checkOptimize(user): // user.resumeOptimizeCredits > 0
// || user.freeOptimizeUsed < freeLimit
checkDownload(user, resume): // resume.paidDownload
// || user.resumeDownloadCredits > 0
deductInterview(user)
deductOptimize(user)
deductDownload(user, resume)
}
```
### 各类用户行为对应的检查
| 行为 | 检查条件 | 扣减项 |
|------|----------|--------|
| 开始 AI 面试 | `interviewCredits >= 1` | `interviewCredits -= 1` |
| AI 诊断/优化简历 | `resumeOptimizeCredits > 0 \|\| freeOptimizeUsed < 3` | 付费优先扣,免费次之 |
| 下载简历 PDF | `resume.paidDownload \|\| resumeDownloadCredits > 0` | 标记 resume.paidDownload |
## 支付流程扩展
### 现有:套餐购买(不变)
```
POST /payment/create { plan: 'growth' | 'sprint' }
→ 创建 Membership 订单(type='membership'
→ WeChat Pay
→ 回调/activate → 注入 plan 对应的全部 credits
```
### 新增:按次购买
```
POST /payment/create-product { type, resumeId?, count? }
→ 从 pricing config 读取单价
→ 创建对应 type 订单
→ WeChat Pay
→ 回调 → 根据 type 增加对应用户 credits / 标记下载
```
支持的产品:
- `interview`:增加 `interviewCredits`
- `optimize`:增加 `resumeOptimizeCredits`
- `download`:标记 `resume.paidDownload=true`
## 数字人面试架构
### 整体流程
```
用户选择岗位 → 配额检查 → AI 出题 → 回答
→ AI 评分+下一题 → ... → 完成 → 报告 → 头像回顾视频(可选)
```
每次 AI 回复时,同步调用 TTS 生成音频,返回给前端:
```typescript
// 面试回答响应
{
text: "请介绍一下你的项目经验",
audioUrl: "/tts/cache/abc123.mp3", // edge-tts 生成
durationMs: 2800, // 音频时长
animationTimings: [], // 可选:逐字时间戳
}
```
### TTS 服务
`backend/src/modules/tts/tts.service.ts`
```typescript
class TtsService {
async synthesize(text: string): Promise<{ audioPath: string; durationMs: number }> {
// 1. 按 text MD5 检查缓存
// 2. 未命中 → child_process.exec('edge-tts ...')
// 3. 保存到 /tmp/tts-cache/ 或 certs 同目录
// 4. 返回路径和时长
}
}
```
edge-tts 命令示例:
```bash
edge-tts --voice zh-CN-XiaoxiaoNeural --text "你好" --write-media /tmp/tts-cache/abc.mp3
```
### 前端数字人组件
`zhiyin-app/src/components/digital-human.vue`
```
┌─────────────────────┐
│ ┌───────┐ │
│ │ 头像 │ │ ← 静态 PNG + CSS呼吸/眨眼
│ │ (嘴型) │ │ ← Canvas 覆盖嘴部区域
│ └───────┘ │
│ "请介绍你的项目" │ ← 字幕气泡
│ │
│ [▼ 音频波形] │ ← 可选
└─────────────────────┘
```
嘴型驱动:
1. `AudioContext.createMediaElementSource(audioEl)``AnalyserNode`
2. 每帧读取 `analyser.getByteTimeDomainData(dataArray)`
3. 计算 RMS → 映射到嘴型开合度 (0~1)
4. Canvas 绘制椭圆(开)或线(闭)
### 新接口
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/interview/create-avatar` | 创建数字人面试(含配额检查) |
| POST | `/interview/:id/answer-avatar` | 回答+AI 回复(含 TTS audioUrl |
| GET | `/tts/:hash` | 获取 TTS 音频文件 |
## 简历下载流程图
```
用户查看优化结果
├─ 点击「下载 PDF」
│ │
│ ├─ resume.paidDownload === true → 直接生成 → 返回 PDF
│ │
│ └─ resume.paidDownload === false
│ │
│ ├─ user.resumeDownloadCredits > 0
│ │ → 扣 1 credit → 标记 paidDownload → 生成 PDF
│ │
│ └─ 无 credits
│ → 弹出支付窗口(¥X/次)
│ → WeChat Pay → 标记 paidDownload → 生成 PDF
└─ 再次优化(内容改变)
→ contentHash 变化 → 新 resume 记录(version++
→ paidDownload = false
→ 需重新付费下载
```
Puppeteer PDF 生成 (`resume-pdf.service.ts`)
1. 读取 resume.contentmarkdown/HTML
2. 套用模板(含姓名、岗位、日期等)
3. `puppeteer.launch()``page.setContent(html)``page.pdf({ format: 'A4' })`
4. 返回 PDF buffer
5. 可选:同时生成 Word(用 HTML → mammoth 或 libreoffice
## 管理后台配置
在现有 `admin.vue` 的 Config Tab 中增加:
```
┌─────────────────────────────────────────────┐
│ 定价配置 [保存] │
│ │
│ AI 面试单价 ¥ [ 5 ] /次 │
│ 简历优化单价 ¥ [ 3 ] /次 │
│ 简历下载单价 ¥ [ 2 ] /次 │
│ 免费优化次数 [ 3 ] 次 │
│ 首次面试免费 [✓] │
│ │
│ ─── 成长版 ─── │
│ 价格 ¥ [ 19.9 ] /月 │
│ 面试额度 [ 999 ] 次 │
│ 优化额度 [ 20 ] 次 │
│ 下载额度 [ 10 ] 次 │
│ 功能列表 [编辑] │
│ │
│ ─── 冲刺版 ─── │
│ ... │
└─────────────────────────────────────────────┘
```
## 实施优先级
| 阶段 | 内容 | 文件涉及 |
|------|------|----------|
| P0-1 | 定价配置化 + User 新字段 + 统一配额检查 | user.schema, SiteConfig, QuotaService |
| P0-2 | 简历下载付费 + Puppeteer PDF | resume.schema, resume.service, resume.controller, ResumePdfService |
| P0-3 | 支付扩展(按次购买) | payment-order.schema, payment.controller |
| P1-1 | TTS 服务 | tts.service, tts.controller, tts.module |
| P1-2 | 数字人面试前端组件 | digital-human.vue, interview.vue |
| P1-3 | 面试接口改版 + 配额接入 | interview.service, interview.controller |
| P2 | Admin 定价管理界面 | admin.vue, admin.controller |
## 兼容性注意事项
- `remaining` 字段保留,老用户数据不受影响
- 新用户注册默认 `interviewCredits=1`(首次免费)
- 会员激活时注入 plan 定义的 credits
- 过期降级时不清零已购买的 credits,仅阻止会员专属 privileges
- 已购买的单次 credits 不过期