全栈手账 订阅 RSS

Koa2 + MySQL + Redis + UniApp 项目开发指南

技术栈:Koa2 (Node.js) + MySQL + Redis + UniApp,适合中小型全栈项目开发。本文档覆盖从环境搭建、项目初始化、数据库设计、前后端联调到部署上线的全流程。

读了 47 分钟 更新于 8 月 24 日

一、技术栈概述

1.1 技术选型

层级 技术 版本建议 说明
后端框架 Koa2 ^2.15.x 轻量级 Node.js 框架,洋葱圈中间件模型
运行环境 Node.js >= 18 LTS 推荐使用 LTS 版本
数据库 MySQL 8.0+ 关系型数据库,存储业务数据
缓存 Redis 6.x / 7.x 缓存、Session、限流、队列
ORM Sequelize ^6.x 支持 MySQL 的 Node.js ORM
前端 UniApp 最新版 一套代码多端运行(小程序/H5/App)
包管理 pnpm / npm - 推荐 pnpm

1.2 Koa2 核心特性

code
    请求进入        │        ▼  ┌─── Middleware 1 (外层) ───┐  │  ┌── Middleware 2 ──┐    │  │  │  ┌─ Middleware 3 ─┐   │  │  │  │   Controller   │   │  │  │  └───────────────┘   │  │  └─────────────────────┘  │  └────────────────────────────┘        │        ▼     响应返回(洋葱模型)
  • 中间件机制:Koa 的核心是中间件,采用洋葱圈模型(await next())
  • 轻量简洁:核心代码不到 2000 行,不捆绑任何中间件
  • Async/Await:原生支持异步,彻底解决回调地狱
  • Context 上下文:将 request 和 response 封装到 ctx 中

1.3 Koa vs Express 对比

对比项 Koa2 Express
中间件模型 洋葱圈(支持 await next()) 线性链式
异步处理 原生 async/await 回调 / Promise
核心体积 极小(不含路由、静态文件等) 稍大
错误处理 可统一 try/catch 捕获 需专门的 error middleware
社区生态 偏小,需自行组合中间件 极其丰富
自由度 高,灵活组合 约定稍多
适用场景 API 服务、微服务 全栈应用

二、环境搭建与项目初始化

2.1 环境准备

bash
# 1. 安装 Node.js (推荐使用 nvm 管理版本)# 下载地址:https://nodejs.org/# 2. 验证安装node -v   # >= 18.xnpm -v    # >= 9.x# 3. 安装 pnpm(可选但推荐)npm install -g pnpm# 4. 安装 MySQL 8.0+# 下载地址:https://dev.mysql.com/downloads/mysql/# 5. 安装 Redis# Windows: 使用 WSL 或 https://github.com/microsoftarchive/redis/releases# Mac: brew install redis# Linux: apt install redis-server / yum install redis

2.2 初始化 Koa2 项目

bash
# 创建项目目录mkdir koa2-uniapp-democd koa2-uniapp-demo# 初始化 package.jsonpnpm init# 安装核心依赖pnpm add koa koa-router koa-body koa-cors jsonwebtoken bcryptjspnpm add sequelize mysql2 redis ioredis dotenvpnpm add joi  # 参数校验# 安装开发依赖pnpm add -D nodemon

2.3 项目目录结构

文本
koa2-uniapp-demo/├── src/│   ├── app.js                 # 应用入口│   ├── config/│   │   └── index.js           # 配置文件│   ├── middleware/│   │   ├── auth.js            # JWT 鉴权中间件│   │   ├── errorHandler.js    # 全局错误处理│   │   └── validator.js       # 参数校验中间件│   ├── routes/│   │   ├── index.js           # 路由汇总│   │   ├── user.js            # 用户模块路由│   │   └── article.js         # 文章模块路由│   ├── models/│   │   ├── index.js           # 数据库连接初始化│   │   ├── User.js            # 用户模型│   │   └── Article.js         # 文章模型│   ├── services/│   │   ├── userService.js     # 用户业务逻辑│   │   └── articleService.js  # 文章业务逻辑│   ├── controllers/│   │   ├── userController.js  # 用户控制器│   │   └── articleController.js│   ├── utils/│   │   ├── jwt.js             # JWT 工具函数│   │   ├── redis.js           # Redis 连接与工具│   │   └── response.js        # 统一响应格式│   └── validators/│       ├── user.js            # 用户参数校验规则│       └── article.js├── .env                       # 环境变量├── .env.example               # 环境变量模板└── package.json

三、后端核心配置

3.1 环境变量(.env)

env
# 应用配置PORT=3000NODE_ENV=development# MySQL 配置DB_HOST=localhostDB_PORT=3306DB_USER=rootDB_PASSWORD=your_passwordDB_NAME=koa2_demo# Redis 配置REDIS_HOST=127.0.0.1REDIS_PORT=6379REDIS_PASSWORD=REDIS_DB=0# JWT 配置JWT_SECRET=your_jwt_secret_keyJWT_EXPIRES_IN=7d

3.2 配置文件(src/config/index.js)

javascript
require('dotenv').config();module.exports = {  port: process.env.PORT || 3000,  env: process.env.NODE_ENV || 'development',  database: {    host: process.env.DB_HOST || 'localhost',    port: parseInt(process.env.DB_PORT) || 3306,    username: process.env.DB_USER || 'root',    password: process.env.DB_PASSWORD || '',    database: process.env.DB_NAME || 'koa2_demo',    dialect: 'mysql',    timezone: '+08:00',    pool: {      max: 10,      min: 0,      acquire: 30000,      idle: 10000,    },    logging: process.env.NODE_ENV === 'development' ? console.log : false,  },  redis: {    host: process.env.REDIS_HOST || '127.0.0.1',    port: parseInt(process.env.REDIS_PORT) || 6379,    password: process.env.REDIS_PASSWORD || undefined,    db: parseInt(process.env.REDIS_DB) || 0,  },  jwt: {    secret: process.env.JWT_SECRET || 'default_secret',    expiresIn: process.env.JWT_EXPIRES_IN || '7d',  },};

3.3 应用入口(src/app.js)

javascript
const Koa = require('koa');const Router = require('koa-router');const bodyParser = require('koa-body');const cors = require('koa-cors');const config = require('./config');const { initDatabase } = require('./models');const errorHandler = require('./middleware/errorHandler');const routes = require('./routes');const app = new Koa();// 中间件注册顺序(洋葱模型外到内)app.use(cors());app.use(errorHandler);app.use(bodyParser({ multipart: true }));// 注册路由app.use(routes.routes());app.use(routes.allowedMethods());// 启动服务async function bootstrap() {  try {    // 初始化数据库连接    await initDatabase();    console.log('Database connected successfully');    app.listen(config.port, () => {      console.log(`Server running on http://localhost:${config.port}`);    });  } catch (error) {    console.error('Failed to start server:', error);    process.exit(1);  }}bootstrap();module.exports = app;

四、数据库与模型设计

4.1 数据库初始化(src/models/index.js)

javascript
const { Sequelize } = require('sequelize');const config = require('../config');const sequelize = new Sequelize(  config.database.database,  config.database.username,  config.database.password,  {    host: config.database.host,    port: config.database.port,    dialect: config.database.dialect,    timezone: config.database.timezone,    pool: config.database.pool,    logging: config.database.logging,  });const User = require('./User')(sequelize);const Article = require('./Article')(sequelize);// 模型关联User.hasMany(Article, { foreignKey: 'userId', as: 'articles' });Article.belongsTo(User, { foreignKey: 'userId', as: 'author' });async function initDatabase() {  // 开发环境自动同步表结构(生产环境请用 migrations)  if (config.env === 'development') {    await sequelize.sync({ alter: true });  }}module.exports = {  sequelize,  initDatabase,  User,  Article,};

4.2 用户模型(src/models/User.js)

javascript
const { DataTypes } = require('sequelize');module.exports = (sequelize) => {  const User = sequelize.define('User', {    id: {      type: DataTypes.INTEGER.UNSIGNED,      primaryKey: true,      autoIncrement: true,    },    username: {      type: DataTypes.STRING(50),      allowNull: false,      unique: true,      comment: '用户名',    },    nickname: {      type: DataTypes.STRING(50),      allowNull: true,      comment: '昵称',    },    email: {      type: DataTypes.STRING(100),      allowNull: false,      unique: true,      comment: '邮箱',    },    password: {      type: DataTypes.STRING(255),      allowNull: false,      comment: '密码(bcrypt 加密)',    },    avatar: {      type: DataTypes.STRING(255),      allowNull: true,      comment: '头像 URL',    },    status: {      type: DataTypes.TINYINT,      defaultValue: 1,      comment: '状态:0-禁用 1-正常',    },    lastLoginAt: {      type: DataTypes.DATE,      allowNull: true,      comment: '最后登录时间',    },  }, {    tableName: 'users',    timestamps: true, // 自动添加 createdAt、updatedAt    underscored: true, // 使用下划线命名  });  return User;};

4.3 文章模型(src/models/Article.js)

javascript
const { DataTypes } = require('sequelize');module.exports = (sequelize) => {  const Article = sequelize.define('Article', {    id: {      type: DataTypes.INTEGER.UNSIGNED,      primaryKey: true,      autoIncrement: true,    },    title: {      type: DataTypes.STRING(200),      allowNull: false,      comment: '文章标题',    },    content: {      type: DataTypes.TEXT('long'),      allowNull: false,      comment: '文章内容',    },    summary: {      type: DataTypes.STRING(500),      allowNull: true,      comment: '文章摘要',    },    cover: {      type: DataTypes.STRING(255),      allowNull: true,      comment: '封面图',    },    status: {      type: DataTypes.TINYINT,      defaultValue: 0,      comment: '状态:0-草稿 1-已发布',    },    viewCount: {      type: DataTypes.INTEGER.UNSIGNED,      defaultValue: 0,      comment: '浏览量',    },    userId: {      type: DataTypes.INTEGER.UNSIGNED,      allowNull: false,      comment: '作者 ID',    },  }, {    tableName: 'articles',    timestamps: true,    underscored: true,  });  return Article;};

五、Redis 集成

5.1 Redis 连接工具(src/utils/redis.js)

javascript
const Redis = require('ioredis');const config = require('../config');const redis = new Redis({  host: config.redis.host,  port: config.redis.port,  password: config.redis.password,  db: config.redis.db,  retryStrategy(times) {    const delay = Math.min(times * 50, 2000);    return delay;  },  maxRetriesPerRequest: 3,});redis.on('connect', () => {  console.log('Redis connected successfully');});redis.on('error', (err) => {  console.error('Redis error:', err);});module.exports = redis;

5.2 Redis 常用场景使用示例

javascript
const redis = require('./redis');// ========== 1. 缓存热点数据 ==========// 设置缓存(过期时间 3600 秒)async function setCache(key, data, ttl = 3600) {  await redis.setex(key, ttl, JSON.stringify(data));}// 获取缓存async function getCache(key) {  const data = await redis.get(key);  return data ? JSON.parse(data) : null;}// 删除缓存async function delCache(key) {  await redis.del(key);}// ========== 2. 接口限流 ==========// 简单滑动窗口限流async function rateLimit(key, maxRequests = 60, windowMs = 60) {  const current = await redis.incr(key);  if (current === 1) {    await redis.expire(key, windowMs);  }  return current <= maxRequests;}// ========== 3. 分布式锁 ==========async function acquireLock(lockKey, ttl = 10) {  const result = await redis.set(lockKey, '1', 'NX', 'EX', ttl);  return result === 'OK';}async function releaseLock(lockKey) {  await redis.del(lockKey);}// ========== 4. 消息队列(List)==========// 生产者async function pushToQueue(queueName, data) {  await redis.rpush(queueName, JSON.stringify(data));}// 消费者(阻塞读取)async function popFromQueue(queueName, timeout = 0) {  const result = await redis.blpop(queueName, timeout);  return result ? JSON.parse(result[1]) : null;}

六、统一响应与工具函数

6.1 统一响应格式(src/utils/response.js)

javascript
class Response {  // 成功响应  static success(data = null, message = 'success') {    return {      code: 200,      message,      data,    };  }  // 分页成功响应  static paginate(list, pagination) {    return {      code: 200,      message: 'success',      data: {        list,        pagination: {          page: pagination.page,          pageSize: pagination.pageSize,          total: pagination.total,          totalPages: Math.ceil(pagination.total / pagination.pageSize),        },      },    };  }  // 创建成功  static created(data = null, message = 'created') {    return {      code: 201,      message,      data,    };  }  // 错误响应  static error(code = 400, message = 'bad request') {    return {      code,      message,    };  }}module.exports = Response;

6.2 JWT 工具(src/utils/jwt.js)

javascript
const jwt = require('jsonwebtoken');const config = require('../config');function sign(payload) {  return jwt.sign(payload, config.jwt.secret, {    expiresIn: config.jwt.expiresIn,  });}function verify(token) {  return jwt.verify(token, config.jwt.secret);}module.exports = { sign, verify };

七、中间件

7.1 全局错误处理(src/middleware/errorHandler.js)

javascript
const Response = require('../utils/response');module.exports = async (ctx, next) => {  try {    await next();    // 404 处理    if (ctx.status === 404) {      ctx.body = Response.error(404, '接口不存在');    }  } catch (err) {    console.error('Server error:', err);    // 参数校验错误    if (err.name === 'ValidationError') {      ctx.status = 400;      ctx.body = Response.error(400, err.message);      return;    }    // Sequelize 校验错误    if (err.name === 'SequelizeValidationError' || err.name === 'SequelizeUniqueConstraintError') {      ctx.status = 400;      ctx.body = Response.error(400, err.errors?.[0]?.message || '数据校验失败');      return;    }    // JWT 错误    if (err.name === 'UnauthorizedError' || err.name === 'JsonWebTokenError') {      ctx.status = 401;      ctx.body = Response.error(401, '登录已过期,请重新登录');      return;    }    // 默认服务器错误    ctx.status = err.status || 500;    ctx.body = Response.error(ctx.status, err.message || '服务器内部错误');  }};

7.2 JWT 鉴权中间件(src/middleware/auth.js)

javascript
const { verify } = require('../utils/jwt');const Response = require('../utils/response');module.exports = (options = {}) => {  return async (ctx, next) => {    const authHeader = ctx.headers.authorization;    if (!authHeader) {      ctx.throw(401, '请先登录');    }    const token = authHeader.replace('Bearer ', '');    try {      const decoded = verify(token);      ctx.state.user = decoded; // 将用户信息挂载到 ctx.state      await next();    } catch (err) {      ctx.body = Response.error(401, '登录已过期,请重新登录');    }  };};

7.3 参数校验中间件(src/middleware/validator.js)

javascript
module.exports = (schema) => {  return async (ctx, next) => {    const data = {      ...ctx.request.body,      ...ctx.query,      ...ctx.params,    };    const { error, value } = schema.validate(data, {      abortEarly: false,      stripUnknown: true,    });    if (error) {      const messages = error.details.map((d) => d.message).join('; ');      ctx.throw(400, messages);    }    ctx.validatedData = value;    await next();  };};

八、控制器与路由示例

8.1 用户控制器(src/controllers/userController.js)

javascript
const bcrypt = require('bcryptjs');const { User } = require('../models');const { sign } = require('../utils/jwt');const redis = require('../utils/redis');const Response = require('../utils/response');// 注册async function register(ctx) {  const { username, email, password } = ctx.validatedData;  // 检查用户是否已存在  const existingUser = await User.findOne({    where: { [require('sequelize').Op.or]: [{ username }, { email }] },  });  if (existingUser) {    ctx.body = Response.error(400, '用户名或邮箱已被注册');    return;  }  // 加密密码  const salt = await bcrypt.genSalt(10);  const hashedPassword = await bcrypt.hash(password, salt);  // 创建用户  const user = await User.create({    username,    email,    password: hashedPassword,    nickname: username,  });  ctx.body = Response.created(    {      id: user.id,      username: user.username,      email: user.email,    },    '注册成功'  );}// 登录async function login(ctx) {  const { username, password } = ctx.validatedData;  const user = await User.findOne({ where: { username } });  if (!user) {    ctx.body = Response.error(400, '用户名或密码错误');    return;  }  if (user.status === 0) {    ctx.body = Response.error(403, '账号已被禁用');    return;  }  const isMatch = await bcrypt.compare(password, user.password);  if (!isMatch) {    ctx.body = Response.error(400, '用户名或密码错误');    return;  }  // 生成 Token  const token = sign({ id: user.id, username: user.username });  // 更新最后登录时间  await user.update({ lastLoginAt: new Date() });  ctx.body = Response.success({    token,    user: {      id: user.id,      username: user.username,      nickname: user.nickname,      avatar: user.avatar,    },  }, '登录成功');}// 获取当前用户信息async function getProfile(ctx) {  const user = await User.findByPk(ctx.state.user.id, {    attributes: { exclude: ['password'] },  });  ctx.body = Response.success(user);}// 退出登录(加入黑名单)async function logout(ctx) {  const token = ctx.headers.authorization.replace('Bearer ', '');  // 将 token 加入黑名单,过期时间与 token 一致  await redis.setex(`blacklist:${token}`, 7 * 24 * 3600, '1');  ctx.body = Response.success(null, '退出成功');}module.exports = { register, login, getProfile, logout };

8.2 用户路由(src/routes/user.js)

javascript
const Router = require('koa-router');const userController = require('../controllers/userController');const auth = require('../middleware/auth');const validator = require('../middleware/validator');const { registerSchema, loginSchema } = require('../validators/user');const router = new Router({ prefix: '/api/users' });// 注册router.post('/register', validator(registerSchema), userController.register);// 登录router.post('/login', validator(loginSchema), userController.login);// 获取个人信息(需登录)router.get('/profile', auth(), userController.getProfile);// 退出登录router.post('/logout', auth(), userController.logout);module.exports = router;

8.3 路由汇总(src/routes/index.js)

javascript
const Router = require('koa-router');const userRoutes = require('./user');const articleRoutes = require('./article');const router = new Router();router.use(userRoutes.routes());router.use(articleRoutes.routes());// 健康检查router.get('/api/health', (ctx) => {  ctx.body = { code: 200, message: 'ok', data: { uptime: process.uptime() } };});module.exports = router;

九、参数校验(Joi 示例)

9.1 用户校验规则(src/validators/user.js)

javascript
const Joi = require('joi');const registerSchema = Joi.object({  username: Joi.string()    .min(3)    .max(20)    .pattern(/^[a-zA-Z0-9_]+$/)    .required()    .messages({      'string.min': '用户名至少3个字符',      'string.max': '用户名最多20个字符',      'string.pattern.base': '用户名只能包含字母、数字和下划线',      'any.required': '用户名不能为空',    }),  email: Joi.string()    .email()    .required()    .messages({      'string.email': '邮箱格式不正确',      'any.required': '邮箱不能为空',    }),  password: Joi.string()    .min(6)    .max(30)    .required()    .messages({      'string.min': '密码至少6个字符',      'string.max': '密码最多30个字符',      'any.required': '密码不能为空',    }),});const loginSchema = Joi.object({  username: Joi.string().required().messages({    'any.required': '用户名不能为空',  }),  password: Joi.string().required().messages({    'any.required': '密码不能为空',  }),});module.exports = { registerSchema, loginSchema };

十、UniApp 前端对接

10.1 API 请求封装

javascript
// utils/request.js - UniApp 请求封装const BASE_URL = 'http://localhost:3000/api';let token = '';export function setToken(t) {  token = t;  uni.setStorageSync('token', t);}export function getToken() {  if (!token) {    token = uni.getStorageSync('token') || '';  }  return token;}export function clearToken() {  token = '';  uni.removeStorageSync('token');}// 统一请求封装function request(method, url, data = {}, options = {}) {  return new Promise((resolve, reject) => {    const header = {      'Content-Type': 'application/json',    };    const t = getToken();    if (t) {      header['Authorization'] = `Bearer ${t}`;    }    uni.request({      url: `${BASE_URL}${url}`,      method,      data,      header,      timeout: 10000,      ...options,      success: (res) => {        const { code, message, data: responseData } = res.data;        if (code === 200 || code === 201) {          resolve(res.data);        } else if (code === 401) {          // Token 过期,跳转登录          clearToken();          uni.reLaunch({ url: '/pages/login/login' });          reject(new Error(message));        } else {          reject(new Error(message));        }      },      fail: (err) => {        reject(new Error('网络请求失败'));      },    });  });}// 封装 GET / POST / PUT / DELETEexport const get = (url, params) => request('GET', url, params);export const post = (url, data) => request('POST', url, data);export const put = (url, data) => request('PUT', url, data);export const del = (url, params) => request('DELETE', url, params);

10.2 登录页面示例

vue
<template>  <view class="login-container">    <view class="form">      <input v-model="username" placeholder="请输入用户名" class="input" />      <input v-model="password" type="password" placeholder="请输入密码" class="input" />      <button @click="handleLogin" class="btn">登录</button>      <button @click="goRegister" class="btn btn-link">注册账号</button>    </view>  </view></template><script>import { post, setToken } from '@/utils/request';export default {  data() {    return {      username: '',      password: '',    };  },  methods: {    async handleLogin() {      if (!this.username || !this.password) {        uni.showToast({ title: '请填写完整信息', icon: 'none' });        return;      }      uni.showLoading({ title: '登录中...' });      try {        const res = await post('/users/login', {          username: this.username,          password: this.password,        });        setToken(res.data.token);        uni.hideLoading();        uni.showToast({ title: '登录成功', icon: 'success' });        // 跳转到首页        uni.switchTab({ url: '/pages/index/index' });      } catch (err) {        uni.hideLoading();        uni.showToast({ title: err.message, icon: 'none' });      }    },    goRegister() {      uni.navigateTo({ url: '/pages/register/register' });    },  },};</script>

十一、部署指南

11.1 生产环境准备

bash
# 1. 安装 PM2 进程管理npm install -g pm2# 2. 配置环境变量(生产环境)# 复制 .env.example 并修改为生产配置cp .env.example .env# 3. 构建 / 启动NODE_ENV=production pm2 start src/app.js --name koa2-app -i max# 4. 查看状态pm2 statuspm2 logs koa2-app# 5. 设置开机自启pm2 startuppm2 save

11.2 Nginx 反向代理配置

nginx
server {    listen 80;    server_name your-domain.com;    # 前端静态文件(UniApp H5 构建产物)    root /path/to/uniapp-dist;    index index.html;    # API 反向代理    location /api/ {        proxy_pass http://127.0.0.1:3000;        proxy_set_header Host $host;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto $scheme;        # WebSocket 支持(如有需要)        proxy_http_version 1.1;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection "upgrade";    }    # 静态资源缓存    location /static/ {        expires 30d;        add_header Cache-Control "public, no-transform";    }    # SPA 路由处理    location / {        try_files $uri $uri/ /index.html;    }}

11.3 Docker 部署(可选)

dockerfile
# DockerfileFROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --only=productionCOPY . .EXPOSE 3000CMD ["node", "src/app.js"]
yaml
# docker-compose.ymlversion: '3.8'services:  app:    build: .    ports:      - "3000:3000"    environment:      - NODE_ENV=production      - DB_HOST=mysql      - REDIS_HOST=redis    depends_on:      - mysql      - redis    restart: always  mysql:    image: mysql:8.0    environment:      MYSQL_ROOT_PASSWORD: root123      MYSQL_DATABASE: koa2_demo    volumes:      - mysql_data:/var/lib/mysql    ports:      - "3306:3306"    restart: always  redis:    image: redis:7-alpine    ports:      - "6379:6379"    volumes:      - redis_data:/data    restart: alwaysvolumes:  mysql_data:  redis_data:

十二、常见问题与最佳实践

12.1 性能优化建议

场景 方案
数据库查询频繁 加 Redis 缓存,缓存热点数据
列表查询慢 加索引、分页、只查必要字段
接口响应慢 使用 koa-compress 压缩响应体
大文件上传 使用 koa-body 的 multipart 配置 + 分片上传
日志管理 使用 log4js 或 winston 记录日志
跨域问题 生产环境用 Nginx 反向代理,开发用 @koa/cors

12.2 安全注意事项

  • 密码存储:始终使用 bcryptjs 哈希,不要明文存储
  • SQL 注入:使用 Sequelize ORM 替代原生 SQL 拼接
  • XSS 防护:前端输出时对用户输入做转义
  • 接口限流:使用 Redis 实现接口限流,防止恶意请求
  • HTTPS:生产环境必须启用 HTTPS
  • JWT 安全:密钥足够复杂,设置合理的过期时间
  • 参数校验:所有接口参数必须做校验(Joi / 自定义校验)

12.3 项目规范

  • 命名规范:文件名小写 + 中划线(user-controller.js),变量名 camelCase
  • 错误处理:统一使用 errorHandler 中间件兜底
  • 响应格式:统一使用 Response 工具类
  • 代码组织:Controller 不直接操作数据库,调 Service 层
  • Git 规范:遵循 Conventional Commits(feat: / fix: / chore:)
  • 环境变量:敏感信息放在 .env,不提交到 Git

12.4 常用命令速查

bash
# 开发模式启动(nodemon 热重载)npx nodemon src/app.js# 生产模式启动(PM2)pm2 start src/app.js --name koa2-apppm2 restart koa2-apppm2 stop koa2-apppm2 logs koa2-app# 数据库迁移(手动建表)npx sequelize-cli db:migrate# 生成迁移文件npx sequelize-cli migration:generate --name create-users-table# 测试接口curl http://localhost:3000/api/health

前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。