Skip to main content

后端API开发实战

今天用Claude Code构建一个完整的电商后端API系统,涵盖RESTful API、GraphQL服务、中间件开发、错误处理、API文档生成等核心内容。

项目概述

项目目标

构建电商API后端系统,包括:

  • RESTful API: 完整的商品、订单、用户管理接口
  • GraphQL服务: 灵活的数据查询接口
  • 中间件系统: 认证、授权、日志、限流等
  • 错误处理: 统一的错误处理机制
  • API文档: 自动生成的Swagger文档
  • 数据验证: 请求参数验证和数据清理
  • 单元测试: 完整的测试覆盖

技术栈

  • 框架: Node.js + Express.js
  • 数据库: PostgreSQL + MongoDB(混合使用)
  • ORM: Prisma(用于PostgreSQL)
  • 认证: JWT + Passport.js
  • 验证: Joi + express-validator
  • 文档: Swagger/OpenAPI
  • GraphQL: Apollo Server
  • 测试: Jest + Supertest
  • 日志: Winston + Morgan
  • 限流: express-rate-limit
  • 缓存: Redis

项目结构预览

ecommerce-api/
├── src/
│ ├── config/ # 配置文件
│ │ ├── database.js
│ │ ├── redis.js
│ │ └── passport.js
│ ├── controllers/ # 控制器
│ │ ├── product.controller.js
│ │ ├── order.controller.js
│ │ └── user.controller.js
│ ├── middleware/ # 中间件
│ │ ├── auth.middleware.js
│ │ ├── validate.middleware.js
│ │ ├── error.middleware.js
│ │ ├── logger.middleware.js
│ │ └── rateLimit.middleware.js
│ ├── models/ # Prisma模型
│ │ └── schema.prisma
│ ├── routes/ # RESTful路由
│ │ ├── index.js
│ │ ├── products.js
│ │ ├── orders.js
│ │ └── users.js
│ ├── graphql/ # GraphQL相关
│ │ ├── schema.js
│ │ ├── resolvers.js
│ │ └── typeDefs.js
│ ├── services/ # 业务逻辑层
│ │ ├── product.service.js
│ │ ├── order.service.js
│ │ └── user.service.js
│ ├── utils/ # 工具函数
│ │ ├── validators.js
│ │ ├── helpers.js
│ │ └── constants.js
│ ├── validators/ # 请求验证模式
│ │ ├── product.validator.js
│ │ ├── order.validator.js
│ │ └── user.validator.js
│ ├── tests/ # 测试文件
│ │ ├── unit/
│ │ └── integration/
│ ├── app.js # Express应用配置
│ └── server.js # 服务器入口
├── prisma/ # Prisma配置
│ └── schema.prisma
├── docs/ # API文档
├── .env.example
├── .eslintrc.js
├── .prettierrc
├── package.json
└── README.md

第一阶段:项目初始化和基础架构

第一轮对话

启动Claude Code:

cd ~/projects
mkdir ecommerce-api
cd ecommerce-api
claude
我要创建一个电商后端API系统,包含:

1. RESTful API设计(商品、订单、用户管理)
2. GraphQL服务(灵活查询)
3. JWT认证和授权
4. 中间件系统(日志、限流、CORS)
5. 统一错误处理
6. 请求参数验证
7. Swagger API文档自动生成
8. PostgreSQL数据库(使用Prisma ORM)
9. Redis缓存
10. 单元测试和集成测试

技术栈:
- Node.js + Express
- Prisma + PostgreSQL
- Redis
- JWT + Passport
- Swagger
- Apollo Server(GraphQL)

请帮我创建完整的项目结构和基础配置。

Claude Code会立即分析需求并开始构建:

🤖 Claude: 我将帮你创建一个专业级的电商API后端系统。

让我先创建项目的基础结构和配置文件...

📁 Creating project structure...

✓ Created src/ directory with subdirectories
✓ Created prisma/ directory
✓ Created tests/ directory
✓ Created .gitignore
✓ Created .env.example
✓ Created package.json with all dependencies
✓ Created ESLint and Prettier configurations
✓ Created README.md with project documentation

Now let me set up the configuration files...

项目配置文件

package.json:

{
"name": "ecommerce-api",
"version": "1.0.0",
"description": "Professional E-commerce API Backend",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest --coverage",
"test:watch": "jest --watch",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"format": "prettier --write \"src/**/*.js\""
},
"keywords": [
"ecommerce",
"api",
"rest",
"graphql",
"express",
"prisma"
],
"author": "",
"license": "MIT",
"dependencies": {
"@prisma/client": "^5.7.0",
"apollo-server-express": "^3.12.1",
"bcryptjs": "^2.4.3",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-validator": "^7.0.1",
"graphql": "^16.8.1",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"joi": "^17.11.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"redis": "^4.6.11",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"winston": "^3.11.0"
},
"devDependencies": {
"eslint": "^8.55.0",
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"prettier": "^3.1.1",
"prisma": "^5.7.0",
"supertest": "^6.3.3"
}
}

.env.example:

# Server Configuration
NODE_ENV=development
PORT=3000
API_VERSION=v1

# Database
DATABASE_URL="postgresql://user:password@localhost:5432/ecommerce?schema=public"

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# JWT
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRE=7d
JWT_REFRESH_SECRET=your-refresh-token-secret
JWT_REFRESH_EXPIRE=30d

# CORS
CORS_ORIGIN=http://localhost:3001

# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

# Logging
LOG_LEVEL=info
LOG_FILE=logs/app.log

# Pagination
DEFAULT_PAGE=1
DEFAULT_LIMIT=20
MAX_LIMIT=100

.gitignore:

# Dependencies
node_modules/

# Environment variables
.env
.env.local
.env.*.local

# Logs
logs/
*.log
npm-debug.log*

# Prisma
prisma/migrations/

# Testing
coverage/

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Build
dist/
build/

Prisma数据库模型

prisma/schema.prisma:

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model User {
id String @id @default(uuid())
email String @unique
username String @unique
password String
role Role @default(CUSTOMER)
profile UserProfile?
addresses Address[]
orders Order[]
reviews Review[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("users")
}

model UserProfile {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
firstName String?
lastName String?
phone String?
avatar String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("user_profiles")
}

model Address {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
fullName String
phone String
province String
city String
district String
address String
postalCode String?
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("addresses")
}

model Category {
id String @id @default(uuid())
name String @unique
slug String @unique
image String?
parentId String?
parent Category? @relation("CategoryToCategory", fields: [parentId], references: [id], onDelete: SetNull)
children Category[] @relation("CategoryToCategory")
products Product[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("categories")
}

model Product {
id String @id @default(uuid())
name String
slug String @unique
description String?
price Decimal @db.Decimal(10, 2)
comparePrice Decimal? @db.Decimal(10, 2)
sku String @unique
stock Int @default(0)
images String[]
categoryId String
category Category @relation(fields: [categoryId], references: [id])
brand String?
tags String[]
attributes Json?
isActive Boolean @default(true)
reviews Review[]
orderItems OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("products")
}

model Order {
id String @id @default(uuid())
orderNumber String @unique
userId String
user User @relation(fields: [userId], references: [id])
items OrderItem[]
status OrderStatus @default(PENDING)
totalAmount Decimal @db.Decimal(10, 2)
discount Decimal @default(0) @db.Decimal(10, 2)
finalAmount Decimal @db.Decimal(10, 2)
shippingAddress Json
paymentMethod String
paymentStatus PaymentStatus @default(PENDING)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("orders")
}

model OrderItem {
id String @id @default(uuid())
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
productId String
product Product @relation(fields: [productId], references: [id])
quantity Int
price Decimal @db.Decimal(10, 2)
totalAmount Decimal @db.Decimal(10, 2)

@@map("order_items")
}

model Review {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
rating Int
comment String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("reviews")
}

enum Role {
ADMIN
CUSTOMER
}

enum OrderStatus {
PENDING
CONFIRMED
PROCESSING
SHIPPED
DELIVERED
CANCELLED
REFUNDED
}

enum PaymentStatus {
PENDING
COMPLETED
FAILED
REFUNDED
}

核心配置文件

src/config/database.js:

const { PrismaClient } = require('@prisma/client');
const logger = require('../utils/logger');

const prisma = new PrismaClient({
log: [
{ level: 'query', emit: 'event' },
{ level: 'error', emit: 'stdout' },
{ level: 'warn', emit: 'stdout' },
],
});

// 监听查询事件(开发环境)
if (process.env.NODE_ENV === 'development') {
prisma.$on('query', (e) => {
logger.debug(`Query: ${e.query}`);
logger.debug(`Duration: ${e.duration}ms`);
});
}

// 测试数据库连接
const testConnection = async () => {
try {
await prisma.$connect();
logger.info('✓ PostgreSQL connected successfully');
} catch (error) {
logger.error('PostgreSQL connection error:', error);
process.exit(1);
}
};

// 优雅关闭
const gracefulShutdown = async () => {
await prisma.$disconnect();
logger.info('PostgreSQL connection closed');
};

process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);

module.exports = { prisma, testConnection };

src/config/redis.js:

const Redis = require('ioredis');
const logger = require('../utils/logger');

const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || undefined,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
});

redis.on('connect', () => {
logger.info('✓ Redis connected successfully');
});

redis.on('error', (error) => {
logger.error('Redis connection error:', error);
});

// 测试连接
const testConnection = async () => {
try {
await redis.ping();
logger.info('✓ Redis ping successful');
} catch (error) {
logger.error('Redis ping failed:', error);
}
};

// 缓存辅助函数
const cache = {
// 获取缓存
get: async (key) => {
try {
const data = await redis.get(key);
return data ? JSON.parse(data) : null;
} catch (error) {
logger.error(`Cache get error for key ${key}:`, error);
return null;
}
},

// 设置缓存
set: async (key, value, ttl = 3600) => {
try {
await redis.setex(key, ttl, JSON.stringify(value));
return true;
} catch (error) {
logger.error(`Cache set error for key ${key}:`, error);
return false;
}
},

// 删除缓存
del: async (key) => {
try {
await redis.del(key);
return true;
} catch (error) {
logger.error(`Cache delete error for key ${key}:`, error);
return false;
}
},

// 删除匹配模式的所有缓存
delPattern: async (pattern) => {
try {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
return keys.length;
} catch (error) {
logger.error(`Cache delete pattern error for ${pattern}:`, error);
return 0;
}
},
};

module.exports = { redis, testConnection, cache };

src/utils/logger.js:

const winston = require('winston');
const path = require('path');

// 自定义格式
const customFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);

// 控制台格式(开发环境)
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ timestamp, level, message, ...metadata }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
})
);

// 创建logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: customFormat,
defaultMeta: { service: 'ecommerce-api' },
transports: [
// 错误日志文件
new winston.transports.File({
filename: path.join(process.cwd(), 'logs', 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
// 综合日志文件
new winston.transports.File({
filename: path.join(process.cwd(), 'logs', 'combined.log'),
maxsize: 5242880,
maxFiles: 5,
}),
],
});

// 开发环境添加控制台输出
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat,
}));
}

module.exports = logger;

Express应用配置

src/app.js:

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const morgan = require('morgan');
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const { graphqlHTTP } = require('express-graphql');
const logger = require('./utils/logger');
const { testConnection } = require('./config/database');
const { testConnection: testRedisConnection } = require('./config/redis');

// 导入路由
const routes = require('./routes');

// Swagger配置
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'E-commerce API',
version: '1.0.0',
description: '专业的电商后端API文档',
contact: {
name: 'API Support',
email: 'support@ecommerce.com',
},
},
servers: [
{
url: `http://localhost:${process.env.PORT || 3000}`,
description: '开发服务器',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
security: [
{
bearerAuth: [],
},
],
},
apis: ['./src/routes/*.js', './src/controllers/*.js'],
};

const swaggerSpec = swaggerJsdoc(swaggerOptions);

// 创建Express应用
const app = express();

// 安全中间件
app.use(helmet());

// CORS配置
app.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:3001',
credentials: true,
}));

// 压缩响应
app.use(compression());

// 请求解析
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// HTTP请求日志
if (process.env.NODE_ENV !== 'test') {
app.use(morgan('combined', {
stream: {
write: (message) => logger.info(message.trim()),
},
}));
}

// 健康检查
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV,
});
});

// API文档
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'E-commerce API Docs',
}));

// API路由
app.use('/api', routes);

// GraphQL路由(稍后实现)
// app.use('/graphql', graphqlHTTP({ ... }));

// 404处理
app.use((req, res) => {
res.status(404).json({
success: false,
message: `Route ${req.originalUrl} not found`,
});
});

// 初始化数据库连接
const initializeDatabase = async () => {
await testConnection();
await testRedisConnection();
};

module.exports = { app, initializeDatabase };

src/server.js:

require('dotenv').config();
const { app, initializeDatabase } = require('./app');
const logger = require('./utils/logger');

const PORT = process.env.PORT || 3000;

const startServer = async () => {
try {
// 初始化数据库连接
await initializeDatabase();

// 启动服务器
const server = app.listen(PORT, () => {
logger.info(`
╔════════════════════════════════════════════╗
║ ║
║ 🚀 E-commerce API Server Started ║
║ ║
║ Environment: ${process.env.NODE_ENV || 'development'}
║ Port: ${PORT}
║ API: http://localhost:${PORT}/api ║
║ Docs: http://localhost:${PORT}/api-docs ║
║ ║
╚════════════════════════════════════════════╝
`);
});

// 优雅关闭
const gracefulShutdown = (signal) => {
logger.info(`${signal} received. Closing server gracefully...`);
server.close(() => {
logger.info('Server closed');
process.exit(0);
});

// 强制关闭超时
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
};

startServer();

第二阶段:RESTful API设计

创建RESTful路由

创建RESTful API路由:

1. 产品路由:
- GET /api/products (列表,支持分页、过滤、排序)
- GET /api/products/:id (详情)
- POST /api/products (创建,需要管理员权限)
- PUT /api/products/:id (更新,需要管理员权限)
- DELETE /api/products/:id (删除,需要管理员权限)

2. 订单路由:
- GET /api/orders (我的订单列表)
- GET /api/orders/:id (订单详情)
- POST /api/orders (创建订单)
- PUT /api/orders/:id (更新订单状态,管理员)

3. 用户路由:
- POST /api/users/register (注册)
- POST /api/users/login (登录)
- GET /api/users/profile (获取个人信息)
- PUT /api/users/profile (更新个人信息)

请创建完整的路由文件,包含Swagger文档注释。

中间件系统实现

src/middleware/auth.middleware.js:

const jwt = require('jsonwebtoken');
const { prisma } = require('../config/database');
const logger = require('../utils/logger');
const { cache } = require('../config/redis');

/**
* 验证JWT token
*/
const authenticate = async (req, res, next) => {
try {
// 从请求头获取token
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: '未提供认证token',
});
}

const token = authHeader.substring(7);

// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET);

// 检查缓存中是否有用户信息
const cacheKey = `user:${decoded.userId}`;
let user = await cache.get(cacheKey);

if (!user) {
// 从数据库获取用户
user = await prisma.user.findUnique({
where: { id: decoded.userId },
include: { profile: true },
});

if (!user) {
return res.status(401).json({
success: false,
message: '用户不存在',
});
}

// 缓存用户信息(1小时)
await cache.set(cacheKey, user, 3600);
}

// 移除密码字段
delete user.password;

// 将用户信息添加到请求对象
req.user = user;
next();
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: '无效的token',
});
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token已过期',
});
}
logger.error('Authentication error:', error);
res.status(500).json({
success: false,
message: '认证失败',
});
}
};

/**
* 检查用户角色
*/
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: '未认证',
});
}

if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: '权限不足',
});
}

next();
};
};

/**
* 可选认证(不强制要求登录)
*/
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next();
}

const token = authHeader.substring(7);
const decoded = jwt.verify(token, process.env.JWT_SECRET);

const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, email: true, username: true, role: true },
});

if (user) {
req.user = user;
}

next();
} catch (error) {
// 忽略错误,继续处理请求
next();
}
};

module.exports = { authenticate, authorize, optionalAuth };

src/middleware/validate.middleware.js:

const { validationResult } = require('express-validator');
const logger = require('../utils/logger');

/**
* 验证请求结果
*/
const validate = (req, res, next) => {
const errors = validationResult(req);

if (!errors.isEmpty()) {
logger.warn('Validation error:', errors.array());

return res.status(400).json({
success: false,
message: '请求参数验证失败',
errors: errors.array().map(err => ({
field: err.path,
message: err.msg,
value: err.value,
})),
});
}

next();
};

/**
* 验证ObjectId
*/
const isValidObjectId = (paramName) => {
return (req, res, next) => {
const id = req.params[paramName];

// UUID验证正则
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

if (!uuidRegex.test(id)) {
return res.status(400).json({
success: false,
message: `无效的${paramName}格式`,
});
}

next();
};
};

module.exports = { validate, isValidObjectId };

src/middleware/error.middleware.js:

const logger = require('../utils/logger');

/**
* 全局错误处理中间件
*/
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;

// 记录错误日志
logger.error('Error:', {
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip,
});

// Prisma错误处理
if (err.code === 'P2002') {
const message = '唯一字段冲突: ' + err.meta.target.join(', ');
error = { message, statusCode: 400 };
}

if (err.code === 'P2025') {
error = { message: '记录不存在', statusCode: 404 };
}

// 验证错误
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = { message, statusCode: 400 };
}

// JWT错误
if (err.name === 'JsonWebTokenError') {
error = { message: '无效的token', statusCode: 401 };
}

if (err.name === 'TokenExpiredError') {
error = { message: 'Token已过期', statusCode: 401 };
}

res.status(error.statusCode || 500).json({
success: false,
message: error.message || '服务器内部错误',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};

/**
* 404处理
*/
const notFound = (req, res, next) => {
res.status(404).json({
success: false,
message: `路由 ${req.originalUrl} 不存在`,
});
};

/**
* Async错误包装器
*/
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};

module.exports = { errorHandler, notFound, asyncHandler };

src/middleware/rateLimit.middleware.js:

const rateLimit = require('express-rate-limit');
const { redis } = require('../config/redis');

/**
* Redis存储适配器
*/
class RedisStore {
constructor(client) {
this.client = client;
}

async increment(key) {
const replies = await this.client
.multi()
.incr(key)
.expire(key, 60) // 60秒过期
.exec();

return {
totalHits: replies[0][1],
resetTime: new Date(Date.now() + 60000),
};
}

async decrement(key) {
await this.client.decr(key);
}

async resetKey(key) {
await this.client.del(key);
}
}

/**
* 创建限流器
*/
const createRateLimiter = ({
windowMs = 15 * 60 * 1000, // 15分钟
max = 100,
message = '请求过于频繁,请稍后再试',
standardHeaders = true,
legacyHeaders = false,
} = {}) => {
const options = {
windowMs,
max,
message: {
success: false,
message,
},
standardHeaders,
legacyHeaders,
};

// 如果Redis可用,使用Redis存储
if (redis.status === 'ready') {
options.store = new RedisStore(redis);
}

return rateLimit(options);
};

/**
* API限流
*/
const apiLimiter = createRateLimiter({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100,
message: 'API请求过于频繁,请稍后再试',
});

/**
* 登录限流
*/
const loginLimiter = createRateLimiter({
windowMs: 15 * 60 * 1000,
max: 5,
message: '登录尝试次数过多,请15分钟后再试',
});

/**
* 注册限流
*/
const registerLimiter = createRateLimiter({
windowMs: 60 * 60 * 1000, // 1小时
max: 3,
message: '注册请求过多,请1小时后再试',
});

module.exports = {
apiLimiter,
loginLimiter,
registerLimiter,
createRateLimiter,
};

请求验证模式

src/validators/product.validator.js:

const { body, param, query } = require('express-validator');
const { isValidObjectId } = require('../middleware/validate.middleware');

/**
* 验证创建产品
*/
const validateCreateProduct = [
body('name')
.trim()
.notEmpty().withMessage('产品名称不能为空')
.isLength({ min: 3, max: 200 }).withMessage('产品名称长度应在3-200之间'),

body('description')
.optional()
.trim()
.isLength({ max: 2000 }).withMessage('描述不能超过2000字符'),

body('price')
.isFloat({ min: 0 }).withMessage('价格必须大于等于0')
.toFloat(),

body('comparePrice')
.optional()
.isFloat({ min: 0 }).withMessage('对比价格必须大于等于0')
.toFloat(),

body('sku')
.trim()
.notEmpty().withMessage('SKU不能为空')
.isAlphanumeric().withMessage('SKU只能包含字母和数字'),

body('stock')
.optional()
.isInt({ min: 0 }).withMessage('库存必须大于等于0')
.toInt(),

body('categoryId')
.notEmpty().withMessage('分类ID不能为空')
.isUUID().withMessage('分类ID格式不正确'),

body('images')
.optional()
.isArray().withMessage('images必须是数组')
.custom((images) => {
if (images.some(img => typeof img !== 'string')) {
throw new Error('所有图片必须是字符串URL');
}
return true;
}),

body('tags')
.optional()
.isArray().withMessage('tags必须是数组'),

body('isActive')
.optional()
.isBoolean().withMessage('isActive必须是布尔值')
.toBoolean(),
];

/**
* 验证更新产品
*/
const validateUpdateProduct = [
param('id').custom(isValidObjectId('id')),

body('name')
.optional()
.trim()
.notEmpty().withMessage('产品名称不能为空')
.isLength({ min: 3, max: 200 }).withMessage('产品名称长度应在3-200之间'),

body('price')
.optional()
.isFloat({ min: 0 }).withMessage('价格必须大于等于0')
.toFloat(),

body('stock')
.optional()
.isInt({ min: 0 }).withMessage('库存必须大于等于0')
.toInt(),

body('isActive')
.optional()
.isBoolean().withMessage('isActive必须是布尔值')
.toBoolean(),
];

/**
* 验证产品查询参数
*/
const validateProductQuery = [
query('page')
.optional()
.isInt({ min: 1 }).withMessage('页码必须大于0')
.toInt(),

query('limit')
.optional()
.isInt({ min: 1, max: 100 }).withMessage('每页数量应在1-100之间')
.toInt(),

query('category')
.optional()
.isUUID().withMessage('分类ID格式不正确'),

query('minPrice')
.optional()
.isFloat({ min: 0 }).withMessage('最低价格必须大于等于0'),

query('maxPrice')
.optional()
.isFloat({ min: 0 }).withMessage('最高价格必须大于等于0'),

query('search')
.optional()
.trim()
.isLength({ min: 1 }).withMessage('搜索关键词不能为空'),

query('sortBy')
.optional()
.isIn(['createdAt', 'price', 'name', 'sales']).withMessage('排序字段不正确'),

query('order')
.optional()
.isIn(['asc', 'desc']).withMessage('排序方向只能是asc或desc'),
];

module.exports = {
validateCreateProduct,
validateUpdateProduct,
validateProductQuery,
};

产品控制器

src/controllers/product.controller.js:

const { prisma } = require('../config/database');
const { cache } = require('../config/redis');
const { asyncHandler } = require('../middleware/error.middleware');
const logger = require('../utils/logger');

/**
* 获取产品列表
* @route GET /api/products
* @access Public
*/
exports.getProducts = asyncHandler(async (req, res) => {
const {
page = 1,
limit = 20,
category,
minPrice,
maxPrice,
search,
sortBy = 'createdAt',
order = 'desc',
tags,
} = req.query;

// 构建缓存键
const cacheKey = `products:${JSON.stringify(req.query)}`;

// 尝试从缓存获取
const cached = await cache.get(cacheKey);
if (cached) {
logger.info('Returning cached products');
return res.json({
success: true,
...cached,
});
}

// 构建查询条件
const where = {
isActive: true,
};

if (category) {
where.categoryId = category;
}

if (minPrice !== undefined || maxPrice !== undefined) {
where.price = {};
if (minPrice !== undefined) where.price.gte = parseFloat(minPrice);
if (maxPrice !== undefined) where.price.lte = parseFloat(maxPrice);
}

if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
];
}

if (tags) {
const tagArray = Array.isArray(tags) ? tags : tags.split(',');
where.tags = { hasSome: tagArray };
}

// 计算分页
const skip = (page - 1) * limit;

// 查询总数和数据
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
skip,
take: parseInt(limit),
orderBy: { [sortBy]: order },
include: {
category: {
select: { id: true, name: true, slug: true },
},
},
}),
prisma.product.count({ where }),
]);

const result = {
success: true,
data: {
products,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit),
},
},
};

// 缓存结果(5分钟)
await cache.set(cacheKey, result, 300);

res.json(result);
});

/**
* 获取产品详情
* @route GET /api/products/:id
* @access Public
*/
exports.getProductById = asyncHandler(async (req, res) => {
const { id } = req.params;

// 检查缓存
const cacheKey = `product:${id}`;
const cached = await cache.get(cacheKey);

if (cached) {
return res.json({
success: true,
data: cached,
});
}

const product = await prisma.product.findUnique({
where: { id },
include: {
category: true,
reviews: {
take: 10,
orderBy: { createdAt: 'desc' },
include: {
user: {
select: { id: true, username: true },
},
},
},
},
});

if (!product) {
return res.status(404).json({
success: false,
message: '产品不存在',
});
}

// 计算平均评分
const avgRating = product.reviews.length > 0
? product.reviews.reduce((sum, r) => sum + r.rating, 0) / product.reviews.length
: 0;

const result = {
...product,
avgRating: Math.round(avgRating * 10) / 10,
reviewCount: product.reviews.length,
};

// 缓存结果(10分钟)
await cache.set(cacheKey, result, 600);

res.json({
success: true,
data: result,
});
});

/**
* 创建产品
* @route POST /api/products
* @access Admin
*/
exports.createProduct = asyncHandler(async (req, res) => {
const {
name,
description,
price,
comparePrice,
sku,
stock,
categoryId,
images,
brand,
tags,
attributes,
isActive,
} = req.body;

// 生成slug
const slug = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');

const product = await prisma.product.create({
data: {
name,
slug,
description,
price: parseFloat(price),
comparePrice: comparePrice ? parseFloat(comparePrice) : null,
sku,
stock: parseInt(stock) || 0,
categoryId,
images: images || [],
brand,
tags: tags || [],
attributes,
isActive: isActive !== undefined ? isActive : true,
},
include: {
category: true,
},
});

// 清除产品列表缓存
await cache.delPattern('products:*');

logger.info(`Product created: ${product.id}`);

res.status(201).json({
success: true,
message: '产品创建成功',
data: product,
});
});

/**
* 更新产品
* @route PUT /api/products/:id
* @access Admin
*/
exports.updateProduct = asyncHandler(async (req, res) => {
const { id } = req.params;
const updateData = req.body;

// 如果更新name,重新生成slug
if (updateData.name) {
updateData.slug = updateData.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}

const product = await prisma.product.update({
where: { id },
data: updateData,
include: {
category: true,
},
});

// 清除相关缓存
await cache.del(`product:${id}`);
await cache.delPattern('products:*');

logger.info(`Product updated: ${id}`);

res.json({
success: true,
message: '产品更新成功',
data: product,
});
});

/**
* 删除产品
* @route DELETE /api/products/:id
* @access Admin
*/
exports.deleteProduct = asyncHandler(async (req, res) => {
const { id } = req.params;

await prisma.product.delete({
where: { id },
});

// 清除相关缓存
await cache.del(`product:${id}`);
await cache.delPattern('products:*');

logger.info(`Product deleted: ${id}`);

res.json({
success: true,
message: '产品删除成功',
});
});

路由集成

src/routes/products.js:

const express = require('express');
const router = express.Router();
const productController = require('../controllers/product.controller');
const { authenticate, authorize } = require('../middleware/auth.middleware');
const { validate } = require('../middleware/validate.middleware');
const {
validateCreateProduct,
validateUpdateProduct,
validateProductQuery,
} = require('../validators/product.validator');

/**
* @swagger
* components:
* schemas:
* Product:
* type: object
* properties:
* id:
* type: string
* format: uuid
* name:
* type: string
* slug:
* type: string
* description:
* type: string
* price:
* type: number
* format: float
* stock:
* type: integer
* category:
* $ref: '#/components/schemas/Category'
*/

/**
* @swagger
* /api/products:
* get:
* summary: 获取产品列表
* tags: [Products]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* description: 每页数量
* - in: query
* name: category
* schema:
* type: string
* description: 分类ID
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* responses:
* 200:
* description: 成功获取产品列表
*/
router.get(
'/',
validateProductQuery,
validate,
productController.getProducts
);

/**
* @swagger
* /api/products/{id}:
* get:
* summary: 获取产品详情
* tags: [Products]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: 产品ID
* responses:
* 200:
* description: 成功获取产品详情
* 404:
* description: 产品不存在
*/
router.get(
'/:id',
productController.getProductById
);

/**
* @swagger
* /api/products:
* post:
* summary: 创建产品
* tags: [Products]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* price:
* type: number
* sku:
* type: string
* categoryId:
* type: string
* responses:
* 201:
* description: 产品创建成功
* 400:
* description: 请求参数错误
* 401:
* description: 未认证
* 403:
* description: 权限不足
*/
router.post(
'/',
authenticate,
authorize('ADMIN'),
validateCreateProduct,
validate,
productController.createProduct
);

/**
* @swagger
* /api/products/{id}:
* put:
* summary: 更新产品
* tags: [Products]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 产品更新成功
*/
router.put(
'/:id',
authenticate,
authorize('ADMIN'),
validateUpdateProduct,
validate,
productController.updateProduct
);

/**
* @swagger
* /api/products/{id}:
* delete:
* summary: 删除产品
* tags: [Products]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 产品删除成功
*/
router.delete(
'/:id',
authenticate,
authorize('ADMIN'),
productController.deleteProduct
);

module.exports = router;

src/routes/index.js:

const express = require('express');
const router = express.Router();
const productRoutes = require('./products');
const orderRoutes = require('./orders');
const userRoutes = require('./users');

// API路由
router.use('/products', productRoutes);
router.use('/orders', orderRoutes);
router.use('/users', userRoutes);

module.exports = router;

第三阶段:GraphQL服务

添加GraphQL支持

添加GraphQL支持,需要:

1. 定义GraphQL Schema(typeDefs)
2. 实现Resolvers
3. 集成Apollo Server
4. 支持以下功能:
- 产品查询(列表、详情、搜索)
- 用户认证
- 订单查询和创建
- 分页支持
- 错误处理

请创建完整的GraphQL服务。

src/graphql/typeDefs.js:

const { gql } = require('apollo-server-express');

const typeDefs = gql`
type User {
id: ID!
email: String!
username: String!
role: String!
profile: UserProfile
createdAt: String!
}

type UserProfile {
id: ID!
firstName: String
lastName: String
phone: String
avatar: String
}

type Category {
id: ID!
name: String!
slug: String!
image: String
parent: Category
children: [Category!]
}

type Product {
id: ID!
name: String!
slug: String!
description: String
price: Float!
comparePrice: Float
sku: String!
stock: Int!
images: [String!]!
category: Category!
brand: String
tags: [String!]!
attributes: String
isActive: Boolean!
avgRating: Float
reviewCount: Int!
createdAt: String!
updatedAt: String!
}

type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}

type ProductEdge {
node: Product!
cursor: String!
}

type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}

type Address {
id: ID!
fullName: String!
phone: String!
province: String!
city: String!
district: String!
address: String!
postalCode: String
isDefault: Boolean!
}

type OrderItem {
id: ID!
product: Product!
quantity: Int!
price: Float!
totalAmount: Float!
}

enum OrderStatus {
PENDING
CONFIRMED
PROCESSING
SHIPPED
DELIVERED
CANCELLED
REFUNDED
}

enum PaymentStatus {
PENDING
COMPLETED
FAILED
REFUNDED
}

type Order {
id: ID!
orderNumber: String!
user: User!
items: [OrderItem!]!
status: OrderStatus!
totalAmount: Float!
discount: Float!
finalAmount: Float!
shippingAddress: Address!
paymentMethod: String!
paymentStatus: PaymentStatus!
notes: String
createdAt: String!
updatedAt: String!
}

type AuthPayload {
token: String!
refreshToken: String!
user: User!
}

input ProductInput {
name: String!
description: String
price: Float!
comparePrice: Float
sku: String!
stock: Int
categoryId: String!
images: [String!]
brand: String
tags: [String!]
attributes: String
isActive: Boolean
}

input AddressInput {
fullName: String!
phone: String!
province: String!
city: String!
district: String!
address: String!
postalCode: String
}

input OrderItemInput {
productId: String!
quantity: Int!
}

enum ProductSortBy {
createdAt
price
name
sales
}

enum SortOrder {
asc
desc
}

type Query {
# 产品查询
products(
first: Int
after: String
where: ProductWhereInput
sortBy: ProductSortBy
sortOrder: SortOrder
): ProductConnection!

product(id: ID!): Product
productBySlug(slug: String!): Product
searchProducts(query: String!, first: Int): ProductConnection!

# 分类查询
categories: [Category!]!
category(id: ID!): Category
categoryBySlug(slug: String!): Category

# 用户查询
me: User
myOrders(first: Int, after: String): ProductConnection!

# 订单查询
order(id: ID!): Order
}

type Mutation {
# 认证
register(email: String!, username: String!, password: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
refreshToken(token: String!): AuthPayload!

# 用户
updateProfile(input: UserProfileInput): User!
changePassword(oldPassword: String!, newPassword: String!): Boolean!

# 产品(管理员)
createProduct(input: ProductInput!): Product!
updateProduct(id: ID!, input: ProductInput!): Product!
deleteProduct(id: ID!): Boolean!

# 订单
createOrder(
items: [OrderItemInput!]!
shippingAddress: AddressInput!
paymentMethod: String!
notes: String
): Order!

cancelOrder(orderId: ID!): Order!
}

input ProductWhereInput {
categoryId: String
minPrice: Float
maxPrice: Float
tags: [String!]
isActive: Boolean
}

input UserProfileInput {
firstName: String
lastName: String
phone: String
avatar: String
}
`;

module.exports = typeDefs;

src/graphql/resolvers.js:

const { prisma } = require('../config/database');
const { AuthenticationError, ForbiddenError } = require('apollo-server-express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { cache } = require('../config/redis');

/**
* 从请求上下文获取用户
*/
const getUser = (context) => {
if (!context.user) {
throw new AuthenticationError('未认证');
}
return context.user;
};

/**
* Resolvers
*/
const resolvers = {
Query: {
/**
* 获取产品列表(支持分页)
*/
products: async (_, { first = 20, after, where = {}, sortBy = 'createdAt', sortOrder = 'desc' }) => {
const take = first;
let skip = 0;
let cursor = null;

if (after) {
cursor = after;
skip = 1;
}

const prismaWhere = {
...where,
isActive: where.isActive !== undefined ? where.isActive : true,
};

const [products, totalCount] = await Promise.all([
prisma.product.findMany({
where: prismaWhere,
take,
skip,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { [sortBy]: sortOrder },
include: {
category: true,
},
}),
prisma.product.count({ where: prismaWhere }),
]);

const edges = products.map((product) => ({
node: product,
cursor: product.id,
}));

const pageInfo = {
hasNextPage: skip + products.length < totalCount,
hasPreviousPage: !!after,
startCursor: edges.length > 0 ? edges[0].cursor : null,
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
};

return {
edges,
pageInfo,
totalCount,
};
},

/**
* 获取单个产品
*/
product: async (_, { id }) => {
const cacheKey = `graphql:product:${id}`;
const cached = await cache.get(cacheKey);

if (cached) return cached;

const product = await prisma.product.findUnique({
where: { id },
include: {
category: true,
reviews: {
take: 10,
orderBy: { createdAt: 'desc' },
include: {
user: {
select: { id: true, username: true },
},
},
},
},
});

if (!product) {
throw new Error('产品不存在');
}

const avgRating = product.reviews.length > 0
? product.reviews.reduce((sum, r) => sum + r.rating, 0) / product.reviews.length
: 0;

const result = {
...product,
avgRating: Math.round(avgRating * 10) / 10,
reviewCount: product.reviews.length,
};

await cache.set(cacheKey, result, 600);

return result;
},

/**
* 通过slug获取产品
*/
productBySlug: async (_, { slug }) => {
const product = await prisma.product.findUnique({
where: { slug },
include: { category: true },
});

return product;
},

/**
* 搜索产品
*/
searchProducts: async (_, { query, first = 20 }) => {
const products = await prisma.product.findMany({
where: {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
{ tags: { hasSome: [query] } },
],
isActive: true,
},
take: first,
include: { category: true },
});

return {
edges: products.map((p) => ({ node: p, cursor: p.id })),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: products.length,
};
},

/**
* 获取所有分类
*/
categories: async () => {
return await prisma.category.findMany({
include: {
parent: true,
children: true,
},
});
},

/**
* 获取当前用户
*/
me: async (_, __, context) => {
return context.user || null;
},

/**
* 获取我的订单
*/
myOrders: async (_, { first = 20, after }, context) => {
const user = getUser(context);

const take = first;
let skip = 0;

if (after) {
skip = 1;
}

const orders = await prisma.order.findMany({
where: { userId: user.id },
take,
skip,
cursor: after ? { id: after } : undefined,
orderBy: { createdAt: 'desc' },
include: {
items: {
include: {
product: true,
},
},
},
});

return {
edges: orders.map((o) => ({ node: o, cursor: o.id })),
pageInfo: {
hasNextPage: false,
hasPreviousPage: !!after,
},
totalCount: orders.length,
};
},
},

Mutation: {
/**
* 用户注册
*/
register: async (_, { email, username, password }) => {
// 检查用户是否存在
const existingUser = await prisma.user.findFirst({
where: { OR: [{ email }, { username }] },
});

if (existingUser) {
throw new Error('邮箱或用户名已存在');
}

// 哈希密码
const hashedPassword = await bcrypt.hash(password, 10);

// 创建用户
const user = await prisma.user.create({
data: {
email,
username,
password: hashedPassword,
role: 'CUSTOMER',
},
});

// 生成token
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);

const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '30d' }
);

return {
token,
refreshToken,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role,
createdAt: user.createdAt,
},
};
},

/**
* 用户登录
*/
login: async (_, { email, password }) => {
const user = await prisma.user.findUnique({
where: { email },
});

if (!user) {
throw new AuthenticationError('邮箱或密码错误');
}

const isValid = await bcrypt.compare(password, user.password);

if (!isValid) {
throw new AuthenticationError('邮箱或密码错误');
}

const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);

const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '30d' }
);

return {
token,
refreshToken,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role,
createdAt: user.createdAt,
},
};
},

/**
* 创建订单
*/
createOrder: async (_, { items, shippingAddress, paymentMethod, notes }, context) => {
const user = getUser(context);

// 获取产品信息
const productIds = items.map(item => item.productId);
const products = await prisma.product.findMany({
where: { id: { in: productIds } },
});

// 构建订单项
const orderItems = items.map(item => {
const product = products.find(p => p.id === item.productId);
if (!product) {
throw new Error(`产品 ${item.productId} 不存在`);
}
return {
productId: item.productId,
quantity: item.quantity,
price: product.price,
totalAmount: parseFloat(product.price) * item.quantity,
};
});

// 计算总金额
const totalAmount = orderItems.reduce((sum, item) => sum + item.totalAmount, 0);

// 生成订单号
const orderNumber = `ORD${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;

// 创建订单
const order = await prisma.order.create({
data: {
orderNumber,
userId: user.id,
totalAmount,
discount: 0,
finalAmount: totalAmount,
shippingAddress,
paymentMethod,
notes,
items: {
create: orderItems,
},
},
include: {
items: {
include: {
product: true,
},
},
},
});

// 更新产品库存
for (const item of items) {
await prisma.product.update({
where: { id: item.productId },
data: {
stock: {
decrement: item.quantity,
},
},
});
}

return order;
},

/**
* 创建产品(管理员)
*/
createProduct: async (_, { input }, context) => {
const user = getUser(context);

if (user.role !== 'ADMIN') {
throw new ForbiddenError('权限不足');
}

const slug = input.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');

const product = await prisma.product.create({
data: {
...input,
slug,
price: parseFloat(input.price),
comparePrice: input.comparePrice ? parseFloat(input.comparePrice) : null,
stock: parseInt(input.stock) || 0,
},
include: {
category: true,
},
});

// 清除缓存
await cache.delPattern('products:*');

return product;
},

/**
* 取消订单
*/
cancelOrder: async (_, { orderId }, context) => {
const user = getUser(context);

const order = await prisma.order.findFirst({
where: {
id: orderId,
userId: user.id,
},
});

if (!order) {
throw new Error('订单不存在');
}

if (order.status !== 'PENDING') {
throw new Error('只能取消待处理订单');
}

const updatedOrder = await prisma.order.update({
where: { id: orderId },
data: { status: 'CANCELLED' },
});

// 恢复库存
for (const item of order.items) {
await prisma.product.update({
where: { id: item.productId },
data: {
stock: {
increment: item.quantity,
},
},
});
}

return updatedOrder;
},
},

// 字段级resolvers
Product: {
avgRating: async (parent) => {
const reviews = await prisma.review.findMany({
where: { productId: parent.id },
});

if (reviews.length === 0) return 0;

return reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
},

reviewCount: async (parent) => {
return await prisma.review.count({
where: { productId: parent.id },
});
},
},
};

module.exports = resolvers;

GraphQL服务器集成

更新src/app.js添加GraphQL支持:

const { ApolloServer } = require('apollo-server-express');
const typeDefs = require('./graphql/typeDefs');
const resolvers = require('./graphql/resolvers');
const { authenticate, optionalAuth } = require('./middleware/auth.middleware');

// 创建Apollo Server
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// 获取用户信息
let user = null;

try {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
const decoded = jwt.verify(token, process.env.JWT_SECRET);
user = decoded;
}
} catch (error) {
// 忽略错误
}

return {
user,
req,
};
},
formatError: (error) => {
logger.error('GraphQL error:', error);
return {
message: error.message,
code: error.extensions?.code || 'INTERNAL_SERVER_ERROR',
};
},
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production',
});

// 在路由之前添加GraphQL
async function startApollo() {
await apolloServer.start();
apolloServer.applyMiddleware({ app, path: '/graphql' });
}

module.exports = { app, initializeDatabase, startApollo };

第四阶段:API文档生成

Swagger配置与文档

src/routes中添加详细的Swagger注释:

/**
* @swagger
* tags:
* name: Products
* description: 产品管理接口
*/

/**
* @swagger
* components:
* schemas:
* Product:
* type: object
* required:
* - id
* - name
* - price
* - sku
* properties:
* id:
* type: string
* format: uuid
* description: 产品唯一标识
* name:
* type: string
* description: 产品名称
* slug:
* type: string
* description: URL友好的产品标识
* description:
* type: string
* description: 产品描述
* price:
* type: number
* format: float
* description: 产品价格
* comparePrice:
* type: number
* format: float
* description: 对比价格(原价)
* sku:
* type: string
* description: 库存单位
* stock:
* type: integer
* description: 库存数量
* images:
* type: array
* items:
* type: string
* category:
* $ref: '#/components/schemas/Category'
* Category:
* type: object
* properties:
* id:
* type: string
* format: uuid
* name:
* type: string
* slug:
* type: string
* Error:
* type: object
* properties:
* success:
* type: boolean
* example: false
* message:
* type: string
* errors:
* type: array
* items:
* type: object
*/

/**
* @swagger
* /api/products:
* get:
* summary: 获取产品列表
* tags: [Products]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* minimum: 1
* maximum: 100
* description: 每页数量
* - in: query
* name: category
* schema:
* type: string
* description: 按分类筛选
* - in: query
* name: minPrice
* schema:
* type: number
* description: 最低价格
* - in: query
* name: maxPrice
* schema:
* type: number
* description: 最高价格
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: sortBy
* schema:
* type: string
* enum: [createdAt, price, name, sales]
* default: createdAt
* description: 排序字段
* - in: query
* name: order
* schema:
* type: string
* enum: [asc, desc]
* default: desc
* description: 排序方向
* responses:
* 200:
* description: 成功获取产品列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* products:
* type: array
* items:
* $ref: '#/components/schemas/Product'
* pagination:
* type: object
* properties:
* page:
* type: integer
* limit:
* type: integer
* total:
* type: integer
* pages:
* type: integer
* 400:
* description: 请求参数错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/

/**
* @swagger
* /api/products/{id}:
* get:
* summary: 获取产品详情
* tags: [Products]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: 产品UUID
* responses:
* 200:
* description: 成功获取产品详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/Product'
* 404:
* description: 产品不存在
*/

/**
* @swagger
* /api/products:
* post:
* summary: 创建新产品
* tags: [Products]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - price
* - sku
* - categoryId
* properties:
* name:
* type: string
* description:
* type: string
* price:
* type: number
* sku:
* type: string
* categoryId:
* type: string
* stock:
* type: integer
* images:
* type: array
* items:
* type: string
* responses:
* 201:
* description: 产品创建成功
* 400:
* description: 请求参数错误
* 401:
* description: 未认证
* 403:
* description: 权限不足(仅管理员)
*/

/**
* @swagger
* /api/products/{id}:
* put:
* summary: 更新产品信息
* tags: [Products]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* price:
* type: number
* stock:
* type: integer
* isActive:
* type: boolean
* responses:
* 200:
* description: 更新成功
* 401:
* description: 未认证
* 403:
* description: 权限不足
* 404:
* description: 产品不存在
*/

/**
* @swagger
* /api/products/{id}:
* delete:
* summary: 删除产品
* tags: [Products]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: 删除成功
* 401:
* description: 未认证
* 403:
* description: 权限不足
* 404:
* description: 产品不存在
*/

第五阶段:测试和调试

单元测试

tests/unit/product.service.test.js:

const { prisma } = require('../../src/config/database');
const { cache } = require('../../src/config/redis');

describe('Product Service', () => {
beforeAll(async () => {
await prisma.$connect();
});

afterAll(async () => {
await prisma.$disconnect();
});

beforeEach(async () => {
// 清理测试数据
await prisma.product.deleteMany({});
await cache.delPattern('products:*');
});

describe('getProducts', () => {
it('should return paginated products', async () => {
// 创建测试数据
await prisma.product.createMany({
data: [
{
name: 'Product 1',
slug: 'product-1',
sku: 'SKU001',
price: 100,
categoryId: 'test-category-id',
},
{
name: 'Product 2',
slug: 'product-2',
sku: 'SKU002',
price: 200,
categoryId: 'test-category-id',
},
],
});

const result = await getProducts({ page: 1, limit: 10 });

expect(result.products).toHaveLength(2);
expect(result.pagination.total).toBe(2);
expect(result.pagination.pages).toBe(1);
});

it('should filter products by price range', async () => {
await prisma.product.createMany({
data: [
{
name: 'Product 1',
slug: 'product-1',
sku: 'SKU001',
price: 50,
categoryId: 'test-category-id',
},
{
name: 'Product 2',
slug: 'product-2',
sku: 'SKU002',
price: 150,
categoryId: 'test-category-id',
},
],
});

const result = await getProducts({
minPrice: 100,
maxPrice: 200,
});

expect(result.products).toHaveLength(1);
expect(result.products[0].price).toBe(150);
});
});
});

集成测试

tests/integration/products.test.js:

const request = require('supertest');
const { app } = require('../../src/app');
const { prisma } = require('../../src/config/database');

describe('Product API Integration Tests', () => {
let authToken;

beforeAll(async () => {
// 创建测试用户并获取token
const response = await request(app)
.post('/api/users/register')
.send({
email: 'test@example.com',
username: 'testuser',
password: 'password123',
});

authToken = response.body.data.token;
});

afterAll(async () => {
await prisma.$disconnect();
});

describe('GET /api/products', () => {
it('should return products list', async () => {
const response = await request(app)
.get('/api/products')
.expect(200);

expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data.products)).toBe(true);
expect(response.body.data.pagination).toBeDefined();
});

it('should support pagination', async () => {
const response = await request(app)
.get('/api/products?page=1&limit=10')
.expect(200);

expect(response.body.data.pagination.page).toBe(1);
expect(response.body.data.pagination.limit).toBe(10);
});
});

describe('POST /api/products', () => {
it('should create product as admin', async () => {
const newProduct = {
name: 'Test Product',
price: 99.99,
sku: 'TEST001',
categoryId: 'category-id',
};

const response = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${authToken}`)
.send(newProduct)
.expect(201);

expect(response.body.success).toBe(true);
expect(response.body.data.name).toBe(newProduct.name);
});

it('should fail without authentication', async () => {
const response = await request(app)
.post('/api/products')
.send({
name: 'Test Product',
price: 99.99,
})
.expect(401);

expect(response.body.success).toBe(false);
});
});
});

第六阶段:部署和优化

Docker配置

Dockerfile:

FROM node:18-alpine

WORKDIR /app

# 安装依赖
COPY package*.json ./
RUN npm ci --only=production

# 复制源代码
COPY . .

# 生成Prisma Client
RUN npx prisma generate

# 暴露端口
EXPOSE 3000

# 启动应用
CMD ["npm", "start"]

docker-compose.yml:

version: '3.8'

services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@db:5432/ecommerce
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- db
- redis

db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=ecommerce
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"

redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data

volumes:
postgres_data:
redis_data:

性能优化清单

为电商API提供性能优化建议

优化方案:

  1. 数据库优化

    • 添加索引到常用查询字段
    • 使用数据库连接池
    • 实现查询结果缓存
  2. Redis缓存策略

    • 产品详情缓存
    • 分类列表缓存
    • 热门产品缓存
    • 用户会话缓存
  3. API响应优化

    • 实现响应压缩
    • 减少不必要的数据字段
    • 使用字段选择(GraphQL)
  4. 负载均衡

    • 使用Nginx反向代理
    • 实现API服务集群

最佳实践总结

1. RESTful API设计原则

  • 使用名词而非动词
  • 保持URL层次清晰
  • 使用HTTP动词表示操作
  • 实现HATEOAS(可选)
  • 版本控制

2. 错误处理最佳实践

// 统一错误响应格式
{
"success": false,
"message": "错误描述",
"errors": [
{
"field": "email",
"message": "邮箱格式不正确"
}
],
"code": "VALIDATION_ERROR"
}

3. 安全最佳实践

  • 使用HTTPS
  • 实现速率限制
  • 输入验证和清理
  • SQL注入防护(Prisma自动处理)
  • XSS防护
  • CORS配置
  • Helmet安全头

4. 性能最佳实践

  • 数据库查询优化
  • 缓存策略
  • 分页实现
  • 字段选择
  • 压缩响应
  • CDN静态资源

5. 可维护性最佳实践

  • 代码结构清晰
  • 使用中间件分离关注点
  • 实现服务层
  • 编写测试
  • API文档
  • 日志记录

开发效率对比

传统开发流程

阶段时间估算
项目架构设计2-3小时
数据库设计2-3小时
RESTful API开发8-12小时
GraphQL实现6-8小时
中间件开发4-6小时
错误处理2-3小时
API文档编写3-4小时
测试编写6-8小时
部署配置2-3小时
总计35-50小时

使用Claude Code

阶段时间估算
项目架构设计15分钟
数据库设计20分钟
RESTful API开发1.5小时
GraphQL实现1小时
中间件开发30分钟
错误处理15分钟
API文档编写30分钟
测试编写45分钟
部署配置20分钟
总计5小时

效率提升: 约8-10倍!

常见问题解答

Q1: RESTful vs GraphQL,如何选择?

RESTful:

  • 适合简单的CRUD操作
  • 缓存更容易实现
  • 更成熟,工具更完善
  • 适合标准化API

GraphQL:

  • 适合复杂关联查询
  • 客户端精确控制数据
  • 减少网络请求
  • 适合复杂前端需求

建议: 同时提供两者,让客户端根据需求选择。

Q2: 如何处理文件上传?

使用multer中间件:

const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
destination: 'uploads/',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
},
});

const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
const extname = allowedTypes.test(path.extname(file.originalname));
const mimetype = allowedTypes.test(file.mimetype);

if (mimetype && extname) {
return cb(null, true);
}
cb(new Error('只允许图片文件'));
},
});

router.post('/products/images', upload.single('image'), (req, res) => {
res.json({
success: true,
data: {
url: `/uploads/${req.file.filename}`,
},
});
});

Q3: 如何实现Webhook?

const crypto = require('crypto');

// 验证Webhook签名
const verifyWebhookSignature = (payload, signature, secret) => {
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(payload).digest('hex');
return signature === digest;
};

router.post('/webhooks/payment', async (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);

if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ message: 'Invalid signature' });
}

// 处理webhook事件
const { event, data } = req.body;

switch (event) {
case 'payment.completed':
await handlePaymentCompleted(data);
break;
case 'payment.failed':
await handlePaymentFailed(data);
break;
}

res.json({ received: true });
});

进阶功能扩展

1. 实时通知(WebSocket)

const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
ws.on('message', (message) => {
// 广播消息给所有客户端
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
});

2. 任务队列(Bull)

const Queue = require('bull');
const emailQueue = new Queue('email', 'redis://localhost:6379');

emailQueue.process(async (job) => {
const { to, subject, body } = job.data;
await sendEmail(to, subject, body);
});

// 添加任务
emailQueue.add({
to: 'user@example.com',
subject: '订单确认',
body: '您的订单已创建',
});

3. 搜索功能(Elasticsearch)

const { Client } = require('@elastic/elasticsearch');

const client = new Client({
node: 'http://localhost:9200',
});

async function searchProducts(query) {
const result = await client.search({
index: 'products',
body: {
query: {
multi_match: {
query,
fields: ['name^2', 'description'],
},
},
},
});

return result.body.hits.hits;
}

总结

这个后端API开发案例教你:

✅ 掌握了RESTful API设计的最佳实践 ✅ 学会了GraphQL服务实现 ✅ 理解了中间件系统的架构 ✅ 掌握了错误处理和验证 ✅ 学会了API文档自动生成 ✅ 了解了缓存策略 ✅ 掌握了测试方法 ✅ 学会了部署优化

关键要点:

  1. 清晰的架构: 分层设计(Controller-Service-Model)
  2. 完善的中间件: 认证、授权、验证、错误处理
  3. 优秀的文档: Swagger自动生成
  4. 全面的测试: 单元测试+集成测试
  5. 性能优化: 缓存、索引、分页
  6. 安全防护: 认证、限流、验证

Claude Code让后端API开发变得高效而专业。通过合理的项目结构和清晰的需求描述,你可以在几小时内完成传统开发需要几天的工作量。

下一步学习