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:
WangDL 2026-05-04 16:09:01 +08:00
commit bd44b7e138
43 changed files with 12031 additions and 0 deletions

18
.env.example Normal file
View 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
View 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
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

175
README.md Normal file
View 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 APIOpenAI / Claude
- [ ] 添加数据库持久化
- [ ] 实现 JWT 认证中间件
- [ ] 添加 Redis 缓存
- [ ] iOS SDK 集成文档

35
eslint.config.mjs Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

75
package.json Normal file
View 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
View 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
View 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
View 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) || [];
}
}

View 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[];
}

View 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
View 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
View 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
View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View 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
View File

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
@Module({
controllers: [AuthController],
})
export class AuthModule {}

View 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;
}

View 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;
}

View 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 };
}
}

View 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 {}

View 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>),
};
}
}

View 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(),
};
}
}

View File

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { KnowledgeController } from './knowledge.controller';
@Module({
controllers: [KnowledgeController],
})
export class KnowledgeModule {}

View 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;
}

View 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 };
}
}

View 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 {}

View 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
View 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();

View 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;
}

View 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;
}

View 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
View 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 {}

View 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());
}
}

View 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;
}

View 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();
}
}

View 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 {}

View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

25
tsconfig.json Normal file
View 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
}
}