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:
charilezhou
2025-12-28 14:51:40 +08:00
commit 74ced8c0c6
62 changed files with 11151 additions and 0 deletions

40
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,2 @@
auto-install-peers=true
strict-peer-dependencies=false

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v24.11.1

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.next
dist
.turbo
coverage
pnpm-lock.yaml

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100
}

65
CLAUDE.md Normal file
View 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 后端 (端口 4000API 文档: /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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

View 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")
}

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

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

View File

@@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
healthCheck() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
};
}
}

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

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

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

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

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,2 @@
# API 配置
NEXT_PUBLIC_API_URL=http://localhost:4000

8
apps/web/.eslintrc.js Normal file
View 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
View 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
View 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
View 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"
}
}

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

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

View 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/'],
};

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

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

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

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

View File

@@ -0,0 +1,5 @@
// 导出所有类型
export * from './types';
// 导出所有工具函数
export * from './utils';

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

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

View File

@@ -0,0 +1,9 @@
{
"extends": "@seclusion/typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

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

View 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"]
}

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

View 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"
}
]
}
}

View 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

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
packages:
- "apps/*"
- "packages/*"

34
turbo.json Normal file
View 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
}
}
}