Files
zhiyin/AGENTS.md
T
yuzhiran 8ee27fdd32 feat: refactor member to pay-per-use gravity purchase; mv webview to clipboard+browser
- member.vue: rewrite from subscription plans (free/growth/sprint) to
  H5-only pay-per-use gravity purchase with quantity selector + QR code
- user.vue: gravity card replacing quota card, add share/contribute/H5-buy
  entry points, plus gravity acquisition modal (share/contribute/buy)
- share.vue: layout fix (flex column), smarter copyLink with cached URL,
  WeChat timeline hint instead of open-type
- share.controller.ts: add GET /:shareCode redirect route (IP record + 302)
- interview.vue: guest mode fix, H5 buy modal, clipboard copy instead of
  webview for mini-program
- App.vue: handleH5UrlParams for ?token=&buy=gravity auto-login
- composables/useGravityPurchase.ts: reusable gravity purchase composable
- remove webview.vue (no longer used), replace with clipboard+browser flow
- AGENTS.md: sync all above changes, fix duplicate numbering
2026-06-20 20:49:15 +08:00

318 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.
# 职引 (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` | 微信支付 v3Native + 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/` | 共享 Schemapricing 定价、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 /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
```
### 部署后端
```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/
chown -R www:www /www/wwwroot/zhiyin.yzrcloud.cn/index.html /www/wwwroot/zhiyin.yzrcloud.cn/assets
# 验证无缺失文件
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 1MVP 上线)进行中 — v1.0.16
| 阶段 | 状态 | 关键交付 |
|------|------|---------|
| Phase 0: 战略升级 | ✅ 完成 | 定价重构(免费 + ¥19.9/月),三层壁垒设计 |
| Phase 0.5: 壁垒构建 | ✅ 完成 | 数据飞轮(面经贡献+题库),留存入围(进步轨迹+打卡日历+每日一题) |
| Phase 1: MVP 上线 | 🚧 当前 | 小程序 v1.0.16 已上传、引力值体系统一(订阅制改为按量购买)、管理后台完善、H5 已部署、生产模式已启用 |
| Phase 1.5: 商业化 | 📋 规划 | 引力值运营策略、每日一题定时推送、PMF 验证 |
| 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 | 测试账号 |
| `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.16`(小程序上传版本号源自 git tag)
---
## 十、技术细节与坑
1. **DOMMatrix polyfill**: `main.ts` 顶部有 pdf-parse 所需的浏览器 API polyfillDOMMatrix / 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 不再免额度,成长版每月 250 引力值,冲刺版每月 600 引力值,每日凌晨 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`