8ee27fdd32
- 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
318 lines
16 KiB
Markdown
318 lines
16 KiB
Markdown
# 职引 (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 /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 1(MVP 上线)进行中 — 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 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 不再免额度,成长版每月 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` |