feat: init api-server v0.1
- NestJS + TypeScript 后端 API - 用户认证 (auth) - 用户管理 (users) - 学习路径与课程 (learning) - AI 分析与对话 (ai) - 用户反馈 (feedback) - 等待名单 (waitlist) - 知识库 (knowledge) - Swagger API 文档(中文、访问控制) - Basic Auth 保护生产环境文档
This commit is contained in:
commit
bd44b7e138
18
.env.example
Normal file
18
.env.example
Normal file
@ -0,0 +1,18 @@
|
||||
PORT=3000
|
||||
|
||||
DATABASE_URL="mysql://ai_study_user:ai_study_password@localhost:3306/ai_study"
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
AI_PROVIDER=mock
|
||||
AI_API_KEY=
|
||||
AI_BASE_URL=
|
||||
|
||||
JWT_SECRET=change_me_in_production
|
||||
|
||||
NODE_ENV=development
|
||||
|
||||
ENABLE_SWAGGER=true
|
||||
SWAGGER_USER=admin
|
||||
SWAGGER_PASSWORD=change_me
|
||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Test
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Misc
|
||||
.cache/
|
||||
tmp/
|
||||
temp/
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
175
README.md
Normal file
175
README.md
Normal file
@ -0,0 +1,175 @@
|
||||
# AI Study Product API
|
||||
|
||||
AI 学习产品的后端 API 服务(v0.1),基于 NestJS + TypeScript 构建。
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| health | 健康检查 | ✅ |
|
||||
| auth | 用户认证(登录/注册/Token刷新) | ✅ |
|
||||
| users | 用户管理 | ✅ |
|
||||
| knowledge | 知识库/文章 | ✅ |
|
||||
| learning | 学习路径/课程/进度 | ✅ |
|
||||
| ai | AI 分析与对话(Mock) | ✅ |
|
||||
| review | 间隔重复复习任务 | ✅ |
|
||||
| feedback | 用户反馈 | ✅ |
|
||||
| waitlist | 等待名单 | ✅ |
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发模式
|
||||
npm run start:dev
|
||||
|
||||
# 生产模式
|
||||
npm run build && npm run start:prod
|
||||
```
|
||||
|
||||
## 接口文档
|
||||
|
||||
### 本地开发环境
|
||||
|
||||
启动服务后,访问:
|
||||
|
||||
- **Swagger UI**: http://localhost:3000/api-docs
|
||||
- **OpenAPI JSON**: http://localhost:3000/api-docs-json
|
||||
|
||||
本地开发环境默认启用 Swagger,无需认证。
|
||||
|
||||
### 服务器内测环境
|
||||
|
||||
如果需要在非本地环境启用 Swagger,配置环境变量:
|
||||
|
||||
```env
|
||||
NODE_ENV=production
|
||||
ENABLE_SWAGGER=true
|
||||
SWAGGER_USER=your_admin_username
|
||||
SWAGGER_PASSWORD=your_secure_password
|
||||
```
|
||||
|
||||
访问时需要输入账号密码:
|
||||
|
||||
- **Swagger UI**: https://your-domain.com/api-docs
|
||||
- **OpenAPI JSON**: https://your-domain.com/api-docs-json
|
||||
|
||||
### 正式生产环境
|
||||
|
||||
默认关闭 Swagger,确保 API 结构不会公开暴露。
|
||||
|
||||
如需临时开启内网访问,配置同上并设置强密码。
|
||||
|
||||
## 环境变量
|
||||
|
||||
复制 `.env.example` 为 `.env` 并配置:
|
||||
|
||||
```env
|
||||
PORT=3000
|
||||
DATABASE_URL="mysql://user:password@localhost:3306/ai_study"
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
AI_PROVIDER=mock
|
||||
AI_API_KEY=
|
||||
AI_BASE_URL=
|
||||
JWT_SECRET=change_me_in_production
|
||||
|
||||
NODE_ENV=development
|
||||
ENABLE_SWAGGER=true
|
||||
SWAGGER_USER=admin
|
||||
SWAGGER_PASSWORD=change_me
|
||||
```
|
||||
|
||||
| 变量 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| PORT | 服务端口 | 3000 |
|
||||
| NODE_ENV | 运行环境 | development |
|
||||
| ENABLE_SWAGGER | 是否启用 Swagger | true (开发) / false (生产) |
|
||||
| SWAGGER_USER | Swagger 用户名 | admin |
|
||||
| SWAGGER_PASSWORD | Swagger 密码 | change_me |
|
||||
|
||||
## API 端点概览
|
||||
|
||||
### 健康检查
|
||||
- `GET /` - 服务健康检查
|
||||
|
||||
### 认证
|
||||
- `POST /auth/login` - 用户登录
|
||||
- `POST /auth/register` - 用户注册
|
||||
- `POST /auth/refresh` - 刷新 Token
|
||||
- `POST /auth/logout` - 退出登录
|
||||
|
||||
### 用户
|
||||
- `POST /users` - 创建用户
|
||||
- `GET /users/:id` - 获取用户信息
|
||||
- `GET /users/:id/profile` - 获取用户详情(含统计数据)
|
||||
- `PATCH /users/:id` - 更新用户信息
|
||||
- `GET /users` - 用户列表
|
||||
|
||||
### 知识库
|
||||
- `GET /knowledge/categories` - 获取分类
|
||||
- `GET /knowledge/articles` - 获取文章列表
|
||||
- `GET /knowledge/articles/:id` - 获取文章详情
|
||||
|
||||
### 学习
|
||||
- `GET /learning/paths` - 获取所有学习路径
|
||||
- `GET /learning/paths/:id` - 获取学习路径详情
|
||||
- `GET /learning/paths/:id/courses` - 获取课程列表
|
||||
- `GET /learning/courses/:id` - 获取课程详情
|
||||
- `GET /learning/lessons/:id` - 获取课时详情
|
||||
- `GET /learning/records?userId=` - 获取用户学习记录
|
||||
- `PATCH /learning/progress` - 更新学习进度
|
||||
|
||||
### AI
|
||||
- `POST /ai/analyze` - AI 学习分析(弱点/进度/推荐)
|
||||
- `POST /ai/chat` - AI 对话
|
||||
- `GET /ai/sessions?userId=` - 获取对话历史
|
||||
- `GET /ai/sessions/:sessionId` - 获取指定会话
|
||||
|
||||
### 复习
|
||||
- `GET /learning/review?userId=` - 获取复习任务
|
||||
- `POST /learning/review` - 创建复习任务
|
||||
|
||||
### 反馈
|
||||
- `POST /feedback` - 提交反馈
|
||||
- `GET /feedback` - 获取反馈列表
|
||||
- `GET /feedback/stats` - 反馈统计
|
||||
- `PATCH /feedback/:id/status` - 更新反馈状态
|
||||
|
||||
### 等待名单
|
||||
- `POST /waitlist` - 加入等待名单
|
||||
- `GET /waitlist` - 获取所有报名
|
||||
- `GET /waitlist/stats` - 报名统计
|
||||
|
||||
## 技术栈
|
||||
|
||||
- NestJS 11.x
|
||||
- TypeScript
|
||||
- class-validator / class-transformer
|
||||
- @nestjs/swagger
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.ts # 入口
|
||||
├── app.module.ts # 根模块
|
||||
├── auth/ # 认证模块
|
||||
├── users/ # 用户模块
|
||||
├── learning/ # 学习模块
|
||||
├── ai/ # AI 模块
|
||||
├── feedback/ # 反馈模块
|
||||
├── waitlist/ # 等待名单模块
|
||||
├── knowledge/ # 知识库模块
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 后续规划
|
||||
|
||||
- [ ] 接入真实 AI API(OpenAI / Claude)
|
||||
- [ ] 添加数据库持久化
|
||||
- [ ] 实现 JWT 认证中间件
|
||||
- [ ] 添加 Redis 缓存
|
||||
- [ ] iOS SDK 集成文档
|
||||
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
10051
package-lock.json
generated
Normal file
10051
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
75
package.json
Normal file
75
package.json
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "api-server",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.4.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-express": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/supertest": "^7.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^17.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
50
src/ai/ai.controller.ts
Normal file
50
src/ai/ai.controller.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { AiService } from './ai.service';
|
||||
|
||||
@ApiTags('ai')
|
||||
@Controller('ai')
|
||||
export class AiController {
|
||||
constructor(private readonly aiService: AiService) {}
|
||||
|
||||
@Post('analyze')
|
||||
@ApiOperation({ summary: 'AI 学习分析', description: '分析用户学习数据,提供学习洞察' })
|
||||
@ApiResponse({ status: 200, description: '返回分析结果' })
|
||||
@ApiResponse({ status: 400, description: '请求参数无效' })
|
||||
async analyze(@Body() body: {
|
||||
userId: string;
|
||||
type: 'weakness' | 'progress' | 'recommendation';
|
||||
context?: any;
|
||||
}) {
|
||||
return this.aiService.analyze(body);
|
||||
}
|
||||
|
||||
@Post('chat')
|
||||
@ApiOperation({ summary: 'AI 对话', description: '向 AI 学习助手发送消息' })
|
||||
@ApiResponse({ status: 200, description: '返回 AI 回复' })
|
||||
@ApiResponse({ status: 400, description: '消息格式无效' })
|
||||
async chat(@Body() body: { userId: string; message: string; context?: string }) {
|
||||
return this.aiService.chat(body);
|
||||
}
|
||||
|
||||
@Get('sessions')
|
||||
@ApiOperation({ summary: '获取对话历史', description: '获取用户的所有对话会话' })
|
||||
@ApiQuery({ name: 'userId', description: '用户 ID', example: 'user_123456' })
|
||||
@ApiResponse({ status: 200, description: '对话会话列表' })
|
||||
async getSessions(@Query('userId') userId: string) {
|
||||
return this.aiService.getSessions(userId);
|
||||
}
|
||||
|
||||
@Get('sessions/:sessionId')
|
||||
@ApiOperation({ summary: '获取指定会话', description: '获取特定的对话会话详情' })
|
||||
@ApiParam({ name: 'sessionId', description: '会话 ID', example: 'session_123' })
|
||||
@ApiQuery({ name: 'userId', description: '用户 ID', example: 'user_123456' })
|
||||
@ApiResponse({ status: 200, description: '找到对话会话' })
|
||||
@ApiResponse({ status: 404, description: '会话不存在' })
|
||||
async getSession(
|
||||
@Query('userId') userId: string,
|
||||
@Param('sessionId') sessionId: string
|
||||
) {
|
||||
return this.aiService.getSession(userId, sessionId);
|
||||
}
|
||||
}
|
||||
10
src/ai/ai.module.ts
Normal file
10
src/ai/ai.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiController } from './ai.controller';
|
||||
import { AiService } from './ai.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AiController],
|
||||
providers: [AiService],
|
||||
exports: [AiService],
|
||||
})
|
||||
export class AiModule {}
|
||||
156
src/ai/ai.service.ts
Normal file
156
src/ai/ai.service.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AnalysisRequest, AnalysisResult, ChatSession, ChatMessage, ChatRequest, ChatResponse } from './entities/ai.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AiService {
|
||||
private sessions: Map<string, ChatSession[]> = new Map();
|
||||
|
||||
async analyze(request: AnalysisRequest): Promise<AnalysisResult> {
|
||||
const { type, context } = request;
|
||||
|
||||
if (type === 'weakness') {
|
||||
return this.analyzeWeakness(context);
|
||||
} else if (type === 'progress') {
|
||||
return this.analyzeProgress(context);
|
||||
} else {
|
||||
return this.generateRecommendation(context);
|
||||
}
|
||||
}
|
||||
|
||||
private analyzeWeakness(context: any): AnalysisResult {
|
||||
return {
|
||||
type: 'weakness_analysis',
|
||||
score: 0.7,
|
||||
insights: [
|
||||
'在归纳概括类题目中失分较多',
|
||||
'对策建议的实操性有待提升',
|
||||
'文章结构逻辑偶有跳跃',
|
||||
],
|
||||
recommendations: [
|
||||
'每天练习一道归纳概括题',
|
||||
'多参考政府官方文件中的对策表达',
|
||||
'使用 AI 辅助检查文章逻辑连贯性',
|
||||
],
|
||||
generatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
private analyzeProgress(context: any): AnalysisResult {
|
||||
return {
|
||||
type: 'progress_analysis',
|
||||
score: 0.85,
|
||||
insights: [
|
||||
'本周学习时长较上周增长 20%',
|
||||
'练习正确率从 65% 提升到 78%',
|
||||
'薄弱知识点数量减少 3 个',
|
||||
],
|
||||
recommendations: [
|
||||
'保持当前学习节奏',
|
||||
'可适当增加实战演练',
|
||||
],
|
||||
generatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
private generateRecommendation(context: any): AnalysisResult {
|
||||
return {
|
||||
type: 'recommendation',
|
||||
score: 0.9,
|
||||
insights: [
|
||||
'根据你的学习数据,推荐以下学习路径',
|
||||
],
|
||||
recommendations: [
|
||||
'继续完成申论基础认知课程',
|
||||
'开始练习材料分析技巧',
|
||||
'每天预留 30 分钟复习错题',
|
||||
],
|
||||
generatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
async chat(request: ChatRequest): Promise<ChatResponse> {
|
||||
const { userId, message, context } = request;
|
||||
const sessions = this.sessions.get(userId) || [];
|
||||
const currentSession = sessions[sessions.length - 1] || this.createSession(userId, context);
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
id: `msg_${Date.now()}_user`,
|
||||
role: 'user',
|
||||
content: message,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
currentSession.messages.push(userMessage);
|
||||
|
||||
const aiResponse = this.generateMockResponse(message, currentSession.messages);
|
||||
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: `msg_${Date.now()}_ai`,
|
||||
role: 'assistant',
|
||||
content: aiResponse.content,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
currentSession.messages.push(assistantMessage);
|
||||
|
||||
if (!sessions.length) {
|
||||
sessions.push(currentSession);
|
||||
}
|
||||
this.sessions.set(userId, sessions);
|
||||
|
||||
return {
|
||||
sessionId: currentSession.id,
|
||||
message: assistantMessage,
|
||||
suggestions: aiResponse.suggestions,
|
||||
};
|
||||
}
|
||||
|
||||
private createSession(userId: string, context?: string): ChatSession {
|
||||
return {
|
||||
id: `session_${Date.now()}`,
|
||||
userId,
|
||||
messages: [],
|
||||
context: context || '你是龙de的 AI 学习助手,专注于帮助用户学习。',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
private generateMockResponse(message: string, history: ChatMessage[]): { content: string; suggestions?: string[] } {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
if (lowerMessage.includes('申论') || lowerMessage.includes('怎么写')) {
|
||||
return {
|
||||
content: '申论写作的关键在于:1) 准确理解材料主题 2) 逻辑清晰地展开论述 3) 提出的对策要具有可操作性。建议你先确定文章的结构框架,再逐步填充内容。需要我帮你分析具体的题目吗?',
|
||||
suggestions: ['帮我分析这道题', '给我一个写作框架', '如何提升对策质量'],
|
||||
};
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('复习') || lowerMessage.includes('忘记')) {
|
||||
return {
|
||||
content: '根据间隔重复原理,我建议你在学习新知识后的 1 天、3 天、7 天、14 天分别复习。复习时先回忆要点,如果想不起来再看原内容,这样记忆效果会更好。要我帮你制定复习计划吗?',
|
||||
suggestions: ['制定复习计划', '查看复习任务', '调整复习间隔'],
|
||||
};
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('算法') || lowerMessage.includes('面试')) {
|
||||
return {
|
||||
content: '面试算法准备建议分三步:1) 掌握基础数据结构(数组、链表、树、图)2) 熟悉常见算法思想(递归、动态规划、二分)3) 多做真题模拟。建议每天至少刷 2 道题,保持手感。',
|
||||
suggestions: ['推荐刷题路线', '讲解动态规划', '模拟面试场景'],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: '好的,我理解你的问题。作为你的学习助手,我可以帮助你:\n\n• 解答学习中的具体问题\n• 分析你的学习薄弱点\n• 提供学习计划建议\n• 陪你练习题目\n\n请告诉我你具体想学习什么内容?',
|
||||
suggestions: ['公考申论指导', 'AI工具使用', '编程面试准备'],
|
||||
};
|
||||
}
|
||||
|
||||
async getSession(userId: string, sessionId: string): Promise<ChatSession | undefined> {
|
||||
const sessions = this.sessions.get(userId) || [];
|
||||
return sessions.find(s => s.id === sessionId);
|
||||
}
|
||||
|
||||
async getSessions(userId: string): Promise<ChatSession[]> {
|
||||
return this.sessions.get(userId) || [];
|
||||
}
|
||||
}
|
||||
86
src/ai/entities/ai.entity.ts
Normal file
86
src/ai/entities/ai.entity.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class AnalysisRequest {
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456' })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: '分析类型', example: 'weakness', enum: ['weakness', 'progress', 'recommendation'] })
|
||||
type: 'weakness' | 'progress' | 'recommendation';
|
||||
|
||||
@ApiPropertyOptional({ description: '分析上下文数据' })
|
||||
context?: {
|
||||
completedLessons?: string[];
|
||||
accuracy?: number;
|
||||
timeSpent?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class AnalysisResult {
|
||||
@ApiProperty({ description: '分析类型', example: 'weakness_analysis' })
|
||||
type: string;
|
||||
|
||||
@ApiProperty({ description: '评分 0-1', example: 0.7 })
|
||||
score: number;
|
||||
|
||||
@ApiProperty({ description: '关键洞察', example: ['在归纳概括类题目中失分较多'] })
|
||||
insights: string[];
|
||||
|
||||
@ApiProperty({ description: '建议', example: ['每天练习一道归纳概括题'] })
|
||||
recommendations: string[];
|
||||
|
||||
@ApiProperty({ description: '分析时间' })
|
||||
generatedAt: Date;
|
||||
}
|
||||
|
||||
export class ChatMessage {
|
||||
@ApiProperty({ description: '消息 ID', example: 'msg_123' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '消息角色', example: 'user', enum: ['user', 'assistant', 'system'] })
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
|
||||
@ApiProperty({ description: '消息内容', example: '我想学习申论怎么写' })
|
||||
content: string;
|
||||
|
||||
@ApiProperty({ description: '时间戳' })
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export class ChatSession {
|
||||
@ApiProperty({ description: '会话 ID', example: 'session_123' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456' })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: '聊天历史', type: [ChatMessage] })
|
||||
messages: ChatMessage[];
|
||||
|
||||
@ApiProperty({ description: '系统上下文', example: '你是龙de的 AI 学习助手' })
|
||||
context: string;
|
||||
|
||||
@ApiProperty({ description: '会话创建时间' })
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class ChatRequest {
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456' })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: '用户消息', example: '我想学习申论怎么写' })
|
||||
message: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '对话上下文(可选)' })
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export class ChatResponse {
|
||||
@ApiProperty({ description: '会话 ID', example: 'session_123' })
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({ description: 'AI 回复消息' })
|
||||
message: ChatMessage;
|
||||
|
||||
@ApiPropertyOptional({ description: '建议的跟进操作', example: ['帮我分析这道题'] })
|
||||
suggestions?: string[];
|
||||
}
|
||||
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
16
src/app.controller.ts
Normal file
16
src/app.controller.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@ApiTags('health')
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '服务健康检查', description: '检查 API 服务器是否正常运行' })
|
||||
@ApiResponse({ status: 200, description: 'API 运行正常', schema: { example: 'Hello World!' } })
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
25
src/app.module.ts
Normal file
25
src/app.module.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { WaitlistModule } from './waitlist/waitlist.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { LearningModule } from './learning/learning.module';
|
||||
import { AiModule } from './ai/ai.module';
|
||||
import { FeedbackModule } from './feedback/feedback.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { KnowledgeModule } from './knowledge/knowledge.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WaitlistModule,
|
||||
UsersModule,
|
||||
LearningModule,
|
||||
AiModule,
|
||||
FeedbackModule,
|
||||
AuthModule,
|
||||
KnowledgeModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
8
src/app.service.ts
Normal file
8
src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
60
src/auth/auth.controller.ts
Normal file
60
src/auth/auth.controller.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '用户登录', description: '使用邮箱和密码登录系统,返回访问令牌' })
|
||||
@ApiResponse({ status: 200, description: '登录成功,返回访问令牌' })
|
||||
@ApiResponse({ status: 401, description: '用户名或密码错误' })
|
||||
async login(@Body() body: { email: string; password: string }) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
accessToken: 'mock_token_' + Date.now(),
|
||||
refreshToken: 'mock_refresh_' + Date.now(),
|
||||
expiresIn: 3600,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('register')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: '用户注册', description: '注册一个新的用户账号' })
|
||||
@ApiResponse({ status: 201, description: '注册成功' })
|
||||
@ApiResponse({ status: 400, description: '输入无效或邮箱已被注册' })
|
||||
async register(@Body() body: { email: string; password: string; nickname?: string }) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
userId: 'user_' + Date.now(),
|
||||
email: body.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '刷新令牌', description: '使用刷新令牌获取新的访问令牌' })
|
||||
@ApiResponse({ status: 200, description: '令牌刷新成功' })
|
||||
@ApiResponse({ status: 401, description: '刷新令牌无效或已过期' })
|
||||
async refresh(@Body() body: { refreshToken: string }) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
accessToken: 'mock_token_' + Date.now(),
|
||||
expiresIn: 3600,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '用户退出', description: '使当前会话失效' })
|
||||
@ApiResponse({ status: 200, description: '退出成功' })
|
||||
async logout() {
|
||||
return { success: true, message: '已退出登录' };
|
||||
}
|
||||
}
|
||||
7
src/auth/auth.module.ts
Normal file
7
src/auth/auth.module.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule {}
|
||||
26
src/feedback/dto/create-feedback.dto.ts
Normal file
26
src/feedback/dto/create-feedback.dto.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateFeedbackDto {
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456' })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: '反馈类型', example: 'feature', enum: ['bug', 'feature', 'general'] })
|
||||
type: 'bug' | 'feature' | 'general';
|
||||
|
||||
@ApiProperty({ description: '反馈内容', example: '希望能添加离线下载功能' })
|
||||
content: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '联系方式(便于后续跟进)', example: 'user@email.com' })
|
||||
contact?: string;
|
||||
}
|
||||
|
||||
export class FeedbackResponse {
|
||||
@ApiProperty({ description: '反馈 ID', example: 'fb_123_abc' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '反馈内容' })
|
||||
content: string;
|
||||
|
||||
@ApiProperty({ description: '提交时间' })
|
||||
createdAt: Date;
|
||||
}
|
||||
27
src/feedback/entities/feedback.entity.ts
Normal file
27
src/feedback/entities/feedback.entity.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class Feedback {
|
||||
@ApiProperty({ description: '反馈 ID', example: 'fb_123_abc' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456' })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: '反馈类型', example: 'feature', enum: ['bug', 'feature', 'general'] })
|
||||
type: 'bug' | 'feature' | 'general';
|
||||
|
||||
@ApiProperty({ description: '反馈内容', example: '希望能添加离线下载功能' })
|
||||
content: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '联系方式' })
|
||||
contact?: string;
|
||||
|
||||
@ApiProperty({ description: '处理状态', example: 'pending', enum: ['pending', 'reviewed', 'resolved'] })
|
||||
status: 'pending' | 'reviewed' | 'resolved';
|
||||
|
||||
@ApiProperty({ description: '提交时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ description: '最后更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
54
src/feedback/feedback.controller.ts
Normal file
54
src/feedback/feedback.controller.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { FeedbackService } from './feedback.service';
|
||||
import { CreateFeedbackDto } from './dto/create-feedback.dto';
|
||||
|
||||
@ApiTags('feedback')
|
||||
@Controller('feedback')
|
||||
export class FeedbackController {
|
||||
constructor(private readonly feedbackService: FeedbackService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '提交反馈', description: '提交用户反馈或建议' })
|
||||
@ApiResponse({ status: 201, description: '反馈提交成功' })
|
||||
@ApiResponse({ status: 400, description: '反馈数据无效' })
|
||||
async create(@Body() createFeedbackDto: CreateFeedbackDto) {
|
||||
const feedback = await this.feedbackService.create(createFeedbackDto);
|
||||
return {
|
||||
success: true,
|
||||
message: '反馈已提交,感谢你的支持',
|
||||
data: { id: feedback.id, createdAt: feedback.createdAt },
|
||||
};
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取反馈列表', description: '获取所有用户反馈提交记录' })
|
||||
@ApiQuery({ name: 'userId', required: false, description: '按用户 ID 筛选', example: 'user_123456' })
|
||||
@ApiResponse({ status: 200, description: '反馈列表' })
|
||||
async findAll(@Query('userId') userId?: string) {
|
||||
if (userId) {
|
||||
return this.feedbackService.findByUserId(userId);
|
||||
}
|
||||
return this.feedbackService.findAll();
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: '获取反馈统计', description: '获取反馈的聚合统计数据' })
|
||||
@ApiResponse({ status: 200, description: '反馈统计数据' })
|
||||
async getStats() {
|
||||
return this.feedbackService.getStats();
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
@ApiOperation({ summary: '更新反馈状态', description: '更新反馈条目的处理状态' })
|
||||
@ApiParam({ name: 'id', description: '反馈 ID', example: 'fb_123_abc' })
|
||||
@ApiResponse({ status: 200, description: '状态已更新' })
|
||||
@ApiResponse({ status: 404, description: '反馈不存在' })
|
||||
async updateStatus(
|
||||
@Param('id') id: string,
|
||||
@Body('status') status: 'pending' | 'reviewed' | 'resolved'
|
||||
) {
|
||||
const feedback = await this.feedbackService.updateStatus(id, status);
|
||||
return { success: true, data: feedback };
|
||||
}
|
||||
}
|
||||
10
src/feedback/feedback.module.ts
Normal file
10
src/feedback/feedback.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FeedbackController } from './feedback.controller';
|
||||
import { FeedbackService } from './feedback.service';
|
||||
|
||||
@Module({
|
||||
controllers: [FeedbackController],
|
||||
providers: [FeedbackService],
|
||||
exports: [FeedbackService],
|
||||
})
|
||||
export class FeedbackModule {}
|
||||
56
src/feedback/feedback.service.ts
Normal file
56
src/feedback/feedback.service.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateFeedbackDto } from './dto/create-feedback.dto';
|
||||
import { Feedback } from './entities/feedback.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FeedbackService {
|
||||
private feedbacks: Feedback[] = [];
|
||||
|
||||
async create(createFeedbackDto: CreateFeedbackDto): Promise<Feedback> {
|
||||
const feedback: Feedback = {
|
||||
id: `fb_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
||||
userId: createFeedbackDto.userId,
|
||||
type: createFeedbackDto.type,
|
||||
content: createFeedbackDto.content,
|
||||
contact: createFeedbackDto.contact,
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
this.feedbacks.push(feedback);
|
||||
console.log('[Feedback] New feedback received:', feedback.type);
|
||||
return feedback;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Feedback[]> {
|
||||
return this.feedbacks;
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<Feedback[]> {
|
||||
return this.feedbacks.filter(f => f.userId === userId);
|
||||
}
|
||||
|
||||
async updateStatus(id: string, status: 'pending' | 'reviewed' | 'resolved'): Promise<Feedback | undefined> {
|
||||
const feedback = this.feedbacks.find(f => f.id === id);
|
||||
if (!feedback) return undefined;
|
||||
|
||||
feedback.status = status;
|
||||
feedback.updatedAt = new Date();
|
||||
return feedback;
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
return {
|
||||
total: this.feedbacks.length,
|
||||
byType: this.feedbacks.reduce((acc, f) => {
|
||||
acc[f.type] = (acc[f.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
byStatus: this.feedbacks.reduce((acc, f) => {
|
||||
acc[f.status] = (acc[f.status] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
};
|
||||
}
|
||||
}
|
||||
42
src/knowledge/knowledge.controller.ts
Normal file
42
src/knowledge/knowledge.controller.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
|
||||
@ApiTags('knowledge')
|
||||
@Controller('knowledge')
|
||||
export class KnowledgeController {
|
||||
@Get('categories')
|
||||
@ApiOperation({ summary: '获取知识分类', description: '获取所有知识库分类' })
|
||||
@ApiResponse({ status: 200, description: '分类列表' })
|
||||
async getCategories() {
|
||||
return [
|
||||
{ id: 'cat_1', name: '公考申论', icon: '📝', count: 48 },
|
||||
{ id: 'cat_2', name: 'AI工具', icon: '🤖', count: 24 },
|
||||
{ id: 'cat_3', name: '编程面试', icon: '💻', count: 72 },
|
||||
];
|
||||
}
|
||||
|
||||
@Get('articles')
|
||||
@ApiOperation({ summary: '获取文章列表', description: '获取知识库文章列表' })
|
||||
@ApiResponse({ status: 200, description: '文章列表' })
|
||||
async getArticles() {
|
||||
return [
|
||||
{ id: 'art_1', title: '申论写作基础', category: 'cat_1', excerpt: '了解申论的基本结构和写作要点...' },
|
||||
{ id: 'art_2', title: 'ChatGPT 入门指南', category: 'cat_2', excerpt: '快速上手 ChatGPT,提升工作效率...' },
|
||||
];
|
||||
}
|
||||
|
||||
@Get('articles/:id')
|
||||
@ApiOperation({ summary: '获取文章详情', description: '获取文章详细内容' })
|
||||
@ApiResponse({ status: 200, description: '文章详情' })
|
||||
@ApiResponse({ status: 404, description: '文章不存在' })
|
||||
async getArticle(@Param('id') id: string) {
|
||||
return {
|
||||
id,
|
||||
title: '申论写作基础',
|
||||
content: '申论是公务员考试的核心科目...',
|
||||
category: '公考申论',
|
||||
tags: ['申论', '写作', '备考'],
|
||||
createdAt: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
7
src/knowledge/knowledge.module.ts
Normal file
7
src/knowledge/knowledge.module.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { KnowledgeController } from './knowledge.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [KnowledgeController],
|
||||
})
|
||||
export class KnowledgeModule {}
|
||||
131
src/learning/entities/learning.entity.ts
Normal file
131
src/learning/entities/learning.entity.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class LearningPath {
|
||||
@ApiProperty({ description: '学习路径 ID', example: 'path_gongkao' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '学习路径标题', example: '公考申论备考' })
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: '学习路径描述', example: '系统学习申论写作技巧,提升公文写作能力' })
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ description: '分类', example: 'exam', enum: ['exam', 'skill', 'interview'] })
|
||||
category: 'exam' | 'skill' | 'interview';
|
||||
|
||||
@ApiProperty({ description: '难度级别', example: 'intermediate', enum: ['beginner', 'intermediate', 'advanced'] })
|
||||
difficulty: 'beginner' | 'intermediate' | 'advanced';
|
||||
|
||||
@ApiProperty({ description: '预计学习时长(分钟)', example: 2400 })
|
||||
estimatedMinutes: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '封面图片 URL' })
|
||||
coverImage?: string;
|
||||
|
||||
@ApiProperty({ description: '总课时数', example: 48 })
|
||||
totalLessons: number;
|
||||
|
||||
@ApiProperty({ description: '已完成课时数', example: 0 })
|
||||
completedLessons: number;
|
||||
|
||||
@ApiProperty({ description: '创建时间' })
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class Course {
|
||||
@ApiProperty({ description: '课程 ID', example: 'course_gk_1' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '所属学习路径 ID', example: 'path_gongkao' })
|
||||
pathId: string;
|
||||
|
||||
@ApiProperty({ description: '课程标题', example: '申论基础认知' })
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: '课程描述', example: '了解申论考试的性质与要求' })
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ description: '显示顺序', example: 1 })
|
||||
order: number;
|
||||
|
||||
@ApiProperty({ description: '课时列表', type: () => Lesson })
|
||||
lessons: Lesson[];
|
||||
}
|
||||
|
||||
export class Lesson {
|
||||
@ApiProperty({ description: '课时 ID', example: 'lesson_gk_1_1' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '所属课程 ID', example: 'course_gk_1' })
|
||||
courseId: string;
|
||||
|
||||
@ApiProperty({ description: '课时标题', example: '申论是什么' })
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: '课时内容', example: '申论是公务员考试的核心科目...' })
|
||||
content: string;
|
||||
|
||||
@ApiProperty({ description: '课时类型', example: 'reading', enum: ['reading', 'practice', 'quiz'] })
|
||||
type: 'reading' | 'practice' | 'quiz';
|
||||
|
||||
@ApiProperty({ description: '预计时长(分钟)', example: 15 })
|
||||
duration: number;
|
||||
|
||||
@ApiProperty({ description: '显示顺序', example: 1 })
|
||||
order: number;
|
||||
}
|
||||
|
||||
export class LearningRecord {
|
||||
@ApiProperty({ description: '记录 ID', example: 'rec_123' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456' })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: '学习路径 ID', example: 'path_gongkao' })
|
||||
pathId: string;
|
||||
|
||||
@ApiProperty({ description: '课程 ID', example: 'course_gk_1' })
|
||||
courseId: string;
|
||||
|
||||
@ApiProperty({ description: '课时 ID', example: 'lesson_gk_1_1' })
|
||||
lessonId: string;
|
||||
|
||||
@ApiProperty({ description: '学习状态', example: 'in_progress', enum: ['not_started', 'in_progress', 'completed'] })
|
||||
status: 'not_started' | 'in_progress' | 'completed';
|
||||
|
||||
@ApiProperty({ description: '进度百分比', example: 50 })
|
||||
progress: number;
|
||||
|
||||
@ApiProperty({ description: '开始时间' })
|
||||
startedAt: Date;
|
||||
|
||||
@ApiPropertyOptional({ description: '完成时间' })
|
||||
completedAt?: Date;
|
||||
}
|
||||
|
||||
export class ReviewTask {
|
||||
@ApiProperty({ description: '复习任务 ID', example: 'review_123' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456' })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: '课时 ID', example: 'lesson_gk_1_1' })
|
||||
lessonId: string;
|
||||
|
||||
@ApiProperty({ description: '课时标题', example: '申论是什么' })
|
||||
lessonTitle: string;
|
||||
|
||||
@ApiProperty({ description: '复习截止日期' })
|
||||
dueDate: Date;
|
||||
|
||||
@ApiProperty({ description: '复习间隔(天)', example: 1 })
|
||||
interval: number;
|
||||
|
||||
@ApiProperty({ description: '间隔重复难易因子', example: 2.5 })
|
||||
easeFactor: number;
|
||||
|
||||
@ApiProperty({ description: '下次复习日期' })
|
||||
nextReviewDate: Date;
|
||||
}
|
||||
85
src/learning/learning.controller.ts
Normal file
85
src/learning/learning.controller.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { LearningService } from './learning.service';
|
||||
|
||||
@ApiTags('learning')
|
||||
@Controller('learning')
|
||||
export class LearningController {
|
||||
constructor(private readonly learningService: LearningService) {}
|
||||
|
||||
@Get('paths')
|
||||
@ApiOperation({ summary: '获取学习路径列表', description: '获取所有可用的学习路径' })
|
||||
@ApiResponse({ status: 200, description: '学习路径列表' })
|
||||
async getPaths() {
|
||||
return this.learningService.getAllPaths();
|
||||
}
|
||||
|
||||
@Get('paths/:id')
|
||||
@ApiOperation({ summary: '获取学习路径详情', description: '获取指定学习路径的详细信息' })
|
||||
@ApiParam({ name: 'id', description: '学习路径 ID', example: 'path_gongkao' })
|
||||
@ApiResponse({ status: 200, description: '找到学习路径' })
|
||||
@ApiResponse({ status: 404, description: '学习路径不存在' })
|
||||
async getPath(@Param('id') id: string) {
|
||||
return this.learningService.getPathById(id);
|
||||
}
|
||||
|
||||
@Get('paths/:id/courses')
|
||||
@ApiOperation({ summary: '获取路径下的课程', description: '获取指定学习路径的所有课程' })
|
||||
@ApiParam({ name: 'id', description: '学习路径 ID', example: 'path_gongkao' })
|
||||
@ApiResponse({ status: 200, description: '课程列表' })
|
||||
async getCourses(@Param('id') pathId: string) {
|
||||
return this.learningService.getCoursesByPath(pathId);
|
||||
}
|
||||
|
||||
@Get('courses/:id')
|
||||
@ApiOperation({ summary: '获取课程详情', description: '获取指定课程的详细信息,包含课时' })
|
||||
@ApiParam({ name: 'id', description: '课程 ID', example: 'course_gk_1' })
|
||||
@ApiResponse({ status: 200, description: '找到课程' })
|
||||
@ApiResponse({ status: 404, description: '课程不存在' })
|
||||
async getCourse(@Param('id') id: string) {
|
||||
return this.learningService.getCourseById(id);
|
||||
}
|
||||
|
||||
@Get('lessons/:id')
|
||||
@ApiOperation({ summary: '获取课时详情', description: '获取指定课时的详细信息' })
|
||||
@ApiParam({ name: 'id', description: '课时 ID', example: 'lesson_gk_1_1' })
|
||||
@ApiResponse({ status: 200, description: '找到课时' })
|
||||
@ApiResponse({ status: 404, description: '课时不存在' })
|
||||
async getLesson(@Param('id') id: string) {
|
||||
return this.learningService.getLessonById(id);
|
||||
}
|
||||
|
||||
@Get('records')
|
||||
@ApiOperation({ summary: '获取学习记录', description: '获取用户的学习进度记录' })
|
||||
@ApiQuery({ name: 'userId', description: '用户 ID', example: 'user_123456' })
|
||||
@ApiResponse({ status: 200, description: '学习记录列表' })
|
||||
async getRecords(@Query('userId') userId: string) {
|
||||
return this.learningService.getUserRecords(userId);
|
||||
}
|
||||
|
||||
@Patch('progress')
|
||||
@ApiOperation({ summary: '更新学习进度', description: '更新指定课时的学习进度' })
|
||||
@ApiResponse({ status: 200, description: '进度已更新' })
|
||||
@ApiResponse({ status: 404, description: '课时不存在' })
|
||||
async updateProgress(@Body() body: { userId: string; lessonId: string; progress: number }) {
|
||||
const record = await this.learningService.updateProgress(body.userId, body.lessonId, body.progress);
|
||||
return { success: true, data: record };
|
||||
}
|
||||
|
||||
@Get('review')
|
||||
@ApiOperation({ summary: '获取复习任务', description: '获取用户的间隔重复复习任务' })
|
||||
@ApiQuery({ name: 'userId', description: '用户 ID', example: 'user_123456' })
|
||||
@ApiResponse({ status: 200, description: '复习任务列表' })
|
||||
async getReviewTasks(@Query('userId') userId: string) {
|
||||
return this.learningService.getReviewTasks(userId);
|
||||
}
|
||||
|
||||
@Post('review')
|
||||
@ApiOperation({ summary: '创建复习任务', description: '创建一个新的间隔重复复习任务' })
|
||||
@ApiResponse({ status: 201, description: '复习任务已创建' })
|
||||
@ApiResponse({ status: 404, description: '课时不存在' })
|
||||
async createReviewTask(@Body() body: { userId: string; lessonId: string }) {
|
||||
const task = await this.learningService.createReviewTask(body.userId, body.lessonId);
|
||||
return { success: true, data: task };
|
||||
}
|
||||
}
|
||||
10
src/learning/learning.module.ts
Normal file
10
src/learning/learning.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LearningController } from './learning.controller';
|
||||
import { LearningService } from './learning.service';
|
||||
|
||||
@Module({
|
||||
controllers: [LearningController],
|
||||
providers: [LearningService],
|
||||
exports: [LearningService],
|
||||
})
|
||||
export class LearningModule {}
|
||||
172
src/learning/learning.service.ts
Normal file
172
src/learning/learning.service.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { LearningPath, Course, LearningRecord, ReviewTask } from './entities/learning.entity';
|
||||
|
||||
@Injectable()
|
||||
export class LearningService {
|
||||
private paths: LearningPath[] = [
|
||||
{
|
||||
id: 'path_gongkao',
|
||||
title: '公考申论备考',
|
||||
description: '系统学习申论写作技巧,提升公文写作能力',
|
||||
category: 'exam',
|
||||
difficulty: 'intermediate',
|
||||
estimatedMinutes: 2400,
|
||||
totalLessons: 48,
|
||||
completedLessons: 0,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'path_ai_tools',
|
||||
title: 'AI 工具实战',
|
||||
description: '掌握主流 AI 工具使用方法,提升工作效率',
|
||||
category: 'skill',
|
||||
difficulty: 'beginner',
|
||||
estimatedMinutes: 1200,
|
||||
totalLessons: 24,
|
||||
completedLessons: 0,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'path_interview',
|
||||
title: '程序员面试冲刺',
|
||||
description: '算法与系统设计面试题精讲',
|
||||
category: 'interview',
|
||||
difficulty: 'advanced',
|
||||
estimatedMinutes: 3600,
|
||||
totalLessons: 72,
|
||||
completedLessons: 0,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
private courses: Course[] = [
|
||||
{
|
||||
id: 'course_gk_1',
|
||||
pathId: 'path_gongkao',
|
||||
title: '申论基础认知',
|
||||
description: '了解申论考试的性质与要求',
|
||||
order: 1,
|
||||
lessons: [
|
||||
{ id: 'lesson_gk_1_1', courseId: 'course_gk_1', title: '申论是什么', content: '申论是公务员考试的核心科目...', type: 'reading', duration: 15, order: 1 },
|
||||
{ id: 'lesson_gk_1_2', courseId: 'course_gk_1', title: '考试形式解析', content: '申论考试通常包含...', type: 'reading', duration: 20, order: 2 },
|
||||
{ id: 'lesson_gk_1_3', courseId: 'course_gk_1', title: '评分标准', content: '阅卷老师关注的重点...', type: 'reading', duration: 15, order: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'course_ai_1',
|
||||
pathId: 'path_ai_tools',
|
||||
title: 'ChatGPT 入门',
|
||||
description: '学会使用 ChatGPT 提升日常效率',
|
||||
order: 1,
|
||||
lessons: [
|
||||
{ id: 'lesson_ai_1_1', courseId: 'course_ai_1', title: '注册与基础操作', content: '如何创建账号并开始对话...', type: 'reading', duration: 10, order: 1 },
|
||||
{ id: 'lesson_ai_1_2', courseId: 'course_ai_1', title: '有效提问技巧', content: '如何写出好的 Prompt...', type: 'practice', duration: 20, order: 2 },
|
||||
{ id: 'lesson_ai_1_3', courseId: 'course_ai_1', title: '实战案例练习', content: '用 ChatGPT 写一封邮件...', type: 'practice', duration: 25, order: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'course_int_1',
|
||||
pathId: 'path_interview',
|
||||
title: '算法基础',
|
||||
description: '面试必备算法知识点回顾',
|
||||
order: 1,
|
||||
lessons: [
|
||||
{ id: 'lesson_int_1_1', courseId: 'course_int_1', title: '时间空间复杂度', content: '如何分析算法效率...', type: 'reading', duration: 20, order: 1 },
|
||||
{ id: 'lesson_int_1_2', courseId: 'course_int_1', title: '数组与链表', content: '基础数据结构回顾...', type: 'reading', duration: 30, order: 2 },
|
||||
{ id: 'lesson_int_1_3', courseId: 'course_int_1', title: '栈与队列', content: 'LIFO 与 FIFO 的应用...', type: 'reading', duration: 25, order: 3 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
private records: Map<string, LearningRecord[]> = new Map();
|
||||
private reviewTasks: Map<string, ReviewTask[]> = new Map();
|
||||
|
||||
async getAllPaths(): Promise<LearningPath[]> {
|
||||
return this.paths;
|
||||
}
|
||||
|
||||
async getPathById(id: string): Promise<LearningPath | undefined> {
|
||||
return this.paths.find(p => p.id === id);
|
||||
}
|
||||
|
||||
async getCoursesByPath(pathId: string): Promise<Course[]> {
|
||||
return this.courses.filter(c => c.pathId === pathId);
|
||||
}
|
||||
|
||||
async getCourseById(id: string): Promise<Course | undefined> {
|
||||
return this.courses.find(c => c.id === id);
|
||||
}
|
||||
|
||||
async getLessonById(id: string): Promise<{ course: Course; lesson: any } | undefined> {
|
||||
for (const course of this.courses) {
|
||||
const lesson = course.lessons.find(l => l.id === id);
|
||||
if (lesson) {
|
||||
return { course, lesson };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getUserRecords(userId: string): Promise<LearningRecord[]> {
|
||||
return this.records.get(userId) || [];
|
||||
}
|
||||
|
||||
async updateProgress(userId: string, lessonId: string, progress: number): Promise<LearningRecord> {
|
||||
const { lesson } = await this.getLessonById(lessonId) || {};
|
||||
if (!lesson) throw new Error('Lesson not found');
|
||||
|
||||
const records = this.records.get(userId) || [];
|
||||
const existing = records.find(r => r.lessonId === lessonId);
|
||||
|
||||
if (existing) {
|
||||
existing.progress = progress;
|
||||
existing.status = progress >= 100 ? 'completed' : 'in_progress';
|
||||
if (progress >= 100) existing.completedAt = new Date();
|
||||
return existing;
|
||||
}
|
||||
|
||||
const record: LearningRecord = {
|
||||
id: `rec_${Date.now()}`,
|
||||
userId,
|
||||
pathId: lesson.courseId.split('_')[0] + '_' + lesson.courseId.split('_')[1],
|
||||
courseId: lesson.courseId,
|
||||
lessonId,
|
||||
status: progress >= 100 ? 'completed' : 'in_progress',
|
||||
progress,
|
||||
startedAt: new Date(),
|
||||
completedAt: progress >= 100 ? new Date() : undefined,
|
||||
};
|
||||
|
||||
records.push(record);
|
||||
this.records.set(userId, records);
|
||||
return record;
|
||||
}
|
||||
|
||||
async getReviewTasks(userId: string): Promise<ReviewTask[]> {
|
||||
return this.reviewTasks.get(userId) || [];
|
||||
}
|
||||
|
||||
async createReviewTask(userId: string, lessonId: string): Promise<ReviewTask> {
|
||||
const { lesson } = await this.getLessonById(lessonId) || {};
|
||||
if (!lesson) throw new Error('Lesson not found');
|
||||
|
||||
const tasks = this.reviewTasks.get(userId) || [];
|
||||
const existing = tasks.find(t => t.lessonId === lessonId);
|
||||
if (existing) return existing;
|
||||
|
||||
const task: ReviewTask = {
|
||||
id: `review_${Date.now()}`,
|
||||
userId,
|
||||
lessonId,
|
||||
lessonTitle: lesson.title,
|
||||
dueDate: new Date(),
|
||||
interval: 1,
|
||||
easeFactor: 2.5,
|
||||
nextReviewDate: new Date(Date.now() + 86400000),
|
||||
};
|
||||
|
||||
tasks.push(task);
|
||||
this.reviewTasks.set(userId, tasks);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
104
src/main.ts
Normal file
104
src/main.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
const SWAGGER_BASIC_AUTH_USER = process.env.SWAGGER_USER || 'admin';
|
||||
const SWAGGER_BASIC_AUTH_PASSWORD = process.env.SWAGGER_PASSWORD || 'change_me';
|
||||
|
||||
function isLocalhost(url: string): boolean {
|
||||
return url.includes('localhost') || url.includes('127.0.0.1');
|
||||
}
|
||||
|
||||
function isSwaggerEnabled(): boolean {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return process.env.ENABLE_SWAGGER === 'true';
|
||||
}
|
||||
return process.env.ENABLE_SWAGGER !== 'false';
|
||||
}
|
||||
|
||||
function needsBasicAuth(): boolean {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return process.env.ENABLE_SWAGGER === 'true';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createBasicAuthMiddleware() {
|
||||
return (req: any, res: any, next: any) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Swagger API Docs"');
|
||||
res.status(401).send('Authentication required');
|
||||
return;
|
||||
}
|
||||
|
||||
const base64Credentials = authHeader.split(' ')[1];
|
||||
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
||||
const [username, password] = credentials.split(':');
|
||||
|
||||
if (username === SWAGGER_BASIC_AUTH_USER && password === SWAGGER_BASIC_AUTH_PASSWORD) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).send('Invalid credentials');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.enableCors({
|
||||
origin: ['https://longde.cloud', 'http://localhost:4321'],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
if (isSwaggerEnabled()) {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('龙de AI 学习产品 API')
|
||||
.setDescription('AI 学习产品后端 API 文档(v0.1),包含用户管理、学习路径、AI 对话、反馈等功能。')
|
||||
.setVersion('0.1.0')
|
||||
.addTag('health', '服务健康检查')
|
||||
.addTag('auth', '用户认证')
|
||||
.addTag('users', '用户管理')
|
||||
.addTag('knowledge', '知识库')
|
||||
.addTag('learning', '学习路径与进度')
|
||||
.addTag('ai', 'AI 分析与对话')
|
||||
.addTag('review', '复习任务')
|
||||
.addTag('feedback', '用户反馈')
|
||||
.addTag('waitlist', '等待名单')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
|
||||
if (needsBasicAuth()) {
|
||||
app.use('/api-docs', createBasicAuthMiddleware() as any);
|
||||
app.use('/api-docs-json', createBasicAuthMiddleware() as any);
|
||||
}
|
||||
|
||||
SwaggerModule.setup('api-docs', app, document, {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
},
|
||||
customCss: '.swagger-ui .topbar { display: none }',
|
||||
customSiteTitle: '龙de API 文档',
|
||||
});
|
||||
|
||||
app.getHttpAdapter().get('/api-docs-json', (_req: any, res: any) => {
|
||||
res.json(document);
|
||||
});
|
||||
|
||||
console.log('[Swagger] API 文档已启用');
|
||||
if (needsBasicAuth()) {
|
||||
console.log(`[Swagger] Basic Auth 已启用 (${SWAGGER_BASIC_AUTH_USER})`);
|
||||
}
|
||||
} else {
|
||||
console.log('[Swagger] API 文档已禁用');
|
||||
}
|
||||
|
||||
const port = process.env.PORT ?? 3000;
|
||||
await app.listen(port);
|
||||
console.log(`[API] Server running on http://localhost:${port}`);
|
||||
}
|
||||
bootstrap();
|
||||
37
src/users/dto/create-user.dto.ts
Normal file
37
src/users/dto/create-user.dto.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiPropertyOptional({ description: '用户昵称', example: '学习者' })
|
||||
nickname?: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱地址', example: 'user@example.com' })
|
||||
email: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '设备类型', example: 'iPhone', enum: ['iPhone', 'Android', 'iPad', 'Mac'] })
|
||||
device?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '注册来源', example: 'waitlist' })
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export class UserPreferences {
|
||||
@ApiPropertyOptional({ description: '每日学习目标(分钟)', example: 30 })
|
||||
dailyGoal?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '提醒时间 HH:mm 格式', example: '09:00' })
|
||||
reminderTime?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '界面主题', example: 'auto', enum: ['light', 'dark', 'auto'] })
|
||||
theme?: 'light' | 'dark' | 'auto';
|
||||
}
|
||||
|
||||
export class UpdateUserDto {
|
||||
@ApiPropertyOptional({ description: '用户昵称', example: '新昵称' })
|
||||
nickname?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '头像 URL' })
|
||||
avatar?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '用户偏好设置' })
|
||||
preferences?: UserPreferences;
|
||||
}
|
||||
64
src/users/entities/user.entity.ts
Normal file
64
src/users/entities/user.entity.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class User {
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456_abc' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱地址', example: 'user@example.com' })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ description: '用户昵称', example: '学习者' })
|
||||
nickname: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '头像 URL' })
|
||||
avatar?: string;
|
||||
|
||||
@ApiProperty({ description: '设备类型', example: 'iPhone' })
|
||||
device: string;
|
||||
|
||||
@ApiProperty({ description: '注册来源', example: 'api' })
|
||||
source: string;
|
||||
|
||||
@ApiProperty({ description: '用户偏好设置' })
|
||||
preferences: {
|
||||
dailyGoal: number;
|
||||
reminderTime: string;
|
||||
theme: 'light' | 'dark' | 'auto';
|
||||
};
|
||||
|
||||
@ApiProperty({ description: '账户创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ description: '最后更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export class UserProfile {
|
||||
@ApiProperty({ description: '用户 ID', example: 'user_123456_abc' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱地址', example: 'user@example.com' })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ description: '用户昵称', example: '学习者' })
|
||||
nickname: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '头像 URL' })
|
||||
avatar?: string;
|
||||
|
||||
@ApiProperty({ description: '设备类型', example: 'iPhone' })
|
||||
device: string;
|
||||
|
||||
@ApiProperty({ description: '注册来源', example: 'api' })
|
||||
source: string;
|
||||
|
||||
@ApiProperty({ description: '用户统计数据' })
|
||||
stats: {
|
||||
totalLearningDays: number;
|
||||
completedCourses: number;
|
||||
totalMinutes: number;
|
||||
};
|
||||
|
||||
@ApiProperty({ description: '账户创建时间' })
|
||||
createdAt: Date;
|
||||
}
|
||||
60
src/users/users.controller.ts
Normal file
60
src/users/users.controller.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserDto, UpdateUserDto } from './dto/create-user.dto';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '创建用户', description: '在系统中注册一个新用户' })
|
||||
@ApiResponse({ status: 201, description: '用户创建成功' })
|
||||
@ApiResponse({ status: 400, description: '邮箱无效或缺少必填字段' })
|
||||
async create(@Body() createUserDto: CreateUserDto) {
|
||||
const user = await this.usersService.create(createUserDto);
|
||||
return {
|
||||
success: true,
|
||||
data: user,
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '获取用户信息', description: '通过用户 ID 获取用户信息' })
|
||||
@ApiParam({ name: 'id', description: '用户 ID', example: 'user_123456_abc' })
|
||||
@ApiResponse({ status: 200, description: '找到用户' })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.usersService.findById(id);
|
||||
}
|
||||
|
||||
@Get(':id/profile')
|
||||
@ApiOperation({ summary: '获取用户资料', description: '获取详细的用户资料,包含统计数据' })
|
||||
@ApiParam({ name: 'id', description: '用户 ID', example: 'user_123456_abc' })
|
||||
@ApiResponse({ status: 200, description: '获取用户资料成功' })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
async getProfile(@Param('id') id: string) {
|
||||
return this.usersService.getProfile(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: '更新用户信息', description: '更新用户信息和偏好设置' })
|
||||
@ApiParam({ name: 'id', description: '用户 ID', example: 'user_123456_abc' })
|
||||
@ApiResponse({ status: 200, description: '用户更新成功' })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||
const user = await this.usersService.update(id, updateUserDto);
|
||||
return {
|
||||
success: true,
|
||||
data: user,
|
||||
};
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取用户列表', description: '获取所有已注册用户列表' })
|
||||
@ApiResponse({ status: 200, description: '用户列表' })
|
||||
async list() {
|
||||
return this.usersService.list();
|
||||
}
|
||||
}
|
||||
10
src/users/users.module.ts
Normal file
10
src/users/users.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
80
src/users/users.service.ts
Normal file
80
src/users/users.service.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateUserDto, UpdateUserDto } from './dto/create-user.dto';
|
||||
import { User, UserProfile } from './entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
private users: Map<string, User> = new Map();
|
||||
|
||||
async create(createUserDto: CreateUserDto): Promise<User> {
|
||||
const user: User = {
|
||||
id: `user_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
||||
email: createUserDto.email,
|
||||
nickname: createUserDto.nickname || createUserDto.email.split('@')[0],
|
||||
device: createUserDto.device || 'unknown',
|
||||
source: createUserDto.source || 'api',
|
||||
preferences: {
|
||||
dailyGoal: 30,
|
||||
reminderTime: '09:00',
|
||||
theme: 'auto',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
this.users.set(user.id, user);
|
||||
console.log('[Users] New user created:', user.email);
|
||||
return user;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User | undefined> {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<User | undefined> {
|
||||
return Array.from(this.users.values()).find(u => u.email === email);
|
||||
}
|
||||
|
||||
async update(id: string, updateUserDto: UpdateUserDto): Promise<User | undefined> {
|
||||
const user = this.users.get(id);
|
||||
if (!user) return undefined;
|
||||
|
||||
const updated: User = {
|
||||
...user,
|
||||
nickname: updateUserDto.nickname ?? user.nickname,
|
||||
avatar: updateUserDto.avatar ?? user.avatar,
|
||||
preferences: {
|
||||
...user.preferences,
|
||||
...updateUserDto.preferences,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
this.users.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async getProfile(id: string): Promise<UserProfile | undefined> {
|
||||
const user = this.users.get(id);
|
||||
if (!user) return undefined;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
device: user.device,
|
||||
source: user.source,
|
||||
stats: {
|
||||
totalLearningDays: Math.floor(Math.random() * 30),
|
||||
completedCourses: Math.floor(Math.random() * 5),
|
||||
totalMinutes: Math.floor(Math.random() * 500),
|
||||
},
|
||||
createdAt: user.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
async list(): Promise<User[]> {
|
||||
return Array.from(this.users.values());
|
||||
}
|
||||
}
|
||||
21
src/waitlist/dto/create-waitlist.dto.ts
Normal file
21
src/waitlist/dto/create-waitlist.dto.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateWaitlistDto {
|
||||
@ApiPropertyOptional({ description: '用户昵称', example: '小明' })
|
||||
nickname?: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱地址', example: 'user@example.com' })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ description: '使用的设备', example: ['iPhone', 'Mac'], enum: ['iPhone', 'Android', 'iPad', 'Mac'] })
|
||||
devices: string[];
|
||||
|
||||
@ApiProperty({ description: '感兴趣的学习方向', example: ['公考申论'], enum: ['公考申论', 'AI工具学习', '程序员面试', '其他'] })
|
||||
interests: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: '当前最大的痛点', example: '做题没有反馈,不知道自己掌握程度如何' })
|
||||
painpoint?: string;
|
||||
|
||||
@ApiProperty({ description: '是否愿意参加内测', example: true })
|
||||
willingBeta: boolean;
|
||||
}
|
||||
43
src/waitlist/waitlist.controller.ts
Normal file
43
src/waitlist/waitlist.controller.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Controller, Post, Body, Get, HttpCode, HttpStatus, ValidationPipe } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { WaitlistService } from './waitlist.service';
|
||||
import { CreateWaitlistDto } from './dto/create-waitlist.dto';
|
||||
|
||||
@ApiTags('waitlist')
|
||||
@Controller('waitlist')
|
||||
export class WaitlistController {
|
||||
constructor(private readonly waitlistService: WaitlistService) {}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: '加入等待名单', description: '提交邮箱加入产品等待名单' })
|
||||
@ApiResponse({ status: 201, description: '成功加入等待名单' })
|
||||
@ApiResponse({ status: 400, description: '邮箱无效或缺少必填字段' })
|
||||
@ApiResponse({ status: 409, description: '该邮箱已报名' })
|
||||
async create(@Body(new ValidationPipe({ transform: true })) createWaitlistDto: CreateWaitlistDto) {
|
||||
const entry = await this.waitlistService.create(createWaitlistDto);
|
||||
return {
|
||||
success: true,
|
||||
message: '已成功加入等待名单',
|
||||
data: {
|
||||
id: entry.id,
|
||||
email: entry.email,
|
||||
createdAt: entry.createdAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取等待名单', description: '获取所有等待名单报名记录(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '等待名单列表' })
|
||||
async findAll() {
|
||||
return this.waitlistService.findAll();
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: '获取报名统计', description: '获取等待名单的聚合统计数据' })
|
||||
@ApiResponse({ status: 200, description: '报名统计数据,包含设备分布和兴趣方向' })
|
||||
async getStats() {
|
||||
return this.waitlistService.getStats();
|
||||
}
|
||||
}
|
||||
9
src/waitlist/waitlist.module.ts
Normal file
9
src/waitlist/waitlist.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WaitlistController } from './waitlist.controller';
|
||||
import { WaitlistService } from './waitlist.service';
|
||||
|
||||
@Module({
|
||||
controllers: [WaitlistController],
|
||||
providers: [WaitlistService],
|
||||
})
|
||||
export class WaitlistModule {}
|
||||
72
src/waitlist/waitlist.service.ts
Normal file
72
src/waitlist/waitlist.service.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateWaitlistDto } from './dto/create-waitlist.dto';
|
||||
|
||||
export interface WaitlistEntry {
|
||||
id: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
devices: string[];
|
||||
interests: string[];
|
||||
painpoint: string;
|
||||
willingBeta: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WaitlistService {
|
||||
private entries: WaitlistEntry[] = [];
|
||||
|
||||
async create(createWaitlistDto: CreateWaitlistDto): Promise<WaitlistEntry> {
|
||||
const entry: WaitlistEntry = {
|
||||
id: `wl_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
||||
nickname: createWaitlistDto.nickname || '',
|
||||
email: createWaitlistDto.email,
|
||||
devices: createWaitlistDto.devices || [],
|
||||
interests: createWaitlistDto.interests || [],
|
||||
painpoint: createWaitlistDto.painpoint || '',
|
||||
willingBeta: createWaitlistDto.willingBeta || false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
this.entries.push(entry);
|
||||
console.log('[Waitlist] New entry added:', entry.email);
|
||||
return entry;
|
||||
}
|
||||
|
||||
async findAll(): Promise<WaitlistEntry[]> {
|
||||
return this.entries;
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<WaitlistEntry | undefined> {
|
||||
return this.entries.find(entry => entry.email === email);
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
return {
|
||||
total: this.entries.length,
|
||||
betaUsers: this.entries.filter(e => e.willingBeta).length,
|
||||
byDevice: this.getDeviceStats(),
|
||||
byInterest: this.getInterestStats(),
|
||||
};
|
||||
}
|
||||
|
||||
private getDeviceStats() {
|
||||
const stats: Record<string, number> = {};
|
||||
this.entries.forEach(entry => {
|
||||
entry.devices.forEach(device => {
|
||||
stats[device] = (stats[device] || 0) + 1;
|
||||
});
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
|
||||
private getInterestStats() {
|
||||
const stats: Record<string, number> = {};
|
||||
this.entries.forEach(entry => {
|
||||
entry.interests.forEach(interest => {
|
||||
stats[interest] = (stats[interest] || 0) + 1;
|
||||
});
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
29
test/app.e2e-spec.ts
Normal file
29
test/app.e2e-spec.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
9
test/jest-e2e.json
Normal file
9
test/jest-e2e.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolvePackageJsonExports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user