chore: 初始化 monorepo 项目脚手架
- 配置 pnpm workspace + Turborepo - 创建 Next.js 15 前端应用 (apps/web) - 创建 NestJS 10 后端应用 (apps/api) - 集成 Prisma ORM + Swagger + JWT 认证 - 添加共享包 (packages/shared, eslint-config, typescript-config) - 添加项目文档 (README, CLAUDE.md, docs/design.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
out
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Prisma
|
||||
prisma/*.db
|
||||
prisma/*.db-journal
|
||||
|
||||
# Misc
|
||||
*.tsbuildinfo
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.next
|
||||
dist
|
||||
.turbo
|
||||
coverage
|
||||
pnpm-lock.yaml
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100
|
||||
}
|
||||
65
CLAUDE.md
Normal file
65
CLAUDE.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Seclusion 是一个基于 Next.js + NestJS 的 Monorepo 项目模板,使用 pnpm workspace + Turborepo 管理。
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# 开发(同时启动前后端)
|
||||
pnpm dev
|
||||
|
||||
# 构建
|
||||
pnpm build
|
||||
|
||||
# 代码检查
|
||||
pnpm lint
|
||||
pnpm format # 格式化
|
||||
pnpm format:check # 检查格式
|
||||
|
||||
# 测试
|
||||
pnpm test
|
||||
cd apps/api && pnpm test:watch # 单个测试文件监听
|
||||
|
||||
# 数据库
|
||||
pnpm db:generate # 生成 Prisma Client
|
||||
pnpm db:push # 推送 schema 到数据库
|
||||
pnpm db:migrate # 运行迁移
|
||||
cd apps/api && pnpm db:studio # 打开 Prisma Studio
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo 结构
|
||||
|
||||
- **apps/web** - Next.js 15 前端 (端口 3000)
|
||||
- **apps/api** - NestJS 10 后端 (端口 4000,API 文档: /api/docs)
|
||||
- **packages/shared** - 共享类型定义和工具函数
|
||||
- **packages/eslint-config** - 共享 ESLint 配置
|
||||
- **packages/typescript-config** - 共享 TypeScript 配置
|
||||
|
||||
### 后端模块 (apps/api)
|
||||
|
||||
NestJS 采用模块化架构:
|
||||
- **PrismaModule** - 全局数据库服务,其他模块通过依赖注入使用
|
||||
- **AuthModule** - JWT 认证(注册、登录、token 验证)
|
||||
- **UserModule** - 用户 CRUD
|
||||
|
||||
认证流程:使用 `@Public()` 装饰器标记公开接口,其他接口需要 JWT Bearer Token。
|
||||
|
||||
### 共享包使用
|
||||
|
||||
```typescript
|
||||
import type { User, ApiResponse } from '@seclusion/shared';
|
||||
import { formatDate, generateId } from '@seclusion/shared';
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
- `apps/api/prisma/schema.prisma` - 数据库模型定义
|
||||
- `apps/api/.env` - 后端环境变量 (DATABASE_URL, JWT_SECRET)
|
||||
- `apps/web/.env.local` - 前端环境变量 (NEXT_PUBLIC_API_URL)
|
||||
- `turbo.json` - Turborepo 任务依赖配置
|
||||
116
README.md
Normal file
116
README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Seclusion
|
||||
|
||||
基于 Next.js + NestJS 的 Monorepo 项目模板,使用 pnpm workspace + Turborepo 管理。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端**: Next.js 15 + React 19 + TypeScript
|
||||
- **后端**: NestJS 10 + Prisma + Swagger + JWT
|
||||
- **工具链**: pnpm + Turborepo + ESLint + Prettier
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
seclusion/
|
||||
├── apps/
|
||||
│ ├── web/ # Next.js 前端应用
|
||||
│ └── api/ # NestJS 后端应用
|
||||
├── packages/
|
||||
│ ├── shared/ # 共享代码(类型、工具函数)
|
||||
│ ├── eslint-config/ # ESLint 配置
|
||||
│ └── typescript-config/ # TypeScript 配置
|
||||
├── package.json
|
||||
├── pnpm-workspace.yaml
|
||||
└── turbo.json
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js >= 20.0.0
|
||||
- pnpm >= 9.0.0
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 配置环境变量
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
cp apps/api/.env.example apps/api/.env
|
||||
|
||||
# 前端
|
||||
cp apps/web/.env.example apps/web/.env.local
|
||||
```
|
||||
|
||||
### 初始化数据库
|
||||
|
||||
```bash
|
||||
# 生成 Prisma Client
|
||||
pnpm db:generate
|
||||
|
||||
# 推送数据库 schema
|
||||
pnpm db:push
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 同时启动前端和后端
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- 前端: http://localhost:3000
|
||||
- 后端: http://localhost:4000
|
||||
- API 文档: http://localhost:4000/api/docs
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
pnpm dev # 启动所有应用的开发服务器
|
||||
|
||||
# 构建
|
||||
pnpm build # 构建所有应用
|
||||
|
||||
# 代码质量
|
||||
pnpm lint # 运行 ESLint
|
||||
pnpm format # 格式化代码
|
||||
|
||||
# 数据库
|
||||
pnpm db:generate # 生成 Prisma Client
|
||||
pnpm db:push # 推送 schema 到数据库
|
||||
pnpm db:migrate # 运行数据库迁移
|
||||
|
||||
# 清理
|
||||
pnpm clean # 清理所有构建产物和 node_modules
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 认证相关
|
||||
|
||||
- `POST /auth/register` - 用户注册
|
||||
- `POST /auth/login` - 用户登录
|
||||
- `GET /auth/me` - 获取当前用户信息(需认证)
|
||||
|
||||
### 用户管理
|
||||
|
||||
- `GET /users` - 获取所有用户(需认证)
|
||||
- `GET /users/:id` - 获取指定用户(需认证)
|
||||
- `PATCH /users/:id` - 更新用户信息(需认证)
|
||||
- `DELETE /users/:id` - 删除用户(需认证)
|
||||
|
||||
## 共享包使用
|
||||
|
||||
```typescript
|
||||
// 导入类型
|
||||
import type { User, ApiResponse } from '@seclusion/shared';
|
||||
|
||||
// 导入工具函数
|
||||
import { formatDate, generateId } from '@seclusion/shared';
|
||||
```
|
||||
10
apps/api/.env.example
Normal file
10
apps/api/.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# 数据库配置
|
||||
DATABASE_URL="file:./dev.db"
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET="your-super-secret-key-change-in-production"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
|
||||
# 应用配置
|
||||
PORT=4000
|
||||
NODE_ENV=development
|
||||
8
apps/api/.eslintrc.js
Normal file
8
apps/api/.eslintrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['@seclusion/eslint-config/nest'],
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
};
|
||||
8
apps/api/nest-cli.json
Normal file
8
apps/api/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
58
apps/api/package.json
Normal file
58
apps/api/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@seclusion/api",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nest start --watch",
|
||||
"build": "nest build",
|
||||
"start": "node dist/main",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"clean": "rm -rf dist .turbo node_modules",
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:studio": "prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.4.15",
|
||||
"@nestjs/swagger": "^8.1.0",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"@seclusion/shared": "workspace:*",
|
||||
"bcrypt": "^5.1.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.9",
|
||||
"@nestjs/schematics": "^10.2.3",
|
||||
"@nestjs/testing": "^10.4.15",
|
||||
"@seclusion/eslint-config": "workspace:*",
|
||||
"@seclusion/typescript-config": "workspace:*",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"eslint": "^8.57.0",
|
||||
"jest": "^29.7.0",
|
||||
"prisma": "^6.1.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
BIN
apps/api/prisma/dev.db
Normal file
BIN
apps/api/prisma/dev.db
Normal file
Binary file not shown.
19
apps/api/prisma/schema.prisma
Normal file
19
apps/api/prisma/schema.prisma
Normal file
@@ -0,0 +1,19 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
password String
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
16
apps/api/src/app.controller.ts
Normal file
16
apps/api/src/app.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@ApiTags('健康检查')
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get('health')
|
||||
@ApiOperation({ summary: '健康检查' })
|
||||
healthCheck() {
|
||||
return this.appService.healthCheck();
|
||||
}
|
||||
}
|
||||
23
apps/api/src/app.module.ts
Normal file
23
apps/api/src/app.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
PrismaModule,
|
||||
AuthModule,
|
||||
UserModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
11
apps/api/src/app.service.ts
Normal file
11
apps/api/src/app.service.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
healthCheck() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
38
apps/api/src/auth/auth.controller.ts
Normal file
38
apps/api/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Controller, Post, Body, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { RegisterDto, LoginDto } from './dto/auth.dto';
|
||||
import { Public } from './decorators/public.decorator';
|
||||
import { CurrentUser } from './decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
|
||||
@ApiTags('认证')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '用户注册' })
|
||||
@ApiBody({ type: RegisterDto })
|
||||
register(@Body() dto: RegisterDto) {
|
||||
return this.authService.register(dto);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '用户登录' })
|
||||
@ApiBody({ type: LoginDto })
|
||||
login(@Body() dto: LoginDto) {
|
||||
return this.authService.login(dto);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: '获取当前用户信息' })
|
||||
getProfile(@CurrentUser() user: { id: string; email: string; name: string }) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
27
apps/api/src/auth/auth.module.ts
Normal file
27
apps/api/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET', 'secret'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '7d'),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
94
apps/api/src/auth/auth.service.ts
Normal file
94
apps/api/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { RegisterDto, LoginDto } from './dto/auth.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async register(dto: RegisterDto) {
|
||||
// 检查邮箱是否已存在
|
||||
const existingUser = await this.prisma.user.findUnique({
|
||||
where: { email: dto.email },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new ConflictException('该邮箱已被注册');
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
||||
|
||||
// 创建用户
|
||||
const user = await this.prisma.user.create({
|
||||
data: {
|
||||
email: dto.email,
|
||||
password: hashedPassword,
|
||||
name: dto.name,
|
||||
},
|
||||
});
|
||||
|
||||
// 生成 token
|
||||
const tokens = await this.generateTokens(user.id, user.email);
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
},
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
async login(dto: LoginDto) {
|
||||
// 查找用户
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { email: dto.email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('邮箱或密码错误');
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(dto.password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('邮箱或密码错误');
|
||||
}
|
||||
|
||||
// 生成 token
|
||||
const tokens = await this.generateTokens(user.id, user.email);
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
},
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
private async generateTokens(userId: string, email: string) {
|
||||
const payload = { sub: userId, email };
|
||||
|
||||
const accessToken = await this.jwtService.signAsync(payload);
|
||||
|
||||
const refreshToken = await this.jwtService.signAsync(payload, {
|
||||
expiresIn: '30d',
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
10
apps/api/src/auth/decorators/current-user.decorator.ts
Normal file
10
apps/api/src/auth/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: string | undefined, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
4
apps/api/src/auth/decorators/public.decorator.ts
Normal file
4
apps/api/src/auth/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
28
apps/api/src/auth/dto/auth.dto.ts
Normal file
28
apps/api/src/auth/dto/auth.dto.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class RegisterDto {
|
||||
@ApiProperty({ example: 'user@example.com', description: '用户邮箱' })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: 'password123', description: '密码(至少6位)' })
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '张三', description: '用户名称' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({ example: 'user@example.com', description: '用户邮箱' })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: 'password123', description: '密码' })
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
25
apps/api/src/auth/guards/jwt-auth.guard.ts
Normal file
25
apps/api/src/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
37
apps/api/src/auth/strategies/jwt.strategy.ts
Normal file
37
apps/api/src/auth/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
interface JwtPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get<string>('JWT_SECRET', 'secret'),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: payload.sub },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('用户不存在');
|
||||
}
|
||||
|
||||
return { id: user.id, email: user.email, name: user.name };
|
||||
}
|
||||
}
|
||||
44
apps/api/src/main.ts
Normal file
44
apps/api/src/main.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
const port = configService.get<number>('PORT', 4000);
|
||||
|
||||
// 全局验证管道
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// CORS 配置
|
||||
app.enableCors({
|
||||
origin: ['http://localhost:3000'],
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// Swagger 配置
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Seclusion API')
|
||||
.setDescription('Seclusion 项目 API 文档')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
|
||||
await app.listen(port);
|
||||
console.log(`🚀 Application is running on: http://localhost:${port}`);
|
||||
console.log(`📚 Swagger docs: http://localhost:${port}/api/docs`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
10
apps/api/src/prisma/prisma.module.ts
Normal file
10
apps/api/src/prisma/prisma.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
13
apps/api/src/prisma/prisma.service.ts
Normal file
13
apps/api/src/prisma/prisma.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
||||
9
apps/api/src/user/dto/user.dto.ts
Normal file
9
apps/api/src/user/dto/user.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsString, IsOptional } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateUserDto {
|
||||
@ApiPropertyOptional({ example: '张三', description: '用户名称' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
}
|
||||
38
apps/api/src/user/user.controller.ts
Normal file
38
apps/api/src/user/user.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Controller, Get, Patch, Delete, Param, Body, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
import { UpdateUserDto } from './dto/user.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
|
||||
@ApiTags('用户')
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取所有用户' })
|
||||
findAll() {
|
||||
return this.userService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '根据 ID 获取用户' })
|
||||
findById(@Param('id') id: string) {
|
||||
return this.userService.findById(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: '更新用户信息' })
|
||||
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
|
||||
return this.userService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: '删除用户' })
|
||||
delete(@Param('id') id: string) {
|
||||
return this.userService.delete(id);
|
||||
}
|
||||
}
|
||||
11
apps/api/src/user/user.module.ts
Normal file
11
apps/api/src/user/user.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Module({
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService],
|
||||
})
|
||||
export class UserModule {}
|
||||
72
apps/api/src/user/user.service.ts
Normal file
72
apps/api/src/user/user.service.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { UpdateUserDto } from './dto/user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateUserDto) {
|
||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
return this.prisma.user.update({
|
||||
where: { id },
|
||||
data: dto,
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
await this.prisma.user.delete({ where: { id } });
|
||||
|
||||
return { message: '用户已删除' };
|
||||
}
|
||||
}
|
||||
13
apps/api/tsconfig.json
Normal file
13
apps/api/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@seclusion/typescript-config/nestjs.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"strictPropertyInitialization": false,
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
2
apps/web/.env.example
Normal file
2
apps/web/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
# API 配置
|
||||
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||
8
apps/web/.eslintrc.js
Normal file
8
apps/web/.eslintrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['@seclusion/eslint-config/next'],
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
};
|
||||
6
apps/web/next-env.d.ts
vendored
Normal file
6
apps/web/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
8
apps/web/next.config.ts
Normal file
8
apps/web/next.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ['@seclusion/shared'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
28
apps/web/package.json
Normal file
28
apps/web/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@seclusion/web",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"clean": "rm -rf .next .turbo node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@seclusion/shared": "workspace:*",
|
||||
"next": "^15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@seclusion/eslint-config": "workspace:*",
|
||||
"@seclusion/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^15.1.3",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
27
apps/web/src/app/globals.css
Normal file
27
apps/web/src/app/globals.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 255, 255, 255;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
20
apps/web/src/app/layout.tsx
Normal file
20
apps/web/src/app/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Seclusion',
|
||||
description: 'A monorepo template with Next.js and NestJS',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
33
apps/web/src/app/page.tsx
Normal file
33
apps/web/src/app/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { formatDate } from '@seclusion/shared';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center p-24">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Seclusion</h1>
|
||||
<p className="text-lg text-gray-600 mb-8">
|
||||
A monorepo template with Next.js + NestJS
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<a
|
||||
href="/api/health"
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||
>
|
||||
API Health Check
|
||||
</a>
|
||||
<a
|
||||
href="http://localhost:4000/api/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
|
||||
>
|
||||
API Docs
|
||||
</a>
|
||||
</div>
|
||||
<p className="mt-8 text-sm text-gray-400">
|
||||
Generated at: {formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
25
apps/web/src/lib/api.ts
Normal file
25
apps/web/src/lib/api.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
interface FetchOptions extends RequestInit {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
|
||||
const { token, headers, ...rest } = options;
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
...headers,
|
||||
},
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.message || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
11
apps/web/tsconfig.json
Normal file
11
apps/web/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@seclusion/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
189
docs/design.md
Normal file
189
docs/design.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Seclusion 项目设计文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
**Seclusion** 是一个基于 Next.js + NestJS 的全栈 Monorepo 项目模板,旨在为后续项目开发提供统一的脚手架。
|
||||
|
||||
## 2. 技术选型
|
||||
|
||||
| 层级 | 技术栈 | 版本 |
|
||||
|------|--------|------|
|
||||
| 前端 | Next.js + React + TypeScript | 15 / 19 / 5.7 |
|
||||
| 后端 | NestJS + Prisma + Swagger | 10 / 6 / 8 |
|
||||
| 认证 | Passport + JWT | - |
|
||||
| 数据库 | SQLite (可替换) | - |
|
||||
| 包管理 | pnpm workspace | 9.x |
|
||||
| 构建工具 | Turborepo | 2.x |
|
||||
| 代码规范 | ESLint + Prettier | - |
|
||||
|
||||
## 3. 项目结构
|
||||
|
||||
```
|
||||
seclusion/
|
||||
├── apps/
|
||||
│ ├── web/ # Next.js 前端应用
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── app/ # App Router 页面
|
||||
│ │ │ ├── components/ # React 组件
|
||||
│ │ │ └── lib/ # 工具库(API 客户端等)
|
||||
│ │ └── public/ # 静态资源
|
||||
│ │
|
||||
│ └── api/ # NestJS 后端应用
|
||||
│ ├── src/
|
||||
│ │ ├── auth/ # 认证模块
|
||||
│ │ ├── user/ # 用户模块
|
||||
│ │ ├── prisma/ # 数据库服务
|
||||
│ │ └── common/ # 公共装饰器/守卫
|
||||
│ └── prisma/ # Prisma schema 和迁移
|
||||
│
|
||||
├── packages/
|
||||
│ ├── shared/ # 共享代码
|
||||
│ │ └── src/
|
||||
│ │ ├── types/ # 类型定义
|
||||
│ │ └── utils/ # 工具函数
|
||||
│ ├── eslint-config/ # ESLint 配置包
|
||||
│ └── typescript-config/ # TypeScript 配置包
|
||||
│
|
||||
├── package.json # 根配置
|
||||
├── pnpm-workspace.yaml # 工作区定义
|
||||
└── turbo.json # Turborepo 任务配置
|
||||
```
|
||||
|
||||
## 4. 架构设计
|
||||
|
||||
### 4.1 Monorepo 架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Turborepo │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ apps/web │ ◄─────► │ apps/api │ │
|
||||
│ │ (Next.js) │ HTTP │ (NestJS) │ │
|
||||
│ └────────┬────────┘ └────────┬────────┘ │
|
||||
│ │ │ │
|
||||
│ └───────────┬───────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ packages/shared │ │
|
||||
│ │ (类型 + 工具函数) │ │
|
||||
│ └─────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 后端模块架构
|
||||
|
||||
```
|
||||
AppModule
|
||||
│
|
||||
├── ConfigModule (全局) # 环境变量管理
|
||||
├── PrismaModule (全局) # 数据库连接
|
||||
├── AuthModule # 认证模块
|
||||
│ ├── AuthController
|
||||
│ ├── AuthService
|
||||
│ ├── JwtStrategy
|
||||
│ └── JwtAuthGuard
|
||||
└── UserModule # 用户模块
|
||||
├── UserController
|
||||
└── UserService
|
||||
```
|
||||
|
||||
### 4.3 认证流程
|
||||
|
||||
```
|
||||
┌──────────┐ POST /auth/login ┌──────────────┐
|
||||
│ Client │ ──────────────────────► │ AuthModule │
|
||||
└──────────┘ └──────┬───────┘
|
||||
▲ │
|
||||
│ { accessToken } ▼
|
||||
└───────────────────────────── 验证密码,生成 JWT
|
||||
|
||||
┌──────────┐ GET /users (Bearer) ┌──────────────┐
|
||||
│ Client │ ──────────────────────► │ JwtAuthGuard │
|
||||
└──────────┘ └──────┬───────┘
|
||||
▲ │
|
||||
│ { users } ▼
|
||||
└───────────────────────────── 验证 Token,放行请求
|
||||
```
|
||||
|
||||
## 5. 数据模型
|
||||
|
||||
### 5.1 User 模型
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
password String
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
```
|
||||
|
||||
## 6. API 接口设计
|
||||
|
||||
| 方法 | 路径 | 描述 | 认证 |
|
||||
|------|------|------|------|
|
||||
| POST | /auth/register | 用户注册 | 否 |
|
||||
| POST | /auth/login | 用户登录 | 否 |
|
||||
| GET | /auth/me | 获取当前用户 | 是 |
|
||||
| GET | /users | 获取所有用户 | 是 |
|
||||
| GET | /users/:id | 获取指定用户 | 是 |
|
||||
| PATCH | /users/:id | 更新用户信息 | 是 |
|
||||
| DELETE | /users/:id | 删除用户 | 是 |
|
||||
| GET | /health | 健康检查 | 否 |
|
||||
|
||||
## 7. 共享包设计
|
||||
|
||||
### 7.1 类型定义 (@seclusion/shared/types)
|
||||
|
||||
- `ApiResponse<T>` - 通用 API 响应
|
||||
- `PaginationParams` - 分页请求参数
|
||||
- `PaginatedResponse<T>` - 分页响应
|
||||
- `User` - 用户类型
|
||||
- `LoginDto` / `CreateUserDto` - 请求 DTO
|
||||
- `AuthResponse` / `TokenPayload` - 认证相关类型
|
||||
|
||||
### 7.2 工具函数 (@seclusion/shared/utils)
|
||||
|
||||
- `formatDate()` - 日期格式化
|
||||
- `generateId()` - 随机 ID 生成
|
||||
- `deepClone()` - 深拷贝
|
||||
- `isEmpty()` - 空值判断
|
||||
- `safeJsonParse()` - 安全 JSON 解析
|
||||
|
||||
## 8. 开发规范
|
||||
|
||||
### 8.1 端口分配
|
||||
|
||||
- 前端: 3000
|
||||
- 后端: 4000
|
||||
- Swagger 文档: 4000/api/docs
|
||||
|
||||
### 8.2 环境变量
|
||||
|
||||
**后端 (apps/api/.env)**
|
||||
```env
|
||||
DATABASE_URL="file:./dev.db"
|
||||
JWT_SECRET="your-secret-key"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
PORT=4000
|
||||
```
|
||||
|
||||
**前端 (apps/web/.env.local)**
|
||||
```env
|
||||
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||
```
|
||||
|
||||
## 9. 构建依赖关系
|
||||
|
||||
```
|
||||
build
|
||||
├── packages/shared (先构建)
|
||||
├── apps/web (依赖 shared)
|
||||
└── apps/api (依赖 shared)
|
||||
```
|
||||
|
||||
Turborepo 通过 `^build` 依赖确保构建顺序正确。
|
||||
14
docs/index.md
Normal file
14
docs/index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 文档索引
|
||||
|
||||
本目录包含 Seclusion 项目的所有文档。
|
||||
|
||||
## 文档列表
|
||||
|
||||
| 文档 | 说明 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| [design.md](./design.md) | 项目设计文档 | 了解整体架构、技术选型、模块设计 |
|
||||
|
||||
## 快速链接
|
||||
|
||||
- [README.md](../README.md) - 项目入门指南
|
||||
- [CLAUDE.md](../CLAUDE.md) - Claude Code 开发指引
|
||||
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "seclusion",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.2",
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
"build": "turbo run build",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test",
|
||||
"clean": "turbo run clean && rm -rf node_modules",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"db:generate": "turbo run db:generate",
|
||||
"db:push": "turbo run db:push",
|
||||
"db:migrate": "turbo run db:migrate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"prettier": "^3.4.2",
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
}
|
||||
}
|
||||
42
packages/eslint-config/index.js
Normal file
42
packages/eslint-config/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
'import/no-unresolved': 'off',
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['node_modules/', 'dist/', '.next/', '.turbo/'],
|
||||
};
|
||||
13
packages/eslint-config/nest.js
Normal file
13
packages/eslint-config/nest.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ['./index.js'],
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
},
|
||||
};
|
||||
8
packages/eslint-config/next.js
Normal file
8
packages/eslint-config/next.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ['./index.js', 'next/core-web-vitals'],
|
||||
rules: {
|
||||
'@next/next/no-html-link-for-pages': 'off',
|
||||
'react/jsx-key': 'error',
|
||||
},
|
||||
};
|
||||
23
packages/eslint-config/package.json
Normal file
23
packages/eslint-config/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@seclusion/eslint-config",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"index.js",
|
||||
"next.js",
|
||||
"nest.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||
"@typescript-eslint/parser": "^8.18.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.31.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.0.0 || ^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0"
|
||||
}
|
||||
}
|
||||
37
packages/shared/package.json
Normal file
37
packages/shared/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@seclusion/shared",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"import": "./dist/types/index.mjs",
|
||||
"require": "./dist/types/index.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/utils/index.d.ts",
|
||||
"import": "./dist/utils/index.mjs",
|
||||
"require": "./dist/utils/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "eslint src --ext .ts,.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@seclusion/eslint-config": "workspace:*",
|
||||
"@seclusion/typescript-config": "workspace:*",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
5
packages/shared/src/index.ts
Normal file
5
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// 导出所有类型
|
||||
export * from './types';
|
||||
|
||||
// 导出所有工具函数
|
||||
export * from './utils';
|
||||
61
packages/shared/src/types/index.ts
Normal file
61
packages/shared/src/types/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// 通用 API 响应类型
|
||||
export interface ApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 分页请求参数
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 分页响应
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// 用户基础类型
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// 用户创建请求
|
||||
export interface CreateUserDto {
|
||||
email: string;
|
||||
password: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// 用户登录请求
|
||||
export interface LoginDto {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// 认证响应
|
||||
export interface AuthResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: Omit<User, 'createdAt' | 'updatedAt'>;
|
||||
}
|
||||
|
||||
// Token 载荷
|
||||
export interface TokenPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
75
packages/shared/src/utils/index.ts
Normal file
75
packages/shared/src/utils/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// 日期格式化
|
||||
export function formatDate(date: Date | string, format = 'YYYY-MM-DD'): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds);
|
||||
}
|
||||
|
||||
// 延迟函数
|
||||
export function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// 生成随机 ID
|
||||
export function generateId(length = 12): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 深拷贝
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
// 判断是否为空对象
|
||||
export function isEmpty(obj: unknown): boolean {
|
||||
if (obj === null || obj === undefined) return true;
|
||||
if (typeof obj === 'string') return obj.trim().length === 0;
|
||||
if (Array.isArray(obj)) return obj.length === 0;
|
||||
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 安全解析 JSON
|
||||
export function safeJsonParse<T>(json: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(json) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// 首字母大写
|
||||
export function capitalize(str: string): string {
|
||||
if (!str) return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
// 驼峰转短横线
|
||||
export function camelToKebab(str: string): string {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
}
|
||||
|
||||
// 短横线转驼峰
|
||||
export function kebabToCamel(str: string): string {
|
||||
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
}
|
||||
9
packages/shared/tsconfig.json
Normal file
9
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@seclusion/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
13
packages/shared/tsup.config.ts
Normal file
13
packages/shared/tsup.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
'types/index': 'src/types/index.ts',
|
||||
'utils/index': 'src/utils/index.ts',
|
||||
},
|
||||
format: ['cjs', 'esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
});
|
||||
26
packages/typescript-config/base.json
Normal file
26
packages/typescript-config/base.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist", ".next", ".turbo"]
|
||||
}
|
||||
14
packages/typescript-config/nestjs.json
Normal file
14
packages/typescript-config/nestjs.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2022",
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true
|
||||
}
|
||||
}
|
||||
18
packages/typescript-config/nextjs.json
Normal file
18
packages/typescript-config/nextjs.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"jsx": "preserve",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
14
packages/typescript-config/package.json
Normal file
14
packages/typescript-config/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@seclusion/typescript-config",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"base.json",
|
||||
"nextjs.json",
|
||||
"nestjs.json"
|
||||
]
|
||||
}
|
||||
9494
pnpm-lock.yaml
generated
Normal file
9494
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
34
turbo.json
Normal file
34
turbo.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": true
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["build"],
|
||||
"cache": true
|
||||
},
|
||||
"clean": {
|
||||
"cache": false
|
||||
},
|
||||
"db:generate": {
|
||||
"cache": false
|
||||
},
|
||||
"db:push": {
|
||||
"cache": false
|
||||
},
|
||||
"db:migrate": {
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user