📖 第25课:毕业项目——完整API平台

阶段五:实战项目

恭喜你来到最后一课!本课将前面24课的所有知识融合成一个完整的API平台——从项目结构到生产部署,从RESTful API到GraphQL端点,从认证授权到监控告警。这是一个可以上线运行的真实项目模板。

🏗️ 平台架构

项目结构

api-platform/
├── src/
│   ├── index.js              # 入口
│   ├── config/
│   │   ├── index.js          # 配置管理
│   │   └── database.js       # 数据库配置
│   ├── middleware/
│   │   ├── auth.js           # JWT认证
│   │   ├── rateLimit.js      # 限流
│   │   ├── validate.js       # 输入验证
│   │   ├── errorHandler.js   # 错误处理
│   │   └── cache.js          # 缓存
│   ├── routes/
│   │   ├── auth.js           # 认证路由
│   │   ├── users.js          # 用户路由
│   │   ├── products.js       # 商品路由
│   │   └── orders.js         # 订单路由
│   ├── graphql/
│   │   ├── schema.js         # GraphQL Schema
│   │   └── resolvers.js      # Resolvers
│   ├── services/
│   │   ├── userService.js    # 业务逻辑
│   │   ├── productService.js
│   │   └── orderService.js
│   └── utils/
│       ├── logger.js         # 日志
│       └── errors.js         # 自定义错误
├── docs/
│   └── openapi.yaml          # API文档
├── proto/
│   └── order.proto           # gRPC定义
├── tests/
│   ├── auth.test.js
│   ├── products.test.js
│   └── orders.test.js
├── docker-compose.yml
├── Dockerfile
└── package.json

🛠️ 完整平台实现

// platform.js - 完整API平台(单文件版)
const express = require('express');
const crypto = require('crypto');
const { createHandler } = require('graphql-http/lib/use/express');
const { buildSchema } = require('graphql');

const app = express();

// ===== 配置 =====
const config = {
  port: process.env.PORT || 3000,
  jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production',
  jwtExpiry: '15m',
  refreshTokenExpiry: '7d',
  rateLimitWindow: 60000,
  rateLimitMax: 200,
  cacheDefaultTTL: 60,
};

// ===== 工具函数 =====
const hashPassword = (pwd) => crypto.createHash('sha256').update(pwd + config.jwtSecret).digest('hex');
const createJWT = (payload) => {
  const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
  const body = Buffer.from(JSON.stringify({ ...payload, iat: Math.floor(Date.now() / 1000) })).toString('base64url');
  const sig = crypto.createHmac('sha256', config.jwtSecret).update(`${header}.${body}`).digest('base64url');
  return `${header}.${body}.${sig}`;
};
const verifyJWT = (token) => {
  const [h, b, s] = token.split('.');
  const expected = crypto.createHmac('sha256', config.jwtSecret).update(`${h}.${b}`).digest('base64url');
  if (s !== expected) throw new Error('Invalid signature');
  const payload = JSON.parse(Buffer.from(b, 'base64').toString());
  if (payload.exp < Date.now() / 1000) throw new Error('Token expired');
  return payload;
};

// ===== LRU缓存 =====
class LRUCache {
  constructor(max = 500, defaultTTL = 60) { this.max = max; this.defaultTTL = defaultTTL; this.cache = new Map(); }
  get(key) { const e = this.cache.get(key); if (!e || Date.now() > e.exp) { this.cache.delete(key); return null; } this.cache.delete(key); this.cache.set(key, e); return e.data; }
  set(key, data, ttl) { if (this.cache.size >= this.max) { const k = this.cache.keys().next().value; this.cache.delete(k); } this.cache.set(key, { data, exp: Date.now() + (ttl || this.defaultTTL) * 1000 }); }
  invalidate(prefix) { for (const k of [...this.cache.keys()]) if (k.startsWith(prefix)) this.cache.delete(k); }
  stats() { return { size: this.cache.size, max: this.max }; }
}
const cache = new LRUCache();

// ===== 数据层 =====
const db = {
  users: new Map([
    ['1', { id: '1', name: '管理员', email: 'admin@example.com', password: hashPassword('admin123'), role: 'admin', createdAt: '2025-01-01' }],
    ['2', { id: '2', name: '张三', email: 'zhang@example.com', password: hashPassword('user123'), role: 'user', createdAt: '2025-01-02' }],
  ]),
  products: new Map([
    ['1', { id: '1', name: '机械键盘', price: 599, category: '外设', stock: 120, status: 'active', createdAt: '2025-01-05' }],
    ['2', { id: '2', name: '无线鼠标', price: 199, category: '外设', stock: 250, status: 'active', createdAt: '2025-01-06' }],
    ['3', { id: '3', name: '4K显示器', price: 2999, category: '显示器', stock: 45, status: 'active', createdAt: '2025-01-07' }],
    ['4', { id: '4', name: '降噪耳机', price: 1299, category: '音频', stock: 60, status: 'active', createdAt: '2025-01-08' }],
  ]),
  orders: new Map([
    ['1', { id: '1', userId: '2', items: [{ productId: '1', quantity: 1, price: 599 }, { productId: '2', quantity: 2, price: 199 }], total: 997, status: 'completed', createdAt: '2025-01-10' }],
  ]),
  nextUserId: 3, nextProductId: 5, nextOrderId: 2,
};

// ===== 中间件 =====
app.use(express.json({ limit: '100kb' }));

// CORS
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

// 请求ID
app.use((req, res, next) => {
  req.requestId = req.headers['x-request-id'] || crypto.randomUUID();
  res.setHeader('X-Request-ID', req.requestId);
  next();
});

// 日志
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`${req.method} ${req.path} ${res.statusCode} ${Date.now() - start}ms [${req.requestId}]`);
  });
  next();
});

// 限流
const rateCounters = new Map();
app.use((req, res, next) => {
  if (req.path === '/health' || req.path === '/api/auth/login') return next();
  const key = req.user?.sub || req.ip;
  const now = Date.now();
  let counter = rateCounters.get(key);
  if (!counter || now - counter.windowStart > config.rateLimitWindow) {
    counter = { count: 0, windowStart: now };
    rateCounters.set(key, counter);
  }
  counter.count++;
  res.setHeader('X-RateLimit-Limit', String(config.rateLimitMax));
  res.setHeader('X-RateLimit-Remaining', String(Math.max(0, config.rateLimitMax - counter.count)));
  if (counter.count > config.rateLimitMax) {
    return res.status(429).json({ success: false, error: { code: 'RATE_LIMIT', message: '请求频率超限' } });
  }
  next();
});

// 认证
function auth(req, res, next) {
  const token = req.headers.authorization?.substring(7);
  if (!token) return res.status(401).json({ success: false, error: { code: 'AUTH_MISSING' } });
  try {
    req.user = verifyJWT(token);
    next();
  } catch (e) {
    return res.status(401).json({ success: false, error: { code: 'AUTH_INVALID', message: e.message } });
  }
}

// ===== REST路由 =====

// 认证
app.post('/api/auth/login', (req, res) => {
  const { email, password } = req.body;
  const user = [...db.users.values()].find(u => u.email === email);
  if (!user || user.password !== hashPassword(password)) {
    return res.status(401).json({ success: false, error: { code: 'INVALID_CREDENTIALS' } });
  }
  const token = createJWT({ sub: user.id, role: user.role, exp: Math.floor(Date.now() / 1000) + 900 });
  const refreshToken = createJWT({ sub: user.id, type: 'refresh', exp: Math.floor(Date.now() / 1000) + 7 * 86400 });
  res.json({ success: true, data: { token, refreshToken, expiresIn: 900 } });
});

app.post('/api/auth/register', (req, res) => {
  const { name, email, password } = req.body;
  if (!name || !email || !password) return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: 'name, email, password必填' } });
  if ([...db.users.values()].some(u => u.email === email)) return res.status(409).json({ success: false, error: { code: 'CONFLICT', message: '邮箱已注册' } });
  const id = String(db.nextUserId++);
  const user = { id, name, email, password: hashPassword(password), role: 'user', createdAt: new Date().toISOString() };
  db.users.set(id, user);
  const token = createJWT({ sub: id, role: 'user', exp: Math.floor(Date.now() / 1000) + 900 });
  res.status(201).json({ success: true, data: { id, name, email, role: user.role, token } });
});

// 商品
app.get('/api/products', (req, res) => {
  const cacheKey = `products:${req.originalUrl}`;
  const cached = cache.get(cacheKey);
  if (cached) { res.setHeader('X-Cache', 'HIT'); return res.json(cached); }
  res.setHeader('X-Cache', 'MISS');
  const { page = 1, limit = 20, category, status } = req.query;
  let data = [...db.products.values()];
  if (category) data = data.filter(p => p.category === category);
  if (status) data = data.filter(p => p.status === status);
  const total = data.length;
  const p = Math.max(1, parseInt(page));
  const l = Math.min(100, parseInt(limit));
  data = data.slice((p - 1) * l, p * l);
  const response = { success: true, data, pagination: { page: p, limit: l, total, totalPages: Math.ceil(total / l) } };
  cache.set(cacheKey, response, 30);
  res.json(response);
});

app.get('/api/products/:id', (req, res) => {
  const product = db.products.get(req.params.id);
  if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
  res.json({ success: true, data: product });
});

app.post('/api/products', auth, (req, res) => {
  if (req.user.role !== 'admin') return res.status(403).json({ success: false, error: { code: 'FORBIDDEN' } });
  const { name, price, category, stock } = req.body;
  if (!name || price === undefined) return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR' } });
  const id = String(db.nextProductId++);
  const product = { id, name, price, category: category || '未分类', stock: stock || 0, status: 'active', createdAt: new Date().toISOString() };
  db.products.set(id, product);
  cache.invalidate('products:');
  res.status(201).json({ success: true, data: product });
});

// 订单
app.post('/api/orders', auth, async (req, res) => {
  const { items, shippingAddress } = req.body;
  if (!items?.length) return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR' } });

  // 验证商品和计算总价
  let total = 0;
  for (const item of items) {
    const product = db.products.get(item.productId);
    if (!product) return res.status(400).json({ success: false, error: { code: 'PRODUCT_NOT_FOUND', message: `商品 ${item.productId} 不存在` } });
    if (product.stock < item.quantity) return res.status(400).json({ success: false, error: { code: 'INSUFFICIENT_STOCK', message: `${product.name} 库存不足` } });
    total += product.price * item.quantity;
    item.price = product.price;
  }

  const id = String(db.nextOrderId++);
  const order = { id, userId: req.user.sub, items, total, shippingAddress, status: 'pending', createdAt: new Date().toISOString() };
  db.orders.set(id, order);

  // 扣减库存
  items.forEach(item => {
    const product = db.products.get(item.productId);
    if (product) product.stock -= item.quantity;
  });
  cache.invalidate('products:');

  res.status(201).json({ success: true, data: order });
});

app.get('/api/orders', auth, (req, res) => {
  const orders = [...db.orders.values()].filter(o => req.user.role === 'admin' || o.userId === req.user.sub);
  res.json({ success: true, data: orders });
});

app.get('/api/orders/:id', auth, (req, res) => {
  const order = db.orders.get(req.params.id);
  if (!order) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
  if (order.userId !== req.user.sub && req.user.role !== 'admin') return res.status(403).json({ success: false, error: { code: 'FORBIDDEN' } });
  res.json({ success: true, data: order });
});

// ===== GraphQL端点 =====

const graphqlSchema = buildSchema(`
  type Query {
    me: User!
    products(category: String, first: Int = 20): [Product!]!
    product(id: ID!): Product
    myOrders: [Order!]!
  }
  type Mutation {
    createOrder(items: [OrderItemInput!]!): Order!
    updateProfile(name: String, email: String): User!
  }
  type User { id: ID! name: String! email: String! role: String! }
  type Product { id: ID! name: String! price: Float! category: String! stock: Int! status: String! }
  type Order { id: ID! total: Float! status: String! items: [OrderItem!]! createdAt: String! }
  type OrderItem { productId: ID! quantity: Int! price: Float! }
  input OrderItemInput { productId: ID! quantity: Int! }
`);

const graphqlRoot = {
  me: (args, req) => { const u = db.users.get(req.user?.sub); return u ? { id: u.id, name: u.name, email: u.email, role: u.role } : null; },
  products: ({ category, first }) => { let r = [...db.products.values()]; if (category) r = r.filter(p => p.category === category); return r.slice(0, first); },
  product: ({ id }) => db.products.get(id),
  myOrders: (args, req) => [...db.orders.values()].filter(o => o.userId === req.user?.sub),
  createOrder: ({ items }, req) => {
    if (!req.user) throw new Error('未认证');
    let total = 0;
    items.forEach(i => { const p = db.products.get(i.productId); if (p) { total += p.price * i.quantity; i.price = p.price; } });
    const id = String(db.nextOrderId++);
    const order = { id, userId: req.user.sub, items, total, status: 'pending', createdAt: new Date().toISOString() };
    db.orders.set(id, order);
    return order;
  },
  updateProfile: ({ name, email }, req) => {
    if (!req.user) throw new Error('未认证');
    const user = db.users.get(req.user.sub);
    if (name) user.name = name;
    if (email) user.email = email;
    return { id: user.id, name: user.name, email: user.email, role: user.role };
  },
};

app.use('/graphql', auth, createHandler({ schema: graphqlSchema, rootValue: graphqlRoot, context: (req) => req.raw }));

// ===== 管理/健康端点 =====
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', uptime: process.uptime(), cache: cache.stats(), users: db.users.size, products: db.products.size, orders: db.orders.size });
});

// ===== 错误处理 =====
app.use((err, req, res, next) => {
  console.error(`[${req.requestId}] 错误:`, err);
  res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: process.env.NODE_ENV === 'development' ? err.message : '服务器内部错误' }, meta: { requestId: req.requestId } });
});

app.use((req, res) => {
  res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `路径 ${req.method} ${req.path} 不存在` } });
});

// ===== 启动 =====
app.listen(config.port, () => {
  console.log(`🚀 API平台运行在 http://localhost:${config.port}`);
  console.log(`  REST API:   http://localhost:${config.port}/api/`);
  console.log(`  GraphQL:    http://localhost:${config.port}/graphql`);
  console.log(`  健康检查:   http://localhost:${config.port}/health`);
  console.log(`\n  快速测试:`);
  console.log(`  curl -X POST http://localhost:${config.port}/api/auth/login -H "Content-Type: application/json" -d '{"email":"admin@example.com","password":"admin123"}'`);
});

测试完整平台

# 安装依赖
npm install express graphql graphql-http

# 启动平台
node platform.js

# 1. 登录
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"admin123"}' | jq -r '.data.token')

# 2. REST: 商品列表
curl -s http://localhost:3000/api/products | jq .

# 3. REST: 创建订单
curl -s -X POST http://localhost:3000/api/orders \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"productId":"1","quantity":1},{"productId":"2","quantity":2}]}' | jq .
{
  "success": true,
  "data": {
    "id": "2",
    "userId": "1",
    "items": [{"productId":"1","quantity":1,"price":599},{"productId":"2","quantity":2,"price":199}],
    "total": 997,
    "status": "pending",
    "createdAt": "2025-01-15T10:30:00.000Z"
  }
}
# 4. GraphQL: 获取个人信息和订单
curl -s -X POST http://localhost:3000/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ me { name email role } myOrders { id total status items { productId quantity price } } }"}' | jq .

# 5. 健康检查
curl -s http://localhost:3000/health | jq .

🏆 课程总结

25课知识体系

阶段课程核心收获
RESTful基础01-05API设计原则、HTTP语义、URL设计、请求响应、状态码
RESTful进阶06-10JWT/OAuth2认证、版本管理、分页过滤排序、限流熔断、缓存策略
GraphQL11-15Schema设计、查询变更、订阅实时、DataLoader、实战
gRPC16-20Protobuf、四种通信模式、流式处理、gRPC网关
实战项目21-25OpenAPI文档、API网关、微服务设计、安全加固、完整平台

🏆 终极成就:API设计大师

恭喜完成全部25课!你已经掌握了:

你现在可以自信地设计和实现生产级API系统。继续实践,持续学习,保持对新技术的好奇心!🚀