# 职引 (ZhiYin) — AGENTS.md

> AI 模拟面试教练，专注校招。NestJS + uni-app(Vue3) + MongoDB。

---

## 一、项目结构

```
zhiyin/
├── backend/                     # NestJS 10.x 后端 (端口 3006, 前缀 /api)
│   └── src/
│       ├── main.ts              # 入口：DOMMatrix polyfill → NestFactory → CORS → ValidationPipe
│       ├── app.module.ts        # 根模块：导入全部子模块 + JWT/Throttler/Mongoose
│       ├── common/
│       │   ├── guards/          # JwtAuthGuard (全局), admin.guard.ts
│       │   ├── strategies/      # JwtStrategy
│       │   ├── decorators/      # @CurrentUser, @Public()
│       │   └── filters/         # AllExceptionsFilter
│       └── modules/             # 20 个模块（详见下文）
├── zhiyin-app/                  # uni-app 3.x 前端 (H5 + 微信小程序)
│   └── src/
│       ├── pages/               # 20 个页面 (pages.json 路由)
│       ├── composables/         # 可复用组合式函数（如 useGravityPurchase）
│       ├── services/api.ts      # API 调用封装 (uni.request)
│       ├── config.ts            # 端点定义 + api() 辅助函数
│       └── App.vue              # 设计 Token + 全局样式 + H5 URL 参数处理
└── docs/                        # 产品/架构/部署/路线图文档
```

### 后端模块清单

| 模块 | 职责 |
|------|------|
| `user` | 手机/邮箱/密码/微信登录, JWT, 配额 |
| `interview` | AI 面试核心（多轮对话 + 评分 + 报告 + 进度） |
| `ai` | AI 调用封装（deepseek-v4-flash 主 + step-3.5-flash 备） |
| `analyze` | 简历诊断 / 优化 / 技能缺口分析 |
| `resume` | 简历 CRUD |
| `member` | 会员套餐 / 权益扣减 |
| `payment` | 微信支付 v3（Native + JSAPI + 回调） |
| `progress` | 进步轨迹雷达图 / 打卡日历 / 行业基准 / 岗位匹配 |
| `contribution` | 面经贡献 + 公司题库（数据飞轮核心） |
| `schedule` | 定时任务：VIP 过期降级、每日一题推送、微信 token 刷新、月度引力值补给 |
| `share` | 分享链接生成 / 访问追踪 / 积分奖励 |
| `tts` | 语音合成（TTS） |
| `admin` | 管理后台 API |
| `positions` | 热门岗位维护 |
| `interview-review` | 面试复盘（音频上传 -> whisper.cpp ASR -> AI 评析 -> 口语分析） |
|`career-advice` | AI 择业顾问：专业分析 + 岗位匹配 + 推荐对话 |
| `upload` | 文件上传（PDF/图片） |
| `email` | 邮件发送 |
| `daily-question` | 每日一题 API |
| `schemas/` | 共享 Schema（pricing 定价、site-config、company-bank 等） |

### 前端页面（3 Tab + 18 子页）

- **Tab1 面试**: pages/index/index → interview → report → career
- **Tab2 面经**: pages/history/history → contribute → company-bank
- **Tab3 我的**: pages/user/user → login/member/progress/resume/review/career/about/agreement/privacy/admin/share
- 其他: internship, result

---

## 二、架构约定（必须遵守）

### 模块模式
每个业务模块遵循 NestJS 标准结构：
```
模块名/
├── 模块名.module.ts    # @Module({ imports: [MongooseModule.forFeature(...)], controllers, providers, exports })
├── 模块名.controller.ts # @Controller('prefix')，注入 service
├── 模块名.service.ts    # @Injectable()，注入 Model
└── 模块名.schema.ts     # @Schema({ timestamps: true })，class + SchemaFactory
```

### 认证体系
- **全局守卫**: `JwtAuthGuard` 默认拦截所有路由（在 `app.module.ts` 中 `APP_GUARD` 注册）
- **白名单**: 公开接口加 `@Public()` 装饰器（登录、注册、支付回调、分享访问等）
- **管理员**: 管理接口加 `@UseGuards(AdminGuard)`（admin controller 内部）
- **当前用户**: `@CurrentUser('userId')` 从 JWT payload 提取用户 ID
- **JWT 过期**: 7 天，在 `app.module.ts` 和每个模块的 `JwtModule.register` 中配置

### 安全硬性要求
1. **JWT_SECRET 必须来自环境变量**，不允许任何硬编码 fallback（已有历史漏洞修复）
2. **所有外部输入**经过 class-validator `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })`
3. **修改用户额度/积分**使用 `findOneAndUpdate + $inc` 原子操作，禁止 read-modify-write
4. **支付订单查询**校验 userId 归属，防止 IDOR
5. **CORS** 生产环境必须配置白名单（当前在 `main.ts` 中从 `CORS_ORIGINS` 环境变量读取）
6. **用户内容输出到响应**时避免泄漏敏感信息（验证码、密钥等）
7. **MongoDB 查询**中对外部输入的字符串做特殊字符转义（尤其在 admin 模块）

### AI 调用
- 主模型: `opencode-go` (deepseek-v4-flash)
- 备用模型: NVIDIA (stepfun-ai/step-3.5-flash)
- 主用不可用时自动切换（在 `ai` 模块处理）
- 环境变量: `AI_PRIMARY_KEY`, `AI_BACKUP_KEY`

### 支付（微信支付 v3 + 虚拟支付）
- Native 支付（H5 扫码）: `POST /payment/create-product`（按量购买引力值）
- JSAPI 支付（小程序内）: `POST /payment/jsapi-product`（按量购买引力值）
- 虚拟支付（小程序内直接购买）: `POST /virtual-payment/create`（mode=`short_series_coin`，代币名「引力值」，1 币 = 1 元）
- 支付回调: `POST /payment/notify`（@Public，验签 + 解密 + 自动到账）
- 支付结果轮询: `GET /payment/check/:outTradeNo`
- 产品定价: `GET /member/plans`（含 products 字段，定义引力值单价和赠送量）
- 需要微信商户证书文件（通过 postbuild 复制到 dist）
- **注意**: 当前会员体系已从按月订阅制改为按量购买引力值制（小程序内虚拟支付直接购买，H5 扫码支付）

---

## 三、开发命令

> ⚠️ **构建铁律：必须始终使用 `npm run build:*` 命令，禁止直接调用 `npx uni build` 或 `npx nest build`。**
> 前端 `npm run build:mp-weixin` 和 `npm run build:h5` 脚本包含头像文件（`avatar-*.png`）复制步骤，
> 后端 `npm run build` 是 `nest build` 的别名。直接使用 `npx` 会遗漏这些关键步骤，导致线上数字人头像不显示等问题。

### 后端
```bash
# 路径: backend/
npm run start:dev          # 开发模式（watch）
npm run build              # 编译到 dist/
npm test                   # 单元测试（43 个，jest --forceExit --detectOpenHandles）
npm run test:watch         # 监听模式
npm run test:cov           # 覆盖率报告
npm run test:e2e           # 集成测试（需 MongoDB 运行）
npm run test:browser       # Playwright API 测试（需后端运行）
```

### 前端
```bash
# 路径: zhiyin-app/
npm run dev:mp-weixin      # 微信小程序开发（uni -p mp-weixin）
npm run build:mp-weixin    # 构建小程序（输出 dist/build/mp-weixin/）
npm run dev:h5             # H5 开发（端口 8888，带 /api 代理到 localhost:3006）
npm run build:h5           # 构建 H5（输出 dist/build/h5/）
npm test                   # 前端单元测试（vitest，7 个）
```

### 构建检查
```bash
# 后端构建（注意 OOM：需 NODE_OPTIONS="--max-old-space-size=2048"）
cd backend && npm run build
```

### 构建产物说明
- **H5 构建** (`npm run build:h5`)：自动复制 `avatar-*.png` 到 `static/`，复制 `robots.txt` 和 `sitemap.xml` 到输出根目录
- **版本号注入**：`vite.config.js` 中 `define.__APP_VERSION__` 自动从 `git describe --tags` 获取版本号，`about.vue` 页面实时显示
- **小程序上传版本** (`scripts/upload-mp.js`)：自动从 git tag 获取基础版本，末位自增 1（如 tag v1.0.16 → 上传版本 1.0.17）
- **微信分享**：所有主页面均已注册 `onShareAppMessage` + `onShareTimeline`，右上角菜单转发/分享到朋友圈可用

### 部署后端
```bash
cd backend && npm run build
cp -rf dist/* /www/wwwroot/server/zhiyin/backend/dist/
cp -r certs /www/wwwroot/server/zhiyin/backend/dist/src/certs
pm2 restart yhl-backend
sleep 3 && curl -s http://localhost:3006/api/user/wx-login -X POST -H "Content-Type: application/json" -d '{"code":"test"}'
```

### 部署前端 H5
```bash
cd zhiyin-app && npm run build:h5
rm -rf /www/wwwroot/zhiyin.yzrcloud.cn/assets
cp -r dist/build/h5/index.html /www/wwwroot/zhiyin.yzrcloud.cn/
cp -r dist/build/h5/assets /www/wwwroot/zhiyin.yzrcloud.cn/
cp -r dist/build/h5/static /www/wwwroot/zhiyin.yzrcloud.cn/
cp -f dist/build/h5/robots.txt dist/build/h5/sitemap.xml /www/wwwroot/zhiyin.yzrcloud.cn/
chown -R www:www /www/wwwroot/zhiyin.yzrcloud.cn/
# 验证无缺失文件
grep -oP '["'"'"']([a-zA-Z0-9_-]+\.[a-z]+(\.js|\.css|\.png|\.svg))["'"'"']' /www/wwwroot/zhiyin.yzrcloud.cn/assets/index-*.js | sort -u
```

### 小程序上传（先 build 后 upload，两步分开更安全）
```bash
cd zhiyin-app && npm run build:mp-weixin && node scripts/upload-mp.js
# 注意：build:mp-weixin 已自动复制 avatar-*.png 到 dist/build/mp-weixin/static/
# 如遇数字人头像不显示，检查是否漏了 cp 步骤，重新用 npm run build:mp-weixin 构建
```

---

## 四、测试注意事项

- **e2e 测试**需要 MongoDB 运行 + `jest-setup.ts` 设置测试 JWT_SECRET
- `payment.controller.spec.ts` 涉及微信支付，需 mock 外部依赖
- `benchmark.service.spec.ts` 涉及行业基准计算
- Playwright 测试需要后端已在运行（测试 `api.browser.spec.ts`）
- 测试文件位置：`backend/src/**/*.spec.ts`（单元），`backend/test/*.e2e-spec.ts`（集成），`backend/test/*.browser.spec.ts`（浏览器）
- 前端测试：`zhiyin-app/src/**/*.spec.ts`（vitest + jsdom）

---

## 五、定时任务（4 个 cron，在 schedule 模块）

| 服务 | 周期 | 职责 |
|------|------|------|
| `VipExpiryService` | 每日 00:00 | 扫描过期 VIP 并降级为 free 计划 |
| `DailyQuestionPushService` | 每日 09:00 | 通过微信订阅消息推送每日一题（需配置模板 ID） |
| `WechatTokenService` | 每 2 小时 | 刷新微信 access_token（缓存到 Redis） |
| `GravityTopUpService` | 每日 02:00 | 给所有未过期的成长版/冲刺版用户补给月度引力值 |

---

## 六、项目状态与开发阶段

**当前**: Phase 1.5（商业化 + 全量部署）— v1.0.18（开发中）

| 阶段 | 状态 | 关键交付 |
|------|------|---------|
| Phase 0: 战略升级 | ✅ 完成 | 定价重构（免费 + 按量购买），三层壁垒设计 |
| Phase 0.5: 壁垒构建 | ✅ 完成 | 数据飞轮（面经贡献+题库），留存入围（进步轨迹+打卡日历+每日一题） |
| Phase 1: MVP 上线 | ✅ 完成 | 面试复盘（whisper.cpp ASR + AI 评析）、AI 择业顾问 |
| Phase 1.5: 商业化 | 🚧 当前 | 按量购买引力值（¥5/份）、管理后台完善、H5/小程序全量部署上线、清理脚本 |
| Phase 2: 增强 + 题库 | 📋 规划 | 50+ 校招岗位、技能缺口分析、公司真题库建设 |
| Phase 3: 秋招冲刺 | 📋 规划 | 高校合作、B 端服务、KOC 推广 |

详细产品规划见 `docs/PRODUCT-PLAN.md`，路线图见 `docs/ROADMAP.md`。

---

## 七、环境变量

### 后端（`backend/.env`，不提交 git，在 `.gitignore` 中）
```
MONGODB_URI=mongodb://localhost:27017/zhiyin
JWT_SECRET=your-strong-secret-at-least-32-chars
NODE_ENV=production
PORT=3006
AI_PRIMARY_KEY=xxx
AI_BACKUP_KEY=xxx
WECHAT_APPID=wxf466b3c3bc411ffc
WECHAT_MCHID=xxx
WECHAT_API_KEY=xxx
WECHAT_SERIAL_NO=xxx
WECHAT_PRIVATE_KEY_PATH=/path/to/apiclient_key.pem
WX_DAILY_QUESTION_TMPL=微信订阅消息模板 ID
CORS_ORIGINS=http://localhost:8888,https://zhiyin.yzrcloud.cn
EMAIL_HOST=smtp.qiye.aliyun.com
EMAIL_PORT=465
EMAIL_SECURE=true
EMAIL_USER=contact@yuzhiran.com
EMAIL_PASSWORD=xxx
EMAIL_FROM=宇之然AI磁场 <contact@yuzhiran.com>
WHISPER_CPP_PATH=/home/wlt/whisper.cpp   # whisper.cpp 路径
WHISPER_MODEL=base                        # ASR 模型：tiny / base / small
WHISPER_LANGUAGE=zh                       # ASR 语言
WHISPER_THREADS=4                         # ASR CPU 线程数
```

### 前端（`zhiyin-app/.env.production`，已提交 git）
```
VITE_API_BASE_URL=https://zhiyinwx.yzrcloud.cn/api
VITE_APP_NAME=AI磁场
```

---

## 八、测试 / 管理员账号

| 账号 | 密码 | 角色 | 说明 |
|------|------|------|------|
| `13701190814@139.com` | `Zhiyin2024!` | admin | 管理员，可访问管理后台 |
| `test@yzrcloud.cn` | `123456` | user | 测试账号（普通用户，含 5 引力值） |
| `test@test.com` | 验证码 `123456` | admin | 旧管理员（dev 模式可用） |

管理后台路径：`/pages/admin/admin`，进入后自动验证管理员身份（`onMounted` → `doVerify`）。

---

## 九、Git

- 远程仓库: `http://127.0.0.1:2999/txai-dev/zhiyin.git`（本机 Gitea，带 token 认证）
- 默认分支: `master`
- 最新 tag: `v1.0.17`（小程序上传版本 v1.0.18 源自 git tag v1.0.17 + 末位自增 1）

---

## 十、技术细节与坑

1. **DOMMatrix polyfill**: `main.ts` 顶部有 pdf-parse 所需的浏览器 API polyfill（DOMMatrix / DOMPoint），新增 PDF 相关功能时注意兼容性
2. **postbuild**: `backend/package.json` 中的 `postbuild` 脚本自动复制 `certs/` 到 `dist/src/certs/`，这是微信支付证书的必要步骤
3. **微信小程序 appid**: `zhiyin-app/manifest.json` 中 `mp-weixin.appid = wxf466b3c3bc411ffc`；开发模式 `appid = __UNI__DEV__`
4. **前端 API 调用**: `zhiyin-app/src/services/api.ts` 封装了 `uni.request`，自动处理 token 注入（从 `uni.getStorageSync('token')`）和 401 过期跳转
5. **前端环境判断**: `config.ts` 中使用 `// #ifdef H5` / `// #ifdef MP-WEIXIN` 条件编译区分 H5 和小程序
6. **API 限流**: 100 次/60 秒（在 `app.module.ts` 中配置），注意避免在定时任务和批量操作中被限
7. **验证码**: 生产模式（`NODE_ENV=production`）使用真实 SMTP 发邮件验证码；非生产模式手机验证码固定为 `123456`、邮件验证码在响应中返回 `devCode`
8. **MongoDB**: 8 个核心集合 + 2 个分享集合
9. **引力值体系**: 所有计划统一走引力值消耗（面试 5、优化 3、下载 2）。VIP 不再免额度，成长版每月赠送 80 引力值，冲刺版每月赠送 200 引力值，每日凌晨 2 点定时补给。免费用户注册送 5 引力值。小程序内通过分享得引力值/贡献面经/虚拟支付购买三种方式获取引力值；H5 直接扫码支付按量购买（¥5/份）。
10. **api.ts 陷阱**: 对象字面量必须在 `export const apiService = {` 或 `const apiService = { ... export default apiService` 中包裹，否则 uni-app 构建报错 `Expected ";" but found ":"`。git pull 后经常丢失这行声明，需手动补回
11. **H5 构建 assets 清理**: `assets/` 中的旧 hash 文件不能随意删除——`index-*.js`（主 bundle）动态 import 了所有 page chunk，删除仍在引用的文件会导致浏览器 `NS_ERROR_CORRUPTED_CONTENT`
12. **管理后台自动验证**: `admin.vue` 中 `onMounted` 自动调用 `doVerify()`，进入后台即检测 JWT 中 `role` 是否为 `admin`，不再需要手动点击"验证管理员身份"按钮
13. **分享重定向路由**: `share.controller.ts` 中 `GET /api/share/:shareCode` 是公开路由（泛匹配，放最后避免拦截其他路由），访问时记录访问者 IP → 302 重定向到 H5 首页
14. **小程序官网购买走剪贴板**: 小程序内"官网购买"不再使用 webview 内嵌 H5，改为 `uni.setClipboardData` 复制带 JWT token 的 URL 到剪贴板，提示用户在手机浏览器中打开购买（`App.vue` 的 `handleH5UrlParams` 解析 `?token=` 和 `?buy=gravity` 参数自动登录跳转）

---

## 十一、交付检查清单（每次实施/修改后执行）

### Step 1: 代码评审
- [ ] 是否符合现有模块模式（schema→service→controller→module）
- [ ] 是否有多余变量、import、参数
- [ ] 命名是否与项目一致
- [ ] 是否有重复代码或可复用逻辑

### Step 2: 安全评审
- [ ] 外部输入是否有注入风险（HTML、Shell、NoSQL）
- [ ] 接口是否正确加了 JWT guard 或 `@Public()`
- [ ] 管理端接口是否有 AdminGuard
- [ ] 修改用户额度/积分是否用 `$inc` 原子操作
- [ ] 敏感信息是否泄漏到响应或日志中
- [ ] 用户内容输出是否做了转义

### Step 3: 性能评审
- [ ] 是否存在 N+1 查询
- [ ] 大表查询是否有索引覆盖
- [ ] 读操作是否该加 `.lean()`
- [ ] 是否有内存泄漏风险（puppeteer 等资源未释放）

### Step 4: 测试验证
```bash
cd backend && npm run build
npm test -- --forceExit --detectOpenHandles
```

### Step 5: LSP 诊断
- [ ] Changed files diagnostics clean
- [ ] 无 `as any` / `@ts-ignore` / `@ts-expect-error`