Claude Code全栈实战指南:打造现代博客系统完整教程
开篇:AI编程的真实力量
叔本华说过:”天才能够洞察事物的本质,而智者知道如何运用这种洞察。”当我用Claude Code完成第一个完整项目时,深刻体会到了这句话的含义——AI不仅洞察了软件开发的本质,更知道如何将洞察转化为可执行的代码。
今天,我将带你完成一个真正的全栈项目:从零开始构建一个现代化的博客系统。这不是简单的Demo,而是一个具备生产级质量的完整应用,包含用户认证、内容管理、实时评论、SEO优化、容器化部署等企业级功能。
通过这个项目,你将体验到什么叫做”思想的速度编程”。
项目规划:构建什么样的博客系统?
功能蓝图与技术选型
让我们首先让Claude Code帮我们制定项目蓝图:
claude """
我要创建一个现代化的个人博客系统,目标是打造一个具有以下特点的平台:
业务目标:
- 面向技术博主和内容创作者
- 支持个人品牌建设和知识分享
- 具备商业化潜力(广告、付费内容)
- 社区互动功能(评论、点赞、关注)
技术要求:
- 现代化技术栈,性能优异
- SEO友好,搜索引擎收录良好
- 移动端适配,响应式设计
- 安全可靠,数据完整性保障
- 易于部署和维护
- 成本控制合理
功能需求:
1. 核心功能:文章发布、编辑、管理、展示
2. 用户系统:注册、登录、个人资料、权限管理
3. 内容组织:分类、标签、搜索、筛选
4. 社交功能:评论、点赞、关注、分享
5. 管理后台:内容审核、用户管理、数据分析
6. SEO优化:元标签、结构化数据、站点地图
7. 性能优化:缓存、懒加载、CDN集成
请为我设计完整的技术架构和实施方案
"""
基于Claude Code的深度分析,我们确定了以下技术架构:
🏗️ 技术架构设计
graph TB
subgraph "前端层"
A[Next.js 14 App Router] --> B[React 18 + TypeScript]
B --> C[Tailwind CSS + Headless UI]
C --> D[SWR + React Hook Form]
end
subgraph "后端层"
E[Node.js + Express.js] --> F[TypeScript + Prisma ORM]
F --> G[JWT认证 + RBAC权限]
G --> H[文件上传 + 图片处理]
end
subgraph "数据层"
I[PostgreSQL主数据库] --> J[Redis缓存层]
J --> K[Elasticsearch搜索引擎]
end
subgraph "部署层"
L[Docker容器化] --> M[Vercel前端 + Railway后端]
M --> N[AWS S3存储 + CloudFront CDN]
end
A --> E
E --> I
📊 开发效率预期对比
开发阶段 | 传统开发 | Claude Code | 预期提升 |
---|---|---|---|
需求分析与架构设计 | 2-3天 | 2小时 | 12-18x |
数据库建模 | 1天 | 30分钟 | 16x |
后端API开发 | 2周 | 1天 | 14x |
前端界面开发 | 2周 | 1-2天 | 7-14x |
集成测试 | 3天 | 4小时 | 18x |
部署配置 | 2天 | 2小时 | 24x |
总体项目 | 5-6周 | 4-5天 | 12-15x |
第一阶段:项目初始化与架构搭建
智能项目脚手架生成
# 创建项目根目录
mkdir modern-blog-platform
cd modern-blog-platform
# 让Claude Code创建完整的项目结构
claude """
创建一个企业级博客平台的完整项目结构:
项目名称:Modern Blog Platform
项目类型:全栈Web应用
前端技术栈:
- Next.js 14 (App Router)
- TypeScript 5.x
- Tailwind CSS 3.x
- Headless UI
- React Hook Form + Zod
- SWR数据获取
- Framer Motion动画
后端技术栈:
- Node.js + Express.js
- TypeScript
- Prisma ORM
- PostgreSQL
- Redis缓存
- JWT认证
- Multer文件上传
- Sharp图片处理
开发工具:
- ESLint + Prettier
- Husky + lint-staged
- Jest + Testing Library
- Storybook(可选)
- Docker + docker-compose
项目结构要求:
1. 前后端分离的Monorepo结构
2. 共享类型定义和工具函数
3. 完整的配置文件
4. 开发、测试、生产环境配置
5. 自动化脚本和文档
请创建完整的文件结构和基础配置
"""
Claude Code会生成如下项目结构:
modern-blog-platform/
├── frontend/ # Next.js前端应用
│ ├── app/ # App Router结构
│ │ ├── (auth)/ # 认证页面组
│ │ │ ├── login/
│ │ │ └── register/
│ │ ├── (dashboard)/ # 管理后台组
│ │ │ ├── admin/
│ │ │ └── dashboard/
│ │ ├── blog/ # 博客相关页面
│ │ │ ├── [slug]/ # 文章详情
│ │ │ ├── category/[slug]/ # 分类页面
│ │ │ └── tag/[slug]/ # 标签页面
│ │ ├── api/ # API路由
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ ├── error.tsx
│ │ └── not-found.tsx
│ ├── components/ # React组件
│ │ ├── ui/ # 基础UI组件
│ │ │ ├── Button.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── Card.tsx
│ │ │ └── Modal.tsx
│ │ ├── layout/ # 布局组件
│ │ │ ├── Header.tsx
│ │ │ ├── Footer.tsx
│ │ │ └── Sidebar.tsx
│ │ └── features/ # 功能组件
│ │ ├── auth/
│ │ ├── blog/
│ │ └── admin/
│ ├── lib/ # 工具函数
│ │ ├── utils.ts
│ │ ├── api.ts
│ │ ├── auth.ts
│ │ └── validations.ts
│ ├── hooks/ # 自定义Hooks
│ │ ├── useAuth.ts
│ │ ├── useApi.ts
│ │ └── useLocalStorage.ts
│ ├── types/ # TypeScript类型
│ │ ├── auth.ts
│ │ ├── blog.ts
│ │ └── api.ts
│ ├── styles/ # 样式文件
│ ├── public/ # 静态资源
│ ├── tests/ # 测试文件
│ ├── next.config.js
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── package.json
│ └── .env.local.example
├── backend/ # Express后端API
│ ├── src/
│ │ ├── controllers/ # 控制器
│ │ │ ├── authController.ts
│ │ │ ├── blogController.ts
│ │ │ ├── userController.ts
│ │ │ └── adminController.ts
│ │ ├── routes/ # 路由定义
│ │ │ ├── auth.ts
│ │ │ ├── blog.ts
│ │ │ ├── user.ts
│ │ │ └── admin.ts
│ │ ├── middleware/ # 中间件
│ │ │ ├── auth.ts
│ │ │ ├── validation.ts
│ │ │ ├── rateLimit.ts
│ │ │ └── errorHandler.ts
│ │ ├── services/ # 业务逻辑
│ │ │ ├── authService.ts
│ │ │ ├── blogService.ts
│ │ │ ├── userService.ts
│ │ │ └── emailService.ts
│ │ ├── models/ # 数据模型
│ │ ├── utils/ # 工具函数
│ │ │ ├── encryption.ts
│ │ │ ├── validation.ts
│ │ │ ├── fileUpload.ts
│ │ │ └── email.ts
│ │ ├── config/ # 配置文件
│ │ │ ├── database.ts
│ │ │ ├── redis.ts
│ │ │ ├── jwt.ts
│ │ │ └── upload.ts
│ │ ├── types/ # TypeScript类型
│ │ └── app.ts # 应用入口
│ ├── prisma/ # Prisma配置
│ │ ├── schema.prisma
│ │ ├── migrations/
│ │ └── seed.ts
│ ├── tests/ # 测试文件
│ │ ├── unit/
│ │ ├── integration/
│ │ └── e2e/
│ ├── uploads/ # 文件上传目录
│ ├── logs/ # 日志文件
│ ├── tsconfig.json
│ ├── package.json
│ └── .env.example
├── shared/ # 前后端共享代码
│ ├── types/ # 共享类型定义
│ │ ├── user.ts
│ │ ├── blog.ts
│ │ ├── auth.ts
│ │ └── api.ts
│ ├── utils/ # 共享工具函数
│ │ ├── validation.ts
│ │ ├── constants.ts
│ │ └── helpers.ts
│ └── package.json
├── docs/ # 项目文档
│ ├── api.md
│ ├── deployment.md
│ ├── development.md
│ └── architecture.md
├── scripts/ # 自动化脚本
│ ├── setup.sh
│ ├── build.sh
│ ├── deploy.sh
│ └── backup.sh
├── docker/ # Docker配置
│ ├── Dockerfile.frontend
│ ├── Dockerfile.backend
│ ├── docker-compose.dev.yml
│ └── docker-compose.prod.yml
├── .github/ # GitHub Actions
│ └── workflows/
│ ├── ci.yml
│ ├── deploy.yml
│ └── security.yml
├── package.json # 根包配置
├── docker-compose.yml # 开发环境
├── .gitignore
├── .env.example
├── README.md
└── LICENSE
开发环境一键配置
# 让Claude Code配置完整的开发环境
claude """
配置博客平台的开发环境:
要求:
1. 数据库:PostgreSQL + Redis(Docker容器)
2. 环境变量:开发、测试、生产环境模板
3. 开发脚本:一键启动所有服务
4. 代码规范:ESLint + Prettier + Husky
5. 依赖管理:Workspaces配置
6. 热重载:前后端自动重启
7. 调试配置:VS Code调试设置
目标:新开发者clone代码后,运行一个命令即可启动完整开发环境
"""
生成的核心配置文件:
docker-compose.dev.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: blog_postgres
environment:
POSTGRES_DB: blog_dev
POSTGRES_USER: blog_user
POSTGRES_PASSWORD: blog_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U blog_user -d blog_dev"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: blog_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
container_name: blog_elasticsearch
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
volumes:
postgres_data:
redis_data:
elasticsearch_data:
根目录package.json(Workspace配置)
{
"name": "modern-blog-platform",
"private": true,
"workspaces": [
"frontend",
"backend",
"shared"
],
"scripts": {
"dev": "concurrently \"npm run dev:db\" \"npm run dev:backend\" \"npm run dev:frontend\"",
"dev:db": "docker-compose -f docker-compose.dev.yml up -d",
"dev:backend": "cd backend && npm run dev",
"dev:frontend": "cd frontend && npm run dev",
"build": "npm run build:shared && npm run build:backend && npm run build:frontend",
"build:shared": "cd shared && npm run build",
"build:backend": "cd backend && npm run build",
"build:frontend": "cd frontend && npm run build",
"test": "npm run test:backend && npm run test:frontend",
"test:backend": "cd backend && npm test",
"test:frontend": "cd frontend && npm test",
"lint": "npm run lint:backend && npm run lint:frontend",
"lint:backend": "cd backend && npm run lint",
"lint:frontend": "cd frontend && npm run lint",
"setup": "npm install && npm run setup:env && npm run dev:db && sleep 10 && npm run db:setup",
"setup:env": "cp .env.example .env && cp frontend/.env.local.example frontend/.env.local && cp backend/.env.example backend/.env",
"db:setup": "cd backend && npx prisma migrate dev && npx prisma db seed",
"cleanup": "docker-compose -f docker-compose.dev.yml down -v && npm run clean:deps",
"clean:deps": "rm -rf node_modules frontend/node_modules backend/node_modules shared/node_modules"
},
"devDependencies": {
"concurrently": "^8.2.2",
"husky": "^8.0.3",
"lint-staged": "^15.2.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm test"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{json,md,yml,yaml}": [
"prettier --write",
"git add"
]
}
}
一键环境启动
# 安装依赖并启动开发环境
npm run setup
# 输出示例:
# ✅ 安装项目依赖...
# ✅ 创建环境变量文件...
# ✅ 启动数据库容器...
# ✅ 等待数据库就绪...
# ✅ 运行数据库迁移...
# ✅ 导入种子数据...
# ✅ 启动后端服务 (http://localhost:3001)
# ✅ 启动前端服务 (http://localhost:3000)
# 🚀 开发环境就绪!
# 日常开发启动
npm run dev
第二阶段:数据库设计与模型创建
企业级数据建模
claude """
为现代博客平台设计企业级数据库架构:
业务实体分析:
1. 用户体系:用户账户、角色权限、社交关系
2. 内容体系:文章、分类、标签、媒体文件
3. 互动体系:评论、点赞、收藏、分享
4. 系统体系:日志、通知、配置、统计
设计要求:
1. 数据完整性:外键约束、唯一索引、检查约束
2. 查询性能:合理索引设计、分区表考虑
3. 扩展性:软删除、版本控制、审计日志
4. 安全性:敏感信息加密、数据脱敏
5. 维护性:命名规范、注释完整
特殊需求:
- 支持文章草稿和发布状态
- 支持多级评论嵌套
- 支持文章版本历史
- 支持用户关注关系
- 支持内容审核工作流
- 支持SEO元数据管理
- 支持访问统计和分析
请使用Prisma Schema格式定义完整的数据模型
"""
生成的backend/prisma/schema.prisma:
// Prisma Schema for Modern Blog Platform
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ========== 用户体系 ==========
model User {
id String @id @default(cuid())
email String @unique
username String @unique
displayName String?
firstName String?
lastName String?
// 认证信息
password String
emailVerified DateTime?
avatar String?
bio String?
website String?
location String?
// 账户状态
isActive Boolean @default(true)
isVerified Boolean @default(false)
role UserRole @default(USER)
// 社交设置
isPrivate Boolean @default(false)
allowFollow Boolean @default(true)
// 关联关系
posts Post[]
comments Comment[]
likes Like[]
bookmarks Bookmark[]
// 用户关系
following Follow[] @relation("UserFollowing")
followers Follow[] @relation("UserFollowers")
// 通知设置
notifications Notification[] @relation("NotificationUser")
notificationSettings NotificationSetting?
// 统计信息
postsCount Int @default(0)
followersCount Int @default(0)
followingCount Int @default(0)
// 时间戳
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
lastLoginAt DateTime?
@@map("users")
}
model NotificationSetting {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
emailComment Boolean @default(true)
emailLike Boolean @default(true)
emailFollow Boolean @default(true)
emailNewsletter Boolean @default(false)
pushComment Boolean @default(true)
pushLike Boolean @default(true)
pushFollow Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("notification_settings")
}
// ========== 内容体系 ==========
model Post {
id String @id @default(cuid())
title String
slug String @unique
excerpt String?
content String
coverImage String?
// 发布状态
status PostStatus @default(DRAFT)
publishedAt DateTime?
// SEO信息
metaTitle String?
metaDescription String?
canonicalUrl String?
// 内容设置
allowComments Boolean @default(true)
isFeatured Boolean @default(false)
isSticky Boolean @default(false)
// 关联关系
authorId String
author User @relation(fields: [authorId], references: [id])
categoryId String?
category Category? @relation(fields: [categoryId], references: [id])
tags PostTag[]
comments Comment[]
likes Like[]
bookmarks Bookmark[]
versions PostVersion[]
// 统计信息
viewCount Int @default(0)
likeCount Int @default(0)
commentCount Int @default(0)
shareCount Int @default(0)
// 时间戳
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([status, publishedAt])
@@index([authorId])
@@index([categoryId])
@@map("posts")
}
model PostVersion {
id String @id @default(cuid())
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
title String
content String
excerpt String?
version Int
changeLog String?
createdAt DateTime @default(now())
createdBy String
@@unique([postId, version])
@@map("post_versions")
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
description String?
color String?
icon String?
coverImage String?
// 层级结构
parentId String?
parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id])
children Category[] @relation("CategoryHierarchy")
// SEO信息
metaTitle String?
metaDescription String?
posts Post[]
postsCount Int @default(0)
// 排序权重
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([parentId])
@@map("categories")
}
model Tag {
id String @id @default(cuid())
name String @unique
slug String @unique
description String?
color String?
posts PostTag[]
postsCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("tags")
}
model PostTag {
id String @id @default(cuid())
postId String
tagId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([postId, tagId])
@@map("post_tags")
}
// ========== 互动体系 ==========
model Comment {
id String @id @default(cuid())
content String
// 关联关系
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
authorId String
author User @relation(fields: [authorId], references: [id])
// 嵌套评论
parentId String?
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id])
replies Comment[] @relation("CommentReplies")
// 状态管理
status CommentStatus @default(PENDING)
isApproved Boolean @default(false)
// 统计信息
likeCount Int @default(0)
replyCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([postId])
@@index([authorId])
@@index([parentId])
@@map("comments")
}
model Like {
id String @id @default(cuid())
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
@@unique([postId, userId])
@@map("likes")
}
model Bookmark {
id String @id @default(cuid())
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
@@unique([postId, userId])
@@map("bookmarks")
}
model Follow {
id String @id @default(cuid())
followerId String
follower User @relation("UserFollowing", fields: [followerId], references: [id])
followingId String
following User @relation("UserFollowers", fields: [followingId], references: [id])
createdAt DateTime @default(now())
@@unique([followerId, followingId])
@@map("follows")
}
// ========== 系统体系 ==========
model Notification {
id String @id @default(cuid())
type NotificationType
title String
content String?
data Json?
userId String
user User @relation("NotificationUser", fields: [userId], references: [id])
isRead Boolean @default(false)
readAt DateTime?
createdAt DateTime @default(now())
@@index([userId, isRead])
@@map("notifications")
}
model AuditLog {
id String @id @default(cuid())
userId String?
action String
resource String
resourceId String?
oldValues Json?
newValues Json?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([userId])
@@index([resource, resourceId])
@@map("audit_logs")
}
model SiteConfig {
id String @id @default(cuid())
key String @unique
value Json
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("site_configs")
}
model Analytics {
id String @id @default(cuid())
postId String?
userId String?
event String // view, like, share, comment
source String? // direct, search, social, referral
referer String?
ipAddress String?
userAgent String?
country String?
city String?
metadata Json?
createdAt DateTime @default(now())
@@index([postId, event])
@@index([userId, event])
@@index([createdAt])
@@map("analytics")
}
// ========== 枚举类型 ==========
enum UserRole {
USER
AUTHOR
MODERATOR
ADMIN
SUPER_ADMIN
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
DELETED
}
enum CommentStatus {
PENDING
APPROVED
REJECTED
SPAM
}
enum NotificationType {
COMMENT
LIKE
FOLLOW
MENTION
SYSTEM
}
数据库初始化与种子数据
claude """
为博客平台创建完整的种子数据:
要求:
1. 创建系统管理员账户
2. 创建示例用户(作者、读者)
3. 创建文章分类体系(技术、生活、随想等)
4. 创建常用标签
5. 创建示例文章(包含不同状态)
6. 创建示例评论和互动数据
7. 创建系统配置数据
数据特点:
- 真实可信的示例内容
- 涵盖各种业务场景
- 便于功能演示和测试
- 符合中文用户习惯
"""
生成的backend/prisma/seed.ts:
import { PrismaClient, UserRole, PostStatus, CommentStatus } from '@prisma/client'
import bcrypt from 'bcryptjs'
const prisma = new PrismaClient()
async function main() {
console.log('🌱 开始创建种子数据...')
// 创建用户
const hashedPassword = await bcrypt.hash('admin123456', 12)
const admin = await prisma.user.upsert({
where: { email: 'admin@blog.com' },
update: {},
create: {
email: 'admin@blog.com',
username: 'admin',
displayName: '系统管理员',
firstName: '管理员',
password: hashedPassword,
role: UserRole.SUPER_ADMIN,
bio: '博客平台系统管理员,负责平台运营和维护。',
isVerified: true,
emailVerified: new Date(),
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
},
})
const author1 = await prisma.user.upsert({
where: { email: 'zhang.tech@blog.com' },
update: {},
create: {
email: 'zhang.tech@blog.com',
username: 'zhang_tech',
displayName: '张小技',
firstName: '小技',
lastName: '张',
password: hashedPassword,
role: UserRole.AUTHOR,
bio: '全栈开发工程师,专注于现代Web技术,喜欢分享技术心得和最佳实践。',
website: 'https://zhangtech.dev',
location: '北京',
isVerified: true,
emailVerified: new Date(),
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face',
},
})
const author2 = await prisma.user.upsert({
where: { email: 'li.writer@blog.com' },
update: {},
create: {
email: 'li.writer@blog.com',
username: 'li_writer',
displayName: '李文青',
firstName: '文青',
lastName: '李',
password: hashedPassword,
role: UserRole.AUTHOR,
bio: '自由撰稿人,热爱文学和生活,相信文字的力量能够温暖人心。',
location: '上海',
isVerified: true,
emailVerified: new Date(),
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face',
},
})
// 创建普通用户
const reader = await prisma.user.upsert({
where: { email: 'reader@blog.com' },
update: {},
create: {
email: 'reader@blog.com',
username: 'avid_reader',
displayName: '热心读者',
password: hashedPassword,
bio: '喜欢阅读和思考,经常在评论区与作者交流。',
emailVerified: new Date(),
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face',
},
})
console.log('✅ 用户创建完成')
// 创建分类
const techCategory = await prisma.category.upsert({
where: { slug: 'technology' },
update: {},
create: {
name: '技术分享',
slug: 'technology',
description: '前端、后端、DevOps等技术相关文章',
color: '#3B82F6',
icon: '💻',
metaTitle: '技术分享 - 最新技术文章和教程',
metaDescription: '分享最新的技术文章、教程和最佳实践,涵盖前端、后端、DevOps等领域',
sortOrder: 1,
},
})
const lifestyleCategory = await prisma.category.upsert({
where: { slug: 'lifestyle' },
update: {},
create: {
name: '生活感悟',
slug: 'lifestyle',
description: '生活感悟、人生思考、日常分享',
color: '#10B981',
icon: '🌱',
metaTitle: '生活感悟 - 记录生活的点点滴滴',
metaDescription: '记录生活中的感悟和思考,分享人生的酸甜苦辣',
sortOrder: 2,
},
})
const travelCategory = await prisma.category.upsert({
where: { slug: 'travel' },
update: {},
create: {
name: '旅行游记',
slug: 'travel',
description: '旅行见闻、攻略分享、美景记录',
color: '#F59E0B',
icon: '✈️',
metaTitle: '旅行游记 - 发现世界的美好',
metaDescription: '分享旅行中的美好瞬间和难忘体验',
sortOrder: 3,
},
})
console.log('✅ 分类创建完成')
// 创建标签
const tags = await Promise.all([
prisma.tag.upsert({
where: { slug: 'javascript' },
update: {},
create: { name: 'JavaScript', slug: 'javascript', color: '#F7DF1E' },
}),
prisma.tag.upsert({
where: { slug: 'react' },
update: {},
create: { name: 'React', slug: 'react', color: '#61DAFB' },
}),
prisma.tag.upsert({
where: { slug: 'nodejs' },
update: {},
create: { name: 'Node.js', slug: 'nodejs', color: '#339933' },
}),
prisma.tag.upsert({
where: { slug: 'typescript' },
update: {},
create: { name: 'TypeScript', slug: 'typescript', color: '#3178C6' },
}),
prisma.tag.upsert({
where: { slug: 'life' },
update: {},
create: { name: '生活', slug: 'life', color: '#EC4899' },
}),
prisma.tag.upsert({
where: { slug: 'thinking' },
update: {},
create: { name: '思考', slug: 'thinking', color: '#8B5CF6' },
}),
])
console.log('✅ 标签创建完成')
// 创建文章
const post1 = await prisma.post.create({
data: {
title: '深入理解现代JavaScript:从ES6到ES2024',
slug: 'modern-javascript-deep-dive',
excerpt: 'JavaScript在过去几年发生了翻天覆地的变化。从ES6的革命性更新到ES2024的最新特性,本文将带你深入了解现代JavaScript的强大功能和最佳实践。',
content: `# 深入理解现代JavaScript:从ES6到ES2024
JavaScript已经从一个简单的脚本语言发展成为现代Web开发的核心。无论是前端的React、Vue,还是后端的Node.js,JavaScript都在其中扮演着重要角色。
## ES6:JavaScript的重生
ES6(ECMAScript 2015)可以说是JavaScript发展史上的一个重要里程碑。它引入了许多革命性的特性:
### 1. let和const
\`\`\`javascript
// 使用let和const替代var
let count = 0;
const PI = 3.14159;
// 块级作用域
if (true) {
let blockScoped = "我只在这个块中存在";
console.log(blockScoped); // 正常输出
}
// console.log(blockScoped); // ReferenceError
\`\`\`
### 2. 箭头函数
\`\`\`javascript
// 传统函数
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
// 在数组方法中的优雅使用
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
\`\`\`
### 3. 模板字符串
\`\`\`javascript
const name = "张三";
const age = 25;
// 老式字符串拼接
const message1 = "大家好,我是" + name + ",今年" + age + "岁";
// 模板字符串
const message2 = \`大家好,我是\${name},今年\${age}岁\`;
// 多行字符串
const html = \`
<div class="user-card">
<h2>\${name}</h2>
<p>年龄:\${age}</p>
</div>
\`;
\`\`\`
## ES2020及以后:持续进化
### Optional Chaining (?.)
\`\`\`javascript
const user = {
profile: {
address: {
street: "中关村大街"
}
}
};
// 安全地访问深层嵌套属性
const street = user?.profile?.address?.street;
console.log(street); // "中关村大街"
const city = user?.profile?.address?.city;
console.log(city); // undefined(不会报错)
\`\`\`
### Nullish Coalescing (??)
\`\`\`javascript
const config = {
theme: null,
debug: false,
timeout: 0
};
// 使用 || 的问题
const theme1 = config.theme || "light"; // "light"
const debug1 = config.debug || true; // true(错误!)
const timeout1 = config.timeout || 5000; // 5000(错误!)
// 使用 ?? 的解决方案
const theme2 = config.theme ?? "light"; // "light"
const debug2 = config.debug ?? true; // false(正确!)
const timeout2 = config.timeout ?? 5000; // 0(正确!)
\`\`\`
## ES2024最新特性
### Array.prototype.with()
\`\`\`javascript
const fruits = ["苹果", "香蕉", "橘子"];
// 传统方式(会修改原数组)
fruits[1] = "葡萄";
console.log(fruits); // ["苹果", "葡萄", "橘子"]
// 新方式(返回新数组)
const fruits = ["苹果", "香蕉", "橘子"];
const newFruits = fruits.with(1, "葡萄");
console.log(fruits); // ["苹果", "香蕉", "橘子"](原数组不变)
console.log(newFruits); // ["苹果", "葡萄", "橘子"]
\`\`\`
### Promise.withResolvers()
\`\`\`javascript
// 传统Promise创建方式
function createDelayedPromise(delay) {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
setTimeout(() => resolve("完成"), delay);
return { promise, resolve, reject };
}
// 新的withResolvers方法
function createDelayedPromise(delay) {
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve("完成"), delay);
return { promise, resolve, reject };
}
\`\`\`
## 实际项目应用建议
### 1. 合理使用现代特性
在项目中使用现代JavaScript特性时,要考虑:
- 浏览器兼容性需求
- 团队成员的技能水平
- 项目的复杂度
### 2. 渐进式采用
不要一次性使用所有新特性,而是:
- 从最常用的开始(let/const、箭头函数、模板字符串)
- 逐步引入高级特性(解构、扩展操作符、async/await)
- 最后尝试最新特性(optional chaining、nullish coalescing)
### 3. 配置好开发环境
使用合适的工具确保代码质量:
- Babel用于转译
- ESLint用于代码检查
- Prettier用于代码格式化
- TypeScript用于类型安全
## 总结
现代JavaScript为我们提供了强大而优雅的编程体验。通过合理使用这些特性,我们可以写出更简洁、更易维护的代码。但记住,技术是为了解决问题而存在的,不要为了使用新特性而强行使用。
选择最适合你的项目和团队的技术方案,才是最好的实践。`,
status: PostStatus.PUBLISHED,
publishedAt: new Date('2024-01-15'),
authorId: author1.id,
categoryId: techCategory.id,
metaTitle: '深入理解现代JavaScript:从ES6到ES2024 - 技术博客',
metaDescription: 'JavaScript从ES6到ES2024的发展历程,包含最新特性介绍和最佳实践建议',
allowComments: true,
isFeatured: true,
viewCount: 1250,
likeCount: 89,
commentCount: 12,
},
})
// 为文章添加标签
await Promise.all([
prisma.postTag.create({
data: { postId: post1.id, tagId: tags[0].id }, // JavaScript
}),
prisma.postTag.create({
data: { postId: post1.id, tagId: tags[3].id }, // TypeScript
}),
])
// 创建更多文章...
const post2 = await prisma.post.create({
data: {
title: '生活中的小确幸:发现平凡中的美好',
slug: 'finding-happiness-in-ordinary-life',
excerpt: '在忙碌的都市生活中,我们常常忽略了身边的美好。一杯热茶、一本好书、一次深呼吸,都可能成为我们生活中的小确幸。',
content: `# 生活中的小确幸:发现平凡中的美好
现代生活节奏越来越快,我们总是在追求更大的成功、更多的财富、更高的地位。但在这个过程中,我们是否忽略了生活中那些简单而美好的瞬间?
## 什么是小确幸?
"小确幸"这个词来自村上春树,指的是生活中微小而确实的幸福。它不需要特别的条件,也不需要昂贵的代价,只需要我们用心去感受。
### 早晨的第一缕阳光
每天早上,当阳光透过窗帘洒进房间时,我总是会停下手中的事情,静静地感受这份温暖。这份简单的美好,比任何昂贵的礼物都更能让我感到幸福。
### 一杯用心泡制的茶
工作间隙,为自己泡一壶好茶。看着茶叶在热水中慢慢舒展,闻着淡淡的茶香,品味着甘甜的茶汤。这个过程让我暂时忘记了工作的压力,内心变得平静。
### 与朋友的真诚对话
在这个社交媒体盛行的时代,真诚的面对面交流变得珍贵。与好友坐在咖啡厅里,分享彼此的近况、困惑和喜悦,这种心灵的交流比任何娱乐活动都更有价值。
## 如何发现生活中的小确幸?
### 1. 放慢脚步
现代人总是匆匆忙忙,我们需要学会偶尔停下来,观察周围的世界。也许是街角的一朵花,也许是孩子天真的笑声,也许是老人慈祥的面容。
### 2. 用心感受
小确幸需要用心去感受。当我们专注于当下这一刻时,就能发现平凡生活中的不平凡。
### 3. 记录美好
建议大家准备一本"幸福笔记",每天记录下让自己开心的小事。这样,在不开心的时候,翻看这些记录,会让我们重新感受到生活的美好。
### 4. 分享快乐
把自己发现的小确幸分享给身边的人,不仅能让别人感受到快乐,也能让自己的快乐加倍。
## 我的小确幸清单
- 周末早上睡到自然醒
- 读到一本好书中的精彩片段
- 在书店里偶遇一本心仪已久的书
- 和家人一起吃饭时的温馨时光
- 看到陌生人的善意举动
- 完成一个小目标时的成就感
- 雨后的清新空气
- 夜晚的星空
## 结语
生活的幸福不在于拥有多少,而在于感受多少。当我们学会发现和珍惜生活中的小确幸时,就会发现原来幸福一直都在我们身边。
愿每个人都能在平凡的生活中,找到属于自己的小确幸。`,
status: PostStatus.PUBLISHED,
publishedAt: new Date('2024-01-10'),
authorId: author2.id,
categoryId: lifestyleCategory.id,
metaTitle: '生活中的小确幸:发现平凡中的美好 - 生活感悟',
metaDescription: '在忙碌的生活中发现小确幸,学会珍惜平凡中的美好瞬间',
allowComments: true,
viewCount: 890,
likeCount: 156,
commentCount: 23,
},
})
await prisma.postTag.create({
data: { postId: post2.id, tagId: tags[4].id }, // 生活
})
console.log('✅ 文章创建完成')
// 创建评论
const comment1 = await prisma.comment.create({
data: {
content: '写得真好!现代JavaScript确实发展很快,作为前端开发者必须要跟上时代的步伐。特别是Optional Chaining这个特性,真的是太实用了!',
postId: post1.id,
authorId: reader.id,
status: CommentStatus.APPROVED,
isApproved: true,
},
})
const reply1 = await prisma.comment.create({
data: {
content: '感谢支持!确实,Optional Chaining是我最喜欢的ES2020特性之一,极大地简化了代码。你在项目中有使用吗?',
postId: post1.id,
authorId: author1.id,
parentId: comment1.id,
status: CommentStatus.APPROVED,
isApproved: true,
},
})
console.log('✅ 评论创建完成')
// 创建点赞数据
await Promise.all([
prisma.like.create({
data: { postId: post1.id, userId: reader.id },
}),
prisma.like.create({
data: { postId: post1.id, userId: author2.id },
}),
prisma.like.create({
data: { postId: post2.id, userId: author1.id },
}),
])
// 创建关注关系
await prisma.follow.create({
data: {
followerId: reader.id,
followingId: author1.id,
},
})
// 更新统计数据
await prisma.user.update({
where: { id: author1.id },
data: {
postsCount: 1,
followersCount: 1,
},
})
await prisma.user.update({
where: { id: reader.id },
data: {
followingCount: 1,
},
})
// 创建系统配置
await Promise.all([
prisma.siteConfig.upsert({
where: { key: 'site_name' },
update: {},
create: {
key: 'site_name',
value: '"现代博客平台"',
description: '网站名称',
},
}),
prisma.siteConfig.upsert({
where: { key: 'site_description' },
update: {},
create: {
key: 'site_description',
value: '"分享技术、记录生活、传递美好"',
description: '网站描述',
},
}),
prisma.siteConfig.upsert({
where: { key: 'allow_registration' },
update: {},
create: {
key: 'allow_registration',
value: 'true',
description: '是否允许用户注册',
},
}),
])
console.log('✅ 系统配置创建完成')
// 显示统计信息
const stats = {
users: await prisma.user.count(),
categories: await prisma.category.count(),
tags: await prisma.tag.count(),
posts: await prisma.post.count(),
comments: await prisma.comment.count(),
likes: await prisma.like.count(),
follows: await prisma.follow.count(),
}
console.log('\n📊 种子数据统计:')
console.log(`👥 用户: ${stats.users}`)
console.log(`📂 分类: ${stats.categories}`)
console.log(`🏷️ 标签: ${stats.tags}`)
console.log(`📝 文章: ${stats.posts}`)
console.log(`💬 评论: ${stats.comments}`)
console.log(`❤️ 点赞: ${stats.likes}`)
console.log(`👤 关注: ${stats.follows}`)
console.log('\n🎉 种子数据创建完成!')
console.log('\n📧 默认账户信息:')
console.log('管理员: admin@blog.com / admin123456')
console.log('作者1: zhang.tech@blog.com / admin123456')
console.log('作者2: li.writer@blog.com / admin123456')
console.log('读者: reader@blog.com / admin123456')
}
main()
.catch((e) => {
console.error('❌ 种子数据创建失败:', e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
数据库部署与验证
# 运行数据库迁移
cd backend
npx prisma migrate dev --name init
# 生成Prisma Client
npx prisma generate
# 运行种子数据脚本
npx prisma db seed
# 验证数据库
npx prisma studio
# 打开 http://localhost:5555 查看数据
第三阶段:后端API系统架构
企业级认证系统实现
claude """
为博客平台实现企业级的用户认证和授权系统:
核心需求:
1. 安全的用户注册和登录流程
2. JWT token管理(Access + Refresh Token)
3. 基于角色的访问控制(RBAC)
4. 账户安全措施(限流、锁定、审计)
5. 邮箱验证和密码重置
6. 社交登录集成(Google、GitHub)
7. 两步验证支持
技术实现:
- JWT认证机制
- bcrypt密码加密
- Redis session存储
- 邮件服务集成
- 限流中间件
- 安全日志记录
安全要求:
- 防暴力破解
- CSRF防护
- XSS防护
- SQL注入防护
- 敏感信息脱敏
请实现完整的认证授权系统
"""
生成的核心认证代码:
backend/src/controllers/authController.ts
import { Request, Response, NextFunction } from 'express'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { PrismaClient } from '@prisma/client'
import { z } from 'zod'
import rateLimit from 'express-rate-limit'
import { createHash, randomBytes } from 'crypto'
import { sendEmail } from '../services/emailService'
import { redis } from '../config/redis'
import { auditLog } from '../utils/auditLog'
const prisma = new PrismaClient()
// ========== 数据验证模式 ==========
const registerSchema = z.object({
email: z.string().email('邮箱格式不正确'),
username: z
.string()
.min(3, '用户名至少3个字符')
.max(20, '用户名最多20个字符')
.regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
password: z
.string()
.min(8, '密码至少8个字符')
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, '密码必须包含大小写字母和数字'),
displayName: z.string().min(1, '显示名称不能为空').max(50, '显示名称最多50个字符'),
firstName: z.string().optional(),
lastName: z.string().optional(),
})
const loginSchema = z.object({
email: z.string().email('邮箱格式不正确'),
password: z.string().min(1, '密码不能为空'),
rememberMe: z.boolean().optional(),
})
const resetPasswordSchema = z.object({
token: z.string().min(1, 'Token不能为空'),
password: z
.string()
.min(8, '密码至少8个字符')
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, '密码必须包含大小写字母和数字'),
})
// ========== 限流中间件 ==========
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 最多5次尝试
message: {
error: '登录尝试次数过多,请15分钟后再试',
code: 'TOO_MANY_ATTEMPTS'
},
standardHeaders: true,
legacyHeaders: false,
// 自定义键生成器,基于IP和邮箱
keyGenerator: (req: Request) => {
return `auth:${req.ip}:${req.body.email || 'unknown'}`
},
})
export const registerLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1小时
max: 3, // 最多3次注册
message: {
error: '注册尝试次数过多,请1小时后再试',
code: 'TOO_MANY_REGISTRATIONS'
},
})
// ========== JWT工具函数 ==========
interface TokenPayload {
userId: string
role: string
sessionId: string
}
const generateTokens = async (userId: string, role: string, rememberMe = false) => {
const sessionId = randomBytes(32).toString('hex')
const accessToken = jwt.sign(
{ userId, role, sessionId },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
)
const refreshToken = jwt.sign(
{ userId, sessionId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: rememberMe ? '30d' : '7d' }
)
// 将session存储到Redis
const sessionKey = `session:${userId}:${sessionId}`
await redis.setex(sessionKey, rememberMe ? 30 * 24 * 60 * 60 : 7 * 24 * 60 * 60, JSON.stringify({
userId,
role,
createdAt: new Date().toISOString(),
userAgent: '',
ipAddress: '',
}))
return { accessToken, refreshToken, sessionId }
}
const verifyToken = async (token: string, secret: string): Promise<TokenPayload | null> => {
try {
const decoded = jwt.verify(token, secret) as TokenPayload
// 验证session是否存在
const sessionKey = `session:${decoded.userId}:${decoded.sessionId}`
const session = await redis.get(sessionKey)
if (!session) {
return null
}
return decoded
} catch (error) {
return null
}
}
// ========== 控制器函数 ==========
// 用户注册
export const register = async (req: Request, res: Response, next: NextFunction) => {
try {
const validatedData = registerSchema.parse(req.body)
// 检查邮箱和用户名是否已存在
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ email: validatedData.email.toLowerCase() },
{ username: validatedData.username.toLowerCase() }
]
}
})
if (existingUser) {
return res.status(409).json({
error: existingUser.email === validatedData.email.toLowerCase()
? '邮箱已被注册'
: '用户名已被占用',
code: 'USER_EXISTS'
})
}
// 加密密码
const hashedPassword = await bcrypt.hash(validatedData.password, 12)
// 生成邮箱验证token
const emailVerificationToken = randomBytes(32).toString('hex')
const tokenHash = createHash('sha256').update(emailVerificationToken).digest('hex')
// 创建用户
const user = await prisma.user.create({
data: {
email: validatedData.email.toLowerCase(),
username: validatedData.username.toLowerCase(),
password: hashedPassword,
displayName: validatedData.displayName,
firstName: validatedData.firstName,
lastName: validatedData.lastName,
},
select: {
id: true,
email: true,
username: true,
displayName: true,
avatar: true,
role: true,
isVerified: true,
createdAt: true,
}
})
// 存储验证token(24小时有效)
await redis.setex(
`email_verification:${tokenHash}`,
24 * 60 * 60,
user.id
)
// 发送验证邮件
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${emailVerificationToken}`
await sendEmail({
to: user.email,
subject: '验证您的邮箱地址',
template: 'email-verification',
data: {
userName: user.displayName,
verificationUrl,
}
})
// 记录审计日志
await auditLog({
userId: user.id,
action: 'USER_REGISTER',
resource: 'User',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
})
res.status(201).json({
message: '注册成功,请查收邮件验证账户',
user,
requiresVerification: true,
})
} catch (error) {
next(error)
}
}
// 用户登录
export const login = async (req: Request, res: Response, next: NextFunction) => {
try {
const validatedData = loginSchema.parse(req.body)
// 查找用户
const user = await prisma.user.findUnique({
where: { email: validatedData.email.toLowerCase() },
select: {
id: true,
email: true,
username: true,
displayName: true,
avatar: true,
password: true,
role: true,
isActive: true,
isVerified: true,
emailVerified: true,
}
})
if (!user) {
await auditLog({
action: 'LOGIN_FAILED',
resource: 'User',
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
oldValues: { reason: 'USER_NOT_FOUND', email: validatedData.email }
})
return res.status(401).json({
error: '邮箱或密码错误',
code: 'INVALID_CREDENTIALS'
})
}
if (!user.isActive) {
return res.status(403).json({
error: '账户已被禁用,请联系管理员',
code: 'ACCOUNT_DISABLED'
})
}
// 验证密码
const isValidPassword = await bcrypt.compare(validatedData.password, user.password)
if (!isValidPassword) {
await auditLog({
userId: user.id,
action: 'LOGIN_FAILED',
resource: 'User',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
oldValues: { reason: 'INVALID_PASSWORD' }
})
return res.status(401).json({
error: '邮箱或密码错误',
code: 'INVALID_CREDENTIALS'
})
}
// 生成tokens
const { accessToken, refreshToken } = await generateTokens(
user.id,
user.role,
validatedData.rememberMe
)
// 设置refresh token到httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: validatedData.rememberMe ? 30 * 24 * 60 * 60 * 1000 : 7 * 24 * 60 * 60 * 1000,
})
// 更新最后登录时间
await prisma.user.update({
where: { id: user.id },
data: { lastLoginAt: new Date() }
})
// 记录成功登录
await auditLog({
userId: user.id,
action: 'LOGIN_SUCCESS',
resource: 'User',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
})
// 移除密码字段
const { password, ...userWithoutPassword } = user
res.json({
message: '登录成功',
user: userWithoutPassword,
accessToken,
requiresVerification: !user.emailVerified,
})
} catch (error) {
next(error)
}
}
// 刷新token
export const refreshToken = async (req: Request, res: Response, next: NextFunction) => {
try {
const { refreshToken: token } = req.cookies
if (!token) {
return res.status(401).json({
error: '未提供刷新token',
code: 'NO_REFRESH_TOKEN'
})
}
const decoded = await verifyToken(token, process.env.JWT_REFRESH_SECRET!)
if (!decoded) {
return res.status(401).json({
error: '无效的刷新token',
code: 'INVALID_REFRESH_TOKEN'
})
}
// 验证用户是否存在且活跃
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: {
id: true,
email: true,
username: true,
displayName: true,
avatar: true,
role: true,
isActive: true,
}
})
if (!user || !user.isActive) {
return res.status(401).json({
error: '用户不存在或已被禁用',
code: 'USER_INVALID'
})
}
// 生成新的tokens
const tokens = await generateTokens(user.id, user.role)
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
})
res.json({
message: 'Token刷新成功',
user,
accessToken: tokens.accessToken,
})
} catch (error) {
next(error)
}
}
// 登出
export const logout = async (req: Request, res: Response, next: NextFunction) => {
try {
const { refreshToken: token } = req.cookies
if (token) {
const decoded = await verifyToken(token, process.env.JWT_REFRESH_SECRET!)
if (decoded) {
// 删除服务端session
const sessionKey = `session:${decoded.userId}:${decoded.sessionId}`
await redis.del(sessionKey)
// 记录登出
await auditLog({
userId: decoded.userId,
action: 'LOGOUT',
resource: 'User',
resourceId: decoded.userId,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
})
}
}
res.clearCookie('refreshToken')
res.json({ message: '登出成功' })
} catch (error) {
next(error)
}
}
// 邮箱验证
export const verifyEmail = async (req: Request, res: Response, next: NextFunction) => {
try {
const { token } = req.body
if (!token) {
return res.status(400).json({
error: '缺少验证token',
code: 'MISSING_TOKEN'
})
}
const tokenHash = createHash('sha256').update(token).digest('hex')
const userId = await redis.get(`email_verification:${tokenHash}`)
if (!userId) {
return res.status(400).json({
error: '验证链接无效或已过期',
code: 'INVALID_TOKEN'
})
}
// 更新用户验证状态
const user = await prisma.user.update({
where: { id: userId },
data: {
emailVerified: new Date(),
isVerified: true,
},
select: {
id: true,
email: true,
username: true,
displayName: true,
avatar: true,
role: true,
isVerified: true,
}
})
// 删除验证token
await redis.del(`email_verification:${tokenHash}`)
// 记录验证成功
await auditLog({
userId: user.id,
action: 'EMAIL_VERIFIED',
resource: 'User',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
})
res.json({
message: '邮箱验证成功',
user,
})
} catch (error) {
next(error)
}
}
// 发送密码重置邮件
export const forgotPassword = async (req: Request, res: Response, next: NextFunction) => {
try {
const { email } = req.body
if (!email) {
return res.status(400).json({
error: '邮箱地址不能为空',
code: 'MISSING_EMAIL'
})
}
const user = await prisma.user.findUnique({
where: { email: email.toLowerCase() },
select: { id: true, email: true, displayName: true, isActive: true }
})
// 无论用户是否存在都返回成功消息(安全考虑)
if (!user || !user.isActive) {
return res.json({
message: '如果邮箱存在,我们已发送重置链接到您的邮箱'
})
}
// 生成重置token
const resetToken = randomBytes(32).toString('hex')
const tokenHash = createHash('sha256').update(resetToken).digest('hex')
// 存储重置token(1小时有效)
await redis.setex(`password_reset:${tokenHash}`, 60 * 60, user.id)
// 发送重置邮件
const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`
await sendEmail({
to: user.email,
subject: '重置您的密码',
template: 'password-reset',
data: {
userName: user.displayName,
resetUrl,
}
})
// 记录密码重置请求
await auditLog({
userId: user.id,
action: 'PASSWORD_RESET_REQUESTED',
resource: 'User',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
})
res.json({
message: '如果邮箱存在,我们已发送重置链接到您的邮箱'
})
} catch (error) {
next(error)
}
}
// 重置密码
export const resetPassword = async (req: Request, res: Response, next: NextFunction) => {
try {
const validatedData = resetPasswordSchema.parse(req.body)
const tokenHash = createHash('sha256').update(validatedData.token).digest('hex')
const userId = await redis.get(`password_reset:${tokenHash}`)
if (!userId) {
return res.status(400).json({
error: '重置链接无效或已过期',
code: 'INVALID_TOKEN'
})
}
// 加密新密码
const hashedPassword = await bcrypt.hash(validatedData.password, 12)
// 更新密码
await prisma.user.update({
where: { id: userId },
data: { password: hashedPassword }
})
// 删除重置token
await redis.del(`password_reset:${tokenHash}`)
// 删除所有用户session(强制重新登录)
const sessionKeys = await redis.keys(`session:${userId}:*`)
if (sessionKeys.length > 0) {
await redis.del(...sessionKeys)
}
// 记录密码重置成功
await auditLog({
userId,
action: 'PASSWORD_RESET_SUCCESS',
resource: 'User',
resourceId: userId,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
})
res.json({
message: '密码重置成功,请使用新密码登录'
})
} catch (error) {
next(error)
}
}
// 获取当前用户信息
export const getCurrentUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: {
id: true,
email: true,
username: true,
displayName: true,
firstName: true,
lastName: true,
avatar: true,
bio: true,
website: true,
location: true,
role: true,
isVerified: true,
emailVerified: true,
createdAt: true,
lastLoginAt: true,
_count: {
select: {
posts: { where: { status: 'PUBLISHED' } },
followers: true,
following: true,
}
}
}
})
if (!user) {
return res.status(404).json({
error: '用户不存在',
code: 'USER_NOT_FOUND'
})
}
res.json({ user })
} catch (error) {
next(error)
}
}