Blog-SSR 系统操作手册(v1.0.0)

Blog-SSR 系统操作手册(v1.0.0)

📖 文档概述

本文档是 Blog-SSR 系统的完整操作手册,旨在帮助开发者从零开始搭建和运行整个博客系统。系统采用现代化的全栈技术栈,包含前后端分离架构、SSR优化、Docker容器化部署等特性。

注: 操作手册 是一步一步的项目框架搭建说明, 暂时忋完成了核心功能, 其余开发中的功能还在补充中.
注: 操作手册 是一步一步的项目框架搭建说明, 这里出现的代码 是参考项目源码(下载地址: git clone https://github.com/pengtaohenry1213-prog/Blog), 所以有些部分的代码略有差异, 这点要注意.

🎯 适用读者

  • 新加入项目的开发者
  • 需要从零搭建项目的工程师
  • 运维人员进行系统部署

🎯 项目当前状态

已完成的核心功能:

  • ✅ 完整的Docker容器化环境
  • ✅ JWT用户认证系统(包含前端登录界面)
  • ✅ MySQL + Redis数据存储
  • ✅ Express后端API框架
  • ✅ Nginx反向代理配置
  • ✅ 前后端基础架构
    • 前端登录页面可正常使用
    • 后端 API 接口响应正常

开发中的功能:

  • 🚧 文章管理系统(后端模型和API)
  • 🚧 前端管理后台完整功能
  • 🚧 Nuxt SSR前台页面

⚠️ 需要补充的功能:

  • 文章模型定义和数据库迁移
  • 文章管理 API 接口
  • 前端文章管理界面
  • 用户权限管理
  • 统计数据展示

📋 前置要求

  • 熟悉 Node.js 开发环境
  • 了解 Docker 和容器化概念
  • 具备基本的 Linux/macOS 命令行操作能力

🎯 开发工作流选择指南

重要提醒:开发阶段优先使用本地开发模式

开发环境推荐方案

🎨 本地开发模式(推荐用于日常开发)

bash 复制代码
# 启动基础设施服务(MySQL + Redis)
pnpm run docker-dev:up:redis
pnpm run docker-dev:up:mysql

# 本地启动后端服务(支持热重载)
pnpm run dev:backend

# 本地启动前端服务
pnpm run dev:frontend

🐳 Docker 开发模式(适用于集成测试)

bash 复制代码
# docker 容器创建&部署
pnpm run docker:build

# 完整容器化环境启动
pnpm run docker:up
两种模式的对比
方面 本地开发模式 Docker 开发模式
启动速度 ~3秒 ⚡ ~30秒 🐌
热重载 即时响应 ⚡ 需要 volume 映射
调试体验 优秀(断点调试) 一般
资源占用
文件访问 直接访问 通过 volume
适用场景 日常开发、代码编写 集成测试、演示环境
开发阶段最佳实践
  1. 基础设施服务使用 Docker

    • MySQL 和 Redis 建议一直使用 Docker 容器
    • 保证环境一致性和数据持久化
  2. 应用服务使用本地模式

    • 后端服务:pnpm run dev:backend(支持热重载)
    • 前端服务:pnpm run dev:frontend(Vite 热重载)
    • Nuxt 服务:pnpm run dev:nuxt(如需要)
  3. 何时使用 Docker 模式

    • 全栈集成测试时
    • 向团队演示功能时
    • 模拟生产环境配置时
    • CI/CD 自动化测试时

执行顺序总览表 当前顺序: 环境准备 → 项目初始化 → Docker基础服务 → 后端基础框架搭建(Express + 中间件 + 工具函数) → 数据库设计与初始化(模型定义 + 初始化脚本 + 数据库同步) → JWT认证系统 → Redis缓存集成 → 前端后台管理

阶段一:基础运行环境准备

🎯 本阶段目标

确保本地开发环境具备运行 Blog-SSR 项目的最低要求。

✅ 预期结果

  • 所有必需工具正确安装并可正常运行
  • Node.js ≥ 18.0.0, pnpm ≥ 9.0.0, Docker ≥ 24.0.0
  • Docker 引擎正常运行,可创建和启动容器
  • 网络连接正常,可访问 Docker Hub

📏 验证标准

检查项 验证命令 期望结果
Node.js 版本 node --version ≥ 18.0.0
pnpm 版本 pnpm --version ≥ 9.0.0
Docker 版本 docker --version ≥ 24.0.0
Docker 引擎状态 docker info 无错误输出
Docker Hub 连接 curl -fsSL https://registry-1.docker.io/v2/ HTTP 200

📦 技术栈详情

  • 容器化: Docker + Docker Compose
  • 代码规范: ESLint + Prettier + Husky + lint-staged
  • concurrently: Node.js 命令行工具, 功能是在同一个终端窗口中,并行(同时)运行多个命令行命令,而无需打开多个终端分别执行。

📋 环境依赖清单

工具 版本要求 安装方式 验证命令
Node.js ≥ 18.0.0 官网下载 node --version
pnpm ≥ 9.0.0 npm install -g pnpm pnpm --version
Docker ≥ 24.0.0 Docker Desktop docker --version
Docker Compose ≥ 2.0.0 随 Docker Desktop docker compose version
Git ≥ 2.30.0 系统包管理器 git --version
concurrently ≥ 9.0.0 pnpm add -D concurrently 已随项目安装

技术栈版本详情:

组件 版本 说明
后端
Express 4.18.2 Web 框架
Sequelize 6.37.7 ORM
MySQL2 3.6.5 数据库驱动
Redis 4.6.12 缓存客户端
JWT 9.0.2 身份认证
前端后台管理
Vue 3.5.24 前端框架
Vite rolldown-vite@7.2.5 构建工具
Element Plus 2.13.0 UI 组件库
Pinia 3.0.4 状态管理
SSR前台
Nuxt 3.12.0 SSR 框架
容器化
MySQL 8.0 数据库
Redis 8.4.0 缓存
Nginx Alpine 反向代理

当前项目状态分析

已有的资源:

  • ✅ Node.js、pnpm、Docker、Docker Compose 安装验证
  • ✅ 技术栈版本清单已定义

缺失的部分:

  • ❌ 项目代码和配置

🧭 操作步骤

1. 安装 Node.js
bash 复制代码
# 使用 nvm 管理 Node.js 版本(推荐)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc
nvm install 18
nvm use 18
2. 安装 pnpm
bash 复制代码
npm install -g pnpm
3. 安装 Docker Desktop
  • 访问 Docker 官网
  • 下载对应操作系统版本
  • 完成安装并启动 Docker Desktop

阶段二:项目结构初始化

🎯 本阶段目标

创建完整的 Monorepo 项目结构,配置工作区依赖管理。

✅ 预期结果

  • 完整的项目目录结构按照规范创建
  • pnpm 工作区正确配置,可识别所有子包
  • 共享模块 @blog/common 成功创建并可导入
  • 根项目依赖正确安装,工作区识别正常

📏 验证标准

检查项 验证命令 期望结果
工作区配置 cat pnpm-workspace.yaml 包含 packages: - 'packages/*'
工作区识别 pnpm ls --depth=-1 列出所有工作区包
共享模块 ls -la packages/common/ 包含 index.js, package.json, types/, utils/
依赖安装 pnpm install 无错误,成功安装所有依赖
模块导入 node -e "import('./packages/common/index.js')" 无错误,可正常导入

📦 技术栈详情

  • 包管理: pnpm workspaces (monorepo)

当前项目状态分析

已有的资源:

  • ✅ pnpm 工作区配置已创建
  • ✅ 共享模块 @blog/common 已定义

缺失的部分:

  • ❌ 各服务模块的具体实现

🧭 操作步骤

注: 也可直接下载源码, 如果克隆现有仓库

bash 复制代码
git clone https://github.com/pengtaohenry1213-prog/Blog
cd blog-ssr
步骤 1:克隆或初始化项目
bash 复制代码
# 如果是从头开始
mkdir blog-ssr && cd blog-ssr
git init
步骤 2:初始化根项目配置

项目根目录执行: pnpm init 以创建package.json. 修改 package.json

json 复制代码
{
  "name": "blog-ssr",
  "version": "1.0.0",
  "description": "全栈博客系统,支持 SSR 和后台管理",
  "private": true,
  "packageManager": "pnpm@9.0.0",
  "scripts": {
    "docker:up": "docker compose -f docker/docker-compose.yml --env-file .env up -d",
    "docker:down": "docker compose -f docker/docker-compose.yml down",

    "docker-dev:up": "docker compose -f docker/docker-compose.dev.yml --env-file .env.development up -d",
    "docker-dev:down": "docker compose -f docker/docker-compose.dev.yml down",
    "docker-dev:delete": "docker compose -f docker/docker-compose.dev.yml --env-file .env.development down -v",
    "dev:backend": "pnpm --filter @blog/backend run dev",
    "dev:frontend": "pnpm --filter @blog/frontend run dev"
  }
}
步骤 3:配置 pnpm 工作区

创建 touch pnpm-workspace.yaml

yaml 复制代码
packages:
  - 'packages/*'
步骤 4:创建共享模块 mkdir packages && mkdir packages/common
bash 复制代码
mkdir -p packages/common/{types,utils}

创建 touch packages/common/package.json

json 复制代码
{
  "name": "@blog/common",
  "version": "1.0.0",
  "description": "Blog-SSR 共享模块",
  "main": "index.js",
  "type": "module",
  "exports": {
    ".": "./index.js",
    "./types": "./types/index.js",
    "./utils": "./utils/index.js"
  }
}

创建 touch packages/common/types/index.js

javascript 复制代码
export {};

创建 touch packages/common/utils/index.js

javascript 复制代码
/**
 * 共享工具函数
 */
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
  const d = new Date(date);
  if (isNaN(d.getTime())) {
    return '';
  }

  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  const hours = String(d.getHours()).padStart(2, '0');
  const minutes = String(d.getMinutes()).padStart(2, '0');
  const seconds = String(d.getSeconds()).padStart(2, '0');

  return format
    .replace('YYYY', year)
    .replace('MM', month)
    .replace('DD', day)
    .replace('HH', hours)
    .replace('mm', minutes)
    .replace('ss', seconds);
}

创建 touch packages/common/index.js

javascript 复制代码
export * from './types/index.js';
export * from './utils/index.js';
步骤 5:安装根级开发依赖
bash 复制代码
pnpm add -D -w eslint prettier lint-staged husky sass

📄 环境变量配置详解

🎯 配置原则

项目采用多环境配置策略:

  • .env.development - 开发环境配置(本地开发)
  • .env - 生产环境配置(Docker 容器)

🧭 配置步骤

步骤 1:创建开发环境配置

在项目根目录, 创建 touch .env.development 文件:

bash 复制代码
# 后端服务配置
NODE_ENV=development
HOST=0.0.0.0
PORT=3001

# 数据库配置(Docker 容器)
DB_HOST=localhost
DB_PORT=3306
DB_NAME=blog_db
DB_USER=blog_root
DB_PASSWORD=blog123

# Redis 配置(Docker 容器)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=redis123

# JWT 配置
JWT_SECRET=dev_jwt_secret_key_for_development_only
JWT_EXPIRES_IN=7d

# CORS 配置(允许前端访问)
CORS_ORIGIN=http://localhost

# Nuxt SSR 配置
NUXT_PUBLIC_API_BASE_URL=http://localhost:3001/api
NUXT_PUBLIC_SITE_BASE=http://localhost:3000
NUXT_PUBLIC_SITE_NAME=个人博客

# Docker 服务端口配置
BACKEND_PORT=3001
FRONTEND_PORT=80
NUXT_PORT=3000
步骤 2:创建生产环境配置

在项目根目录, 创建 touch .env 文件:

bash 复制代码
# 生产环境配置(用于 Docker 容器间通信)
NODE_ENV=production
...
步骤 3: 根目录附加配置文件

创建: touch .gitignore

🔐 安全注意事项

  1. JWT 密钥: 生产环境必须使用强随机密钥
  2. 数据库密码: 使用复杂密码,避免默认值
  3. 环境隔离: 不同环境使用不同的配置
  4. 敏感信息 : 不要将 .env 文件提交到版本控制

阶段三: Docker 基础服务配置

🎯 本阶段目标

启动 MySQL 和 Redis 容器,为后端服务提供数据存储和缓存支持。

✅ 预期结果

  • MySQL 和 Redis 容器成功启动并运行
  • 数据库初始化脚本自动执行,创建基础表结构
  • 容器间网络互通,可通过服务名访问
  • 数据持久化配置生效,重启后数据保留

📏 验证标准

| 检查项 | 验证命令 | 期望结果 |
|----------|-----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|-------|
| 容器状态 | docker compose ps | mysql 和 redis 状态为 Up/Healthy |
| MySQL 连接 | docker compose exec mysql mysql -uroot -pblog123 -e "SELECT 1;" | 返回结果 1 |
| Redis 连接 | docker compose exec redis redis-cli -a redis123 ping | 返回 PONG |
| 数据库初始化 | docker compose exec mysql mysql -ublog_root -pblog123 blog_db -e "SHOW TABLES;" | 包含基础表结构 |
| 网络连接 | docker compose exec mysql mysql -ublog_root -pblog123 -h mysql -e "SELECT 1;" | 容器内网络正常 |
| 数据持久化 | 重启容器后数据保留 | docker compose restart && docker compose exec mysql mysql -ublog_root -pblog123 blog_db -e "SELECT COUNT(*) FROM users;" | 数据未丢失 |

📦 技术栈详情

  • 容器化: Docker + Docker Compose

📋 环境依赖清单

工具 版本要求 安装方式 验证命令
Docker ≥ 24.0.0 Docker Desktop docker --version
Docker Compose ≥ 2.0.0 随 Docker Desktop docker compose version

📁 项目结构概览

bash 复制代码
docker/
├── docker-compose.yml       # 生产环境完整服务编排
├── docker-compose.dev.yml   # 开发环境服务配置
└── scripts/
    └── init.sql            # 数据库初始化脚本

📋 服务配置说明

服务 镜像版本 容器端口 数据持久化 健康检查
MySQL 8.0 3306
Redis 8.4.0 6379

当前项目状态分析

**已有的资源: **

  • ✅ Docker Compose 配置已创建
  • ✅ MySQL 和 Redis 容器配置已定义
  • ✅ 数据库初始化脚本已准备

**缺失的部分: **

  • ❌ 应用服务容器配置(后端、前端、Nuxt)
步骤 1: 配置 Docker Compose

项目已包含完整的 Docker 配置,直接使用即可。

创建目录: mkdir -p docker/{backend,frontend,nginx,nuxt,scripts}

创建 touch docker/docker-compose.dev.yml 参考 docker/docker-compose.dev.yml 文件,确保包含:

yaml 复制代码
services:
  mysql:
    image: mysql:8.0
    container_name: blog-test-mysql2
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    # 👇 dev模式, 临时暴露端口用于本地开发
    ports:
      - "${DB_PORT}:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    command: --default-authentication-plugin=caching_sha2_password # 设置默认 认证插件为 caching_sha2_password
    networks:
      - blog-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:8.4.0
    container_name: blog-test-redis2
    ports:
      - "${REDIS_PORT:-6379}:6379"  # ✅ 正确暴露端口, 左侧: 使用环境变量 REDIS_PORT,如果未设置则默认为 6379; 右侧: 6379 - 容器内部的 Redis 端口
    environment:
      - REDIS_PASSWORD=${REDIS_PASSWORD} # 指定 Redis 密码
    volumes:
      - redis_data:/data
    networks:
      - blog-network
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

# 顶级 volumes 定义, 优点: 数据不会随容器删除; 更安全、可复用;
volumes:
  mysql_data:
  redis_data:
  # backend_logs:

networks:
  blog-network:
    driver: bridge
  db-network: # 仅数据库/backend 可访问
    driver: bridge

创建 touch docker/scripts/init.sql 参考 docker/scripts/init.sql 文件,确保包含:

sql 复制代码
-- 数据库初始化脚本
-- 此脚本会在 MySQL 容器首次启动时自动执行

-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS blog_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE blog_db;

-- 注意: 表结构会由 Sequelize 自动创建
-- 这里创建索引以优化查询性能
-- 注意: MySQL 的 CREATE INDEX 不支持 IF NOT EXISTS,需要先检查索引是否存在

-- 为 users 表的 username 字段创建唯一索引(如果不存在)
SET @dbname = DATABASE();
SET @tablename = 'users';
SET @indexname = 'idx_users_username';
SET @preparedStatement = (SELECT IF(
  (
    SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
    WHERE table_schema = @dbname
    AND table_name = @tablename
    AND index_name = @indexname
  ) > 0,
  'SELECT 1',
  CONCAT('CREATE UNIQUE INDEX ', @indexname, ' ON ', @tablename, '(username)')
));
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 为 articles 表的 author_id、category_id、publish_time 字段创建联合索引(如果不存在)
SET @tablename = 'articles';
SET @indexname = 'idx_articles_author_category_time';
SET @preparedStatement = (SELECT IF(
  (
    SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
    WHERE table_schema = @dbname
    AND table_name = @tablename
    AND index_name = @indexname
  ) > 0,
  'SELECT 1',
  CONCAT('CREATE INDEX ', @indexname, ' ON ', @tablename, '(author_id, category_id, publish_time)')
));
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 为 articles 表的 publish_time 创建单独索引(用于排序,如果不存在)
SET @indexname = 'idx_articles_publish_time';
SET @preparedStatement = (SELECT IF(
  (
    SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
    WHERE table_schema = @dbname
    AND table_name = @tablename
    AND index_name = @indexname
  ) > 0,
  'SELECT 1',
  CONCAT('CREATE INDEX ', @indexname, ' ON ', @tablename, '(publish_time)')
));
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 为 articles 表的 category_id 创建索引(用于分类查询,如果不存在)
SET @indexname = 'idx_articles_category_id';
SET @preparedStatement = (SELECT IF(
  (
    SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
    WHERE table_schema = @dbname
    AND table_name = @tablename
    AND index_name = @indexname
  ) > 0,
  'SELECT 1',
  CONCAT('CREATE INDEX ', @indexname, ' ON ', @tablename, '(category_id)')
));
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 为 articles 表的 author_id 创建索引(用于作者查询,如果不存在)
SET @indexname = 'idx_articles_author_id';
SET @preparedStatement = (SELECT IF(
  (
    SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
    WHERE table_schema = @dbname
    AND table_name = @tablename
    AND index_name = @indexname
  ) > 0,
  'SELECT 1',
  CONCAT('CREATE INDEX ', @indexname, ' ON ', @tablename, '(author_id)')
));
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
步骤 2: 启动基础服务
bash 复制代码
# 启动 MySQL 和 Redis
# 使用 --env-file 参数直接指定环境文件,确保容器内能获取到正确的环境变量值
docker compose -f docker/docker-compose.dev.yml --env-file .env.development up -d mysql redis

阶段四: 后端基础框架搭建

🎯 本阶段目标

搭建 Express + 中间件 + 工具函数的基础框架,为后续业务逻辑开发提供基础设施。

✅ 预期结果

  • Express 服务器成功启动,可监听指定端口
  • 所有中间件正确配置并生效(CORS、Helmet、安全等)
  • 健康检查接口返回详细的状态信息
  • 日志系统正常工作,记录请求和错误信息
  • 错误处理中间件正确捕获并格式化错误响应

📏 验证标准

检查项 验证命令 期望结果
服务启动 cd packages/backend && pnpm run dev 服务器成功启动,显示监听端口
健康检查 curl http://localhost:3001/api/health 返回包含状态信息的 JSON
CORS 配置 检查跨域请求 允许配置的域名访问
日志记录 查看 logs/ 目录 包含 combined.log 和 error.log
错误处理 curl http://localhost:3001/api/nonexistent 返回 404 错误,格式化响应
中间件顺序 检查请求日志 请求按正确顺序通过中间件
内存监控 健康检查响应 包含 RSS、堆内存等指标
环境变量 检查配置加载 不同环境加载对应配置

📦 技术栈详情

  • 后端框架: Node.js 18+ + Express 5.2.1
  • 数据库: MySQL 8.0 + Sequelize 6.37.7 ORM
  • 缓存: Redis 8.4.0
  • 认证授权: JWT + bcryptjs
  • 安全防护 : Helmet + CORS + 限流
    • Winston: 日志系统
    • CORS: 跨域处理
    • Helmet: 安全中间件
    • bcryptjs: 密码哈希(加密)库
  • 日志系统: Winston
  • API文档: Swagger
  • 容器化: Docker + Docker Compose
  • 代码规范: ESLint + Prettier + Husky + lint-staged
  • 包管理: pnpm workspaces (monorepo)
  • 时间处理: Day.js
  • 工具库: Lodash-es + 共享工具模块

📋 环境依赖清单

工具 版本要求 安装方式 验证命令
Node.js ≥ 18.0.0 已安装 node --version
npm/pnpm ≥ 9.0.0 已安装 pnpm --version
MySQL 8.0 Docker 容器 docker ps
Redis 8.4.0 Docker 容器 docker ps
bcryptjs 3.0.3 pnpm add bcryptjs 已安装

🧭 操作步骤

步骤 1: 项目结构初始化

**主要依赖包(已配置): **

  • Web框架: Express 4.18.2
  • 安全: Helmet 7.1.0 + CORS 2.8.5
  • 日志: Winston 3.11.0
  • 工具: dayjs 1.11.10, debug 4.3.4
步骤 1.5: 从零开始创建(可选)

如果需要重新创建后端项目,以下是具体的创建步骤:

bash 复制代码
# 1. 初始化后端项目
mkdir packages/backend
cd packages/backend
pnpm init

# 2. 安装核心依赖
pnpm add express mysql2 redis sequelize bcryptjs jsonwebtoken cors helmet
pnpm add express-rate-limit express-validator winston dotenv swagger-jsdoc swagger-ui-express
pnpm add debug dayjs

# 3. 安装开发依赖
pnpm add -D eslint nodemon

# 4. 创建 package.json 脚本
# 修改 package.json 添加 scripts 字段
"main": "app.js",
"type": "module",
"scripts": {
  "dev": "node --watch app.js",
  "start": "node app.js",
  "init-db": "node scripts/init-db.js",
  "check-indexes": "node scripts/check-indexes.js"
},

# 5. 核心目录结构
mkdir -p config models modules middleware utils scripts logs
# 然后创建相应的文件和配置
步骤 2: 配置文件搭建

创建 touch config/index.js:

javascript 复制代码
import dotenv from 'dotenv';

// 加载环境变量
const env = process.env.NODE_ENV || 'development';
const envFiles = [
  '../../.env.local',        // 本地覆盖(最高优先级)
  `../../.env.${env}`,       // 环境特定配置
  '../../.env'               // 默认配置
];

envFiles.forEach(file => {
  try {
    dotenv.config({ path: file });
  } catch (error) {
    // 静默跳过不存在的文件
  }
});

// 配置对象
const config = {
  // env config
  env: process.env.NODE_ENV,

  // server config
  server: {
    host: process.env.HOST,
    port: process.env.PORT
  },

  // db config
  database: {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    name: process.env.DB_NAME,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    pool: {
      max: process.env.NODE_ENV === 'production' ? 10 : 5,
      min: 0,
      acquire: 30000,
      idle: 10000
    }
  },

  // redis cache config
  redis: {
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT,
    password: process.env.REDIS_PASSWORD,
  },

  // cors config - cors 是用来解决 跨域资源共享
  // 用于配置 cors: 跨域资源共享 (CORS) 的配置
  cors: {
    // origin: process.env.CORS_ORIGIN,
    origin: function (origin, callback) {
      // 允许没有 origin 的请求(比如移动端应用或 Postman 测试)
      if (!origin) return callback(null, true);
      
      const isProduct = process.env.NODE_ENV === 'production';
      if (isProduct) {
        const corsOrigin = process.env.CORS_ORIGIN || '';
        // 生产环境: 只允许 corsOrigin 里指定的 访问源
        const allowedOrigins = corsOrigin ? corsOrigin.split(',') : [];
        if (allowedOrigins.includes(origin)) {
          return callback(null, true);
        }
        else {
          // 拒绝不在允许列表中的 origin
          const msg = `该网站的CORS策略不允许从指定的源访问: ${origin}`;
          return callback(new Error(msg), false);
        }
      } else {
        // 开发环境, 全部放行
        return callback(null, true);
      }
    },
    credentials: true // 允许发送 cookies 和认证信息
  },

  // logger config
  logging: {
    level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
    file: process.env.NODE_ENV === 'production'
  }
};

export default config;

创建 touch config/database.js

javascript 复制代码
import { Sequelize } from 'sequelize';
import config from './index.js';

const options = {
  host: config.database.host,
  port: config.database.port,
  database: config.database.name,
  username: config.database.user,
  password: config.database.password,
  dialect: 'mysql',
  logging: config.env === 'development' ? console.log : false,
  pool: config.database.pool,
  timezone: '+08:00'
};

const sequelize = new Sequelize(options);

export default sequelize;
步骤 3:工具函数创建

创建 touch utils/logger.js

步骤 4:中间件创建

创建 touch middleware/requestLogger.js

创建 touch middleware/errorHandler.js

步骤 5:应用入口创建

创建 touch app.js

javascript 复制代码
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';

// process.env 在 ./config/index.js 里处理
import config from './config/index.js';
import sequelize from './config/database.js'; // 导入数据库连接
import logger from './utils/logger.js';

/* 
  导入所有模型,确保所有 Sequelize 模型在应用启动时被正确注册和初始化。
  具体作用:
    1. 模型注册 - 让 Sequelize 识别所有定义的数据库模型(User、Article、Category 等)
    2. 关联关系建立 - 执行模型之间的关联关系(如外键、一对多、多对多关系)
    3. 数据库同步支持 - 为后续的 sequelize.sync() 操作提供完整的模型信息,确保数据库表结构能正确创建或更新
    4. 副作用执行 - 即使没有直接使用导入的内容,也会执行 models/index.js 文件中的所有初始化代码
  这是 Sequelize 应用的标准做法,必须在数据库连接和同步之前完成模型的注册。
*/
import './models/index.js';

import { requestLogger } from './middleware/requestLogger.js'; // 导入请求日志记录器
import { errorHandler, notFoundHandler } from './middleware/errorHandler.js'; // 错误处理中间件

const app = express();
const PORT = config.server.port;

// 中间件配置
app.use(helmet({
  contentSecurityPolicy: false, // 禁用CSP, 由前端 vite-plugin-csp 和 nginx 处理
})); // 安全中间件 - helmet: 设置各种 HTTP 头以增强安全性

app.use(cors(config.cors)); // cors: 跨域资源共享 (CORS)

// 安全中间件 - cors: 跨域资源共享 (CORS)
app.use(express.json()); // 解析请求体(JSON)
app.use(express.urlencoded({ extended: true })); // 解析请求体(URL 编码)
app.use(requestLogger); // 请求日志(requestLogger)

// 健康检查接口 - 动态检测各项服务状态
app.get('/api/health', async (req, res) => {
  logger.info('健康检查开始');

  // 初始化状态变量
  let dbStatus = 'disconnected';
  let redisStatus = 'disconnected';
  let overallStatus = 'healthy';

  try {
    // 1. 检查 MySQL 数据库连接
    try {
      // 动态导入数据库连接(避免循环依赖)
      const { default: sequelize } = await import('./config/database.js');
      await sequelize.authenticate();
      dbStatus = 'connected';
      logger.info('✅ 数据库连接正常');
    } catch (dbError) {
      logger.warn('❌ 数据库连接失败:', dbError.message);
      dbStatus = 'disconnected';
      overallStatus = 'degraded'; // 降级但不致命
    }

    // 2. 检查 Redis 连接(如果已配置)
    

    // 3. 获取内存使用情况
    const memUsage = process.memoryUsage();
    const memoryInfo = {
      rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,           // 常驻内存
      heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, // 堆内存总量
      heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,   // 已用堆内存
      external: `${Math.round(memUsage.external / 1024 / 1024)}MB`    // 外部内存
    };

    // 4. 构建响应数据
    const healthData = {
      status: overallStatus,
      timestamp: new Date().toISOString(),
      uptime: `${Math.round(process.uptime())}s`,
      version: process.version,
      environment: process.env.NODE_ENV || 'development',
      database: dbStatus,
      memory: memoryInfo,
      pid: process.pid
    };

    // 5. 根据整体状态返回相应的 HTTP 状态码
    const httpStatus = overallStatus === 'healthy' ? 200 : 503;

    res.status(httpStatus).json({
      code: httpStatus,
      message: overallStatus === 'healthy' ? '服务正常' : '服务部分异常',
      data: healthData
    });

    logger.info(`🏥 健康检查完成 - 状态: ${overallStatus}`);

  } catch (error) {
    // 捕获所有未预期的错误
    logger.error(`💥 健康检查发生严重错误: ${error.message}`, {
      stack: error.stack,
      uptime: process.uptime(),
      memory: process.memoryUsage()
    });

    // 返回服务不可用状态
    res.status(503).json({
      code: 503,
      message: '服务异常',
      data: {
        status: 'unhealthy',
        timestamp: new Date().toISOString(),
        uptime: `${Math.round(process.uptime())}s`,
        version: process.version,
        environment: process.env.NODE_ENV || 'development',
        database: dbStatus,
        error: error.message,
        pid: process.pid
      }
    });
  }
});

// 错误处理
app.use(notFoundHandler);   // 处理 404 错误
app.use(errorHandler);      // 全局错误处理

// 启动服务
const startServer = async () => {
  try {
    // ✅ 验证 MySQL 连接可用
    // 动态导入数据库连接(避免循环依赖)
      const { default: sequelize } = await import('./config/database.js');
    await sequelize.authenticate();
    logger.info('数据库连接成功');

    logger.info(`服务器运行在 http://localhost:${PORT}`);

    // ✅ 同步 Sequelize 模型(开发环境)
    if (config.env === 'development') {
      try {
        await sequelize.sync({ alter: true });
        // await sequelize.sync({ force: true });
        logger.info('数据库模型已同步');
      } catch (syncError) {
        logger.warn('数据库模型同步失败(可能是表结构已存在):', syncError.message);
        logger.info('继续启动服务器...');
      }
    }

    // ✅ 监听指定端口(默认 3001)
    // ✅ 输出启动成功日志
    app.listen(PORT, () => {
      console.log('\n' + '='.repeat(60));
      console.log('🚀 服务器启动成功!', config.env);
      console.log('='.repeat(60));
      console.log(`📍 服务地址: http://localhost:${PORT}`);
      console.log(`📚 API 文档: http://localhost:${PORT}/api-docs`);
      console.log(`🏥 健康检查: http://localhost:${PORT}/api/health`);
      console.log('='.repeat(60) + '\n');
      
      logger.info(`服务器运行在 http://localhost:${PORT}`);
      logger.info(`API 文档: http://localhost:${PORT}/api-docs`);
    });
  } catch (error) {
    console.log('\n⚠️  服务器启动失败,请解决上述问题后重试\n');
    process.exit(1);
  }
};

// 执行启动服务器
startServer();

阶段五:数据库设计与初始化

🎯 本阶段目标

创建数据库表结构,初始化基础数据。

✅ 预期结果

  • Sequelize 模型正确定义,包含所有必要字段
  • 数据库表结构自动创建或同步成功
  • 基础数据(管理员用户、分类等)正确插入
  • 数据库索引优化配置生效
  • 密码加密工具正常工作

📏 验证标准

检查项 验证命令 期望结果
表结构同步 cd packages/backend && pnpm run init-db 无错误,显示同步成功
模型定义 检查 models/ 目录 包含 User.js, Category.js 等模型文件
基础数据 查询数据库 包含管理员用户和默认分类
密码加密 测试 hashPassword 函数 密码正确加密和验证
数据库索引 SHOW INDEX FROM users; 包含必要的索引
外键关系 检查表结构 模型间的关联关系正确
数据完整性 插入测试数据 无约束冲突,数据正确存储

📦 技术栈详情

  • 数据库: MySQL 8.0 + Sequelize 6.37.7 ORM

🧭 操作步骤

步骤 1:创建utils工具脚本

创建 touch utils/bcrypt.js

步骤 2:创建数据模型

创建 touch models/User.js:

创建 touch models/Category.js:

创建 touch models/index.js:

步骤 3:创建初始化脚本

创建 touch scripts/init-db.js

步骤 4:运行初始化脚本
bash 复制代码
cd packages/backend
pnpm run init-db

阶段六:JWT认证系统

🎯 本阶段目标

JWT认证系统实现方案: 构建 backend + frontend 的JWT登录管理

✅ 预期结果

  • 用户注册、登录、登出功能完整实现
  • JWT token 正确生成、验证和过期处理
  • 权限控制中间件按角色正确过滤访问
  • 密码安全加密存储,支持验证
  • 认证失败时返回适当的错误信息

📏 验证标准

检查项 验证命令 期望结果
用户注册 curl -X POST http://localhost:3001/api/auth/register -d '{"username":"test","password":"123456"}' 返回 201 状态和 token
用户登录 curl -X POST http://localhost:3001/api/auth/login -d '{"username":"admin","password":"admin123"}' 返回 200 状态和 token
JWT 验证 带 token 访问受保护接口 返回用户数据,无 401 错误
权限控制 普通用户访问管理员接口 返回 403 权限不足错误
Token 过期 使用过期 token 访问 返回 401 无效令牌错误
密码验证 错误的密码登录 返回 401 用户名或密码错误
用户信息获取 GET /api/users/current 返回当前用户信息

📦 技术栈详情

  • 认证授权: JWT + bcryptjs
  • jsonwebtoken: JWT token生成和验证
  • bcryptjs: 密码哈希加密
  • express-validator: 他是 Node.js/Express 生态中处理请求参数校验和数据验证的核心中间件库,专门解决后端接口的参数合法性问题

📋 环境依赖清单

工具 版本要求 安装方式 验证命令
jsonwebtoken 9.0.3 pnpm add jsonwebtoken 已安装
bcryptjs 3.0.3 pnpm add bcryptjs 已安装
express-validator 7.3.1 pnpm add express-validator 已安装

🧭 操作步骤

步骤 1:更新环境变量配置

检查在 .env.evn.development 文件中是否添加JWT配置:

bash 复制代码
# JWT配置
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRES_IN=7d
javascript 复制代码
  // jwt config
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN
  },
  cors: { ... }
步骤 2:创建JWT工具函数 和 Redis 缓存工具类(cache.js)

首先创建JWT相关的工具函数: 进入服务端代码目录: cd packages/backend 创建 touch utils/jwt.js

步骤 3:创建认证中间件 和 express-validator 中间件

创建: touch middleware/auth.js

创建: touch middleware/validator.js

步骤 4:创建Auth控制器、路由 和 服务

创建modules所需目录: mkdir -p modules/{auth,user,article,stats}

4.1 创建控制器 创建: touch modules/auth/controller.js

4.2 创建路由 创建: touch modules/auth/router.js

4.2 创建Auth服务(通过Sequelize与MySQL通信) 创建: touch modules/auth/service.js

步骤 5:创建User控制器、路由 和 服务

创建: touch modules/user/controller.js

创建: touch modules/user/router.js

创建: touch modules/user/service.js

步骤 6:app.js 路由补全
javascript 复制代码
// 导入路由
import userRouter from './modules/user/router.js'; // 用户路由
import authRouter from './modules/auth/router.js'; // 认证路由

const app = express();
...

// 使用路由 - API 路由, 业务路由挂载
app.use('/api/users', userRouter); // 用户模块, 用户注册、登录、信息管理
app.use('/api/auth', authRouter); // 认证模块, 身份认证、权限控制、Token 管理
步骤 7:测试认证系统

创建测试用户后,可以测试以下接口:

  • 在packages/backend项根目录 重新启动服务: npm run dev
  • 在packages/backend项根目录 创建 touch test-users.sh 文件
  • 编辑此文件
bash 复制代码
  #!/bin/bash

  # 服务器地址
  BASE_URL="http://localhost:3001"

  # 登录获取 token
  echo "=== 登录获取 Token ==="
  LOGIN_RESPONSE=$(curl -s -X POST $BASE_URL/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{"username":"admin","password":"admin123"}')

  TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.data.token')
  echo "Token: $TOKEN"

  # 测试各个接口
  echo -e "\n=== 获取当前用户信息 ==="
  curl -X GET $BASE_URL/api/users/current \
    -H "Authorization: Bearer $TOKEN" | jq

  echo -e "\n=== 获取用户列表 ==="
  curl -X GET "$BASE_URL/api/users?page=1&pageSize=5" \
    -H "Authorization: Bearer $TOKEN" | jq

  echo -e "\n=== 获取用户详情 (ID=1) ==="
  curl -X GET $BASE_URL/api/users/1 \
    -H "Authorization: Bearer $TOKEN" | jq
  • 给脚本添加执行权限: chmod +x test-users.sh
  • 执行脚本, 使用 bash 命令运行: bash test-users.sh
  • 预期输出示例
bash 复制代码
=== 登录获取 Token ===
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

=== 获取当前用户信息 ===
{
  "code": 200,
  "message": "获取成功",
  "data": {
    "id": 1,
    "username": "admin",
    "email": "admin@example.com",
    "role": "admin",
    "status": "active"
  }
}

=== 获取用户列表 ===
{
  "code": 200,
  "message": "获取成功",
  "data": {
    "list": [...],
    "total": 1,
    "page": 1,
    "pageSize": 5,
    "totalPages": 1
  }
}

✅ 实际测试验证

完成 JWT 认证系统搭建后,让我们进行完整的 API 测试验证,确保所有功能正常工作。

测试环境准备
bash 复制代码
# 1. 确保基础服务运行
pnpm run docker-dev:up

# 2. 启动后端服务
cd packages/backend
pnpm run dev

# 3. 验证健康检查
curl http://localhost:3001/api/health | jq
# 应返回数据库连接状态正常

阶段七:Redis初始化

🎯 本阶段目标

集成 Redis 缓存服务,实现数据缓存、会话管理等功能。

✅ 预期结果

  • Redis 连接配置正确,客户端成功连接
  • 缓存服务类实现完整的 CRUD 操作
  • 健康检查包含 Redis 连接状态
  • Token 存储到 Redis,支持会话管理
  • 缓存数据在重启后可恢复

📏 验证标准

检查项 验证命令 期望结果
Redis 连接 健康检查接口 redis 状态为 "connected"
缓存操作 测试缓存 set/get 数据正确存储和读取
Token 存储 登录后检查 Redis token 存储在 Redis 中
缓存过期 设置过期时间的数据 到期后自动清除
连接恢复 重启 Redis 后 应用自动重连
内存监控 Redis INFO 命令 内存使用正常
数据持久化 重启容器后 AOF 数据保留

📝 Redis 缓存策略

缓存键命名规范:

  • user:{id} - 用户信息缓存
  • article:{id} - 文章内容缓存
  • articles:list:{page} - 文章列表缓存
  • token:{userId} - 用户认证令牌

缓存过期时间:

  • 用户信息:1小时
  • 文章内容:30分钟
  • 文章列表:10分钟
  • 认证令牌:7天

缓存更新策略:

  • 数据更新时主动删除相关缓存
  • 使用缓存穿透保护
  • 实现缓存预热机制

📦 技术栈详情

  • 缓存: Redis 8.4.0, 内存数据库和缓存服务

📋 环境依赖清单

工具 版本要求 安装方式 验证命令
Redis 5.10.0 pnpm add redis 已安装
Docker Redis 8.4.0 Docker 容器 docker ps

📁 项目结构概览

bash 复制代码
packages/backend/
├── config/
│   └── redis.js           # Redis 连接配置
├── utils/
│   └── cache.js           # Redis 缓存服务类(已创建)
└── app.js                 # 应用入口(需要集成 Redis)

当前项目状态分析

已有的资源:

  • ✅ Redis 依赖已安装(redis 5.10.0)
  • ✅ Docker Redis 服务配置已完成

缺失的部分:

  • ❌ Redis 连接配置已实现(config/redis.js)
  • ❌ 后端集成 Redis 连接
  • ❌ Redis 缓存服务在应用中的使用

🧭 操作步骤

停止服务 + 清理数据库:
dash 复制代码
# 停止 Docker 服务
pnpm docker-dev:down

# 删除数据库卷(这会删除所有数据)
# pnpm docker-dev:delete (暂时取消此步骤)

# 重新启动服务
pnpm docker-dev:up
步骤 1:确认 Redis 连接配置

项目已包含完整的 Redis 连接配置:

bash 复制代码
packages/backend/
├── config/
    └── redis.js        # Redis 连接配置
步骤 2:Redis 连接配置详解

cd packages/backend 目录下, 创建 touch config/redis.js

cd packages/backend 目录下, 创建 touch utils/cache.js

javascript 复制代码
/** 
 * 核心逻辑是使用 redisClient 实例 执行 Redis 操作,
 * 通过 try-catch 块处理错误,
 * 使用 logger 记录错误信息,
 * 最后 导出 单例实例 供其他模块 使用。
*/

import redisClient from '../config/redis.js';
import logger from './logger.js';

/**
 * Redis 缓存工具类
 */
class CacheService {
  /**
   * 获取缓存
   * @param {string} key 缓存键
   * @returns {Promise<any>} 缓存值
   */
  async get(key) {
    try {
      const value = await redisClient.get(key);
      return value ? JSON.parse(value) : null;
    } catch (error) {
      logger.error(`Cache get error: ${error.message}`);
      return null;
    }
  }

  /**
   * 设置缓存
   * @param {string} key 缓存键
   * @param {any} value 缓存值
   * @param {number} expire 过期时间(秒)
   */
  async set(key, value, expire = 3600) {
    try {
      await redisClient.setEx(key, expire, JSON.stringify(value));
    } catch (error) {
      logger.error(`Cache set error: ${error.message}`);
    }
  }

  /**
   * 删除缓存
   * @param {string} key 缓存键
   */
  async del(key) {
    try {
      await redisClient.del(key);
    } catch (error) {
      logger.error(`Cache del error: ${error.message}`);
    }
  }

  /**
   * 批量删除缓存(支持通配符)
   * @param {string} pattern 匹配模式
   */
  async delPattern(pattern) {
    try {
      const keys = await redisClient.keys(pattern);
      if (keys.length > 0) {
        await redisClient.del(keys);
      }
    } catch (error) {
      logger.error(`Cache delPattern error: ${error.message}`);
    }
  }

  /**
   * 检查缓存是否存在
   * @param {string} key 缓存键
   * @returns {Promise<boolean>}
   */
  async exists(key) {
    try {
      return await redisClient.exists(key) === 1;
    } catch (error) {
      logger.error(`Cache exists error: ${error.message}`);
      return false;
    }
  }
}

export default new CacheService();
步骤 4:在应用中集成 Redis + 更新健康检查接口

更新 app.js,集成 Redis 连接:

javascript 复制代码
/** 
 * 核心逻辑是更新健康检查接口,集成 Redis 连接,并返回 Redis 连接状态。
*/

import express from 'express';
// ... 其他导入
import redisClient from './config/redis.js'; // 导入 Redis 客户端

const app = express();
// ... 中间件配置

// 路由配置(后续添加)
app.use('/api/auth', authRouter); // 认证模块, 身份认证、权限控制、Token 管理
app.use('/api/users', userRouter); // 用户模块, 用户注册、登录、信息管理
// app.use('/api/articles', articleRouter);

app.get('/api/health', async (req, res) => {
  logger.info('健康检查开始');

  /*
    健康检查内容:
      ✅ MySQL 数据库连接状态
      ✅ Redis 缓存连接状态
      ✅ 内存使用情况(RSS、Heap Total、Heap Used)
      ✅ 服务运行时间(Uptime)
      ✅ Node.js 版本信息
      ✅ 环境标识
  */

  // 初始化状态变量
  let dbStatus = 'disconnected';
  let redisStatus = 'disconnected';
  let overallStatus = 'healthy';

  try {
    // 1. 检查 MySQL 数据库连接
    ...

    // 2. 检查 Redis 连接
    try {
      // 尝试动态导入 Redis 客户端(可能还未创建)
      const { default: redisClient } = await import('./config/redis.js');
      await redisClient.ping();
      redisStatus = 'connected';
      logger.info('✅ Redis 连接正常');
    } catch (redisError) {
      // Redis 连接失败不影响整体健康状态
      logger.warn('⚠️ Redis 连接失败,将以无缓存模式运行:', redisError.message);
      redisStatus = 'disconnected';
    }

    // 3. 获取内存使用情况
    ...

    // 4. 构建响应数据
    const healthData = {
      ...
      redis: redisStatus,
      memory: memoryInfo,
      pid: process.pid
    };

    // 5. 根据整体状态返回相应的 HTTP 状态码
    ...

  } catch (error) {
    ...
    // 返回服务不可用状态
    res.status(503).json({
      code: 503,
      message: '服务异常',
      data: {
        ...
        redis: redisStatus,
        error: error.message,
        pid: process.pid
      }
    });
  }
});

// 启动服务
const startServer = async () => {
  try {
    // 验证数据库连接
    // 动态导入数据库连接(避免循环依赖)
      const { default: sequelize } = await import('./config/database.js');
    await sequelize.authenticate();
    logger.info('数据库连接成功');

    // 检查 Redis 连接
    let redisStatus = 'disconnected';
    try {
      // 尝试动态导入 Redis 客户端(可能还未创建)
      const { default: redisClient } = await import('./config/redis.js');
      await redisClient.ping();
      redisStatus = 'connected';
      logger.info('Redis 连接成功');
    } catch (redisError) {
      logger.warn('Redis 连接失败,将以无缓存模式运行:', redisError.message);
    }

    // 启动服务器
    ...

  } catch (error) {
    ...
  }
};
步骤 5:Redis 缓存使用示例

在业务代码中使用缓存服务, 比如: modules/auth/service.js:

javascript 复制代码
// 在 auth service 中使用缓存存储 token
import cacheService from '../utils/cache.js';

class AuthService {
  async login(username, password) {
    // ... 登录逻辑

    // 为了避免每次用户登录都会生成新的token并覆盖存储在Redis中的旧token,这会导致同一个用户可以同时拥有多个有效的登录会话。
    // 检查用户是否已经登录,如果已登录 则先登出之前的登录
    const existingToken = await cacheService.get(`token:${user.id}`);
    if (existingToken) {
      // 在日志中记录重复登录行为,便于监控
      logger.info(`用户 ${username} 已在其他地方登录,执行登出操作`);
      // 当用户重复登录时,系统会先删除之前的token
      await cacheService.del(`token:${user.id}`);
    }

    // 生成 token
    const token = generateToken({
      userId: user.id,
      username: user.username,
      role: user.role
    });

    console.log('生成 Token: ', token);

    // 将 token 存储到 Redis(7天过期)
    await cacheService.set(`token:${user.id}`, token, 7 * 24 * 60 * 60);

    // 返回用户信息(不包含密码)和 Token
    const userData = user.toJSON();
    delete userData.password;

    return {
      user: userData,
      token
    };
  }

  async logout(userId) {
    // 从 Redis 中删除 token
    await cacheService.del(`token:${userId}`);
    return true
  }

  async register(data) {
    // ... 注册逻辑
    await cacheService.set(`token:${user.id}`, token, 7 * 24 * 60 * 60);

    return {
      user,
      token
    };
  }
}

export default new AuthService();

✅ 验证方式

bash 复制代码
# 1. 检查 Redis 容器状态 
docker compose -f docker/docker-compose.dev.yml --env-file ./.env.development ps redis
# 应显示 redis 容器状态为 Up/Healthy

# 2. 测试 Redis 连接
docker compose -f docker/docker-compose.dev.yml --env-file ./.env.development exec redis redis-cli -a redis123 ping
# 应返回 PONG

# 3. 测试 Redis 密码认证
docker compose -f docker/docker-compose.dev.yml --env-file ./.env.development exec redis redis-cli -a redis123
# 进入 Redis CLI 后执行:
AUTH redis123
# 应返回 OK

# 4. 检查后端启动日志
cd packages/backend && pnpm run dev
# 日志中应显示 "Redis 连接成功"

# 5. 测试健康检查接口
curl http://localhost:3001/api/health | jq
# 应返回详细的健康状态信息,包含:
# - status: "healthy"(数据库和Redis都正常)
# - database: "connected"
# - redis: "connected"
# - memory: 内存使用情况
# - uptime: 服务运行时间
# - version: Node.js版本

# 6. 测试缓存功能
# 在 Redis CLI 中查看缓存数据:
# KEYS *
# GET token:1 (如果有用户登录)

这个阶段为后续的JWT认证、会话管理和数据缓存功能奠定了基础。接下来可以进入"阶段八:JWT + auth + Login"来实现完整的认证系统。


阶段八:前端后台管理(可选)

🎯 本阶段目标

构建 Vue3 + Vite 后台管理界面。

✅ 预期结果

  • Vue3 + Vite 开发服务器成功启动
  • Element Plus 组件库正确集成
  • 登录页面可正常访问和使用
  • API 请求正确配置,支持跨域
  • 路由系统正常工作,支持导航

📏 验证标准

检查项 验证命令 期望结果
开发服务器 cd packages/frontend && pnpm run dev 服务器启动在 5173 端口
页面访问 浏览器访问 http://localhost:5173 显示登录页面
API 集成 检查网络请求 请求发送到后端 API
组件渲染 Element Plus 组件 正确显示和交互
路由跳转 点击登录按钮 成功跳转到对应页面
构建测试 pnpm run build 生成 dist 目录
热重载 修改代码 页面自动刷新
环境变量 VITE_API_BASE_URL 正确配置 API 地址

📋 技术栈详情

  • 前端框架: Vue 3.5.24 + Nuxt 4.2.2 (SSR)
  • 构建工具: Vite (rolldown-vite@7.2.5) + pnpm 9.0.0
  • UI组件库: Element Plus 2.13.0 + 图标库
  • 状态管理: Pinia 3.0.4
  • 路由管理: Vue Router 4.x
  • HTTP客户端: Axios 1.13.2
  • 数据可视化: ECharts 6.0.0
  • Markdown编辑器: Vditor 3.11.2 + md-editor-v3 6.3.1

📋 环境依赖清单

工具 版本要求 安装方式 验证命令
Vue 3.5.24 pnpm add vue 已安装
Vite rolldown-vite@7.2.5 pnpm add vite 已安装
Element Plus 2.13.0 pnpm add element-plus 已安装
Pinia 3.0.4 pnpm add pinia 已安装
Vue Router 4.2.5 路由管理 已安装
Axios 1.13.2 HTTP 客户端 已安装
ECharts 6.0.0 图表库 已安装
Vditor 3.11.2 Markdown 编辑器 已安装

📁 项目结构概览

前端项目结构:

bash 复制代码
packages/frontend/
├── src/
│   ├── api/              # API 接口封装
│   │   ├── index.js     # API 基础配置
│   │   ├── auth.js      # 认证相关接口
│   │   ├── article.js   # 文章相关接口
│   │   ├── stats.js     # 统计相关接口
│   │   └── user.js      # 用户相关接口
│   ├── components/       # 全局组件
│   │   ├── BaseEChart.vue    # 图表组件
│   │   ├── CanvasText.vue    # 画布文本组件
│   │   └── MarkdownEditor.vue # Markdown 编辑器
│   ├── views/            # 页面组件
│   │   ├── admin/        # 后台管理页面
│   │   │   ├── Layout.vue     # 后台布局
│   │   │   ├── Login.vue      # 登录页
│   │   │   ├── Dashboard.vue  # 仪表板
│   │   │   ├── ArticleList.vue # 文章列表
│   │   │   ├── ArticleForm.vue # 文章表单
│   │   │   └── UserList.vue   # 用户列表
│   │   └── frontend/      # 前台页面(可选)
│   ├── stores/           # Pinia 状态管理
│   │   └── auth.js       # 认证状态
│   ├── router/           # 路由配置
│   │   └── index.js      # 路由定义
│   ├── composables/      # 组合式函数
│   │   ├── useDebounce.js    # 防抖
│   │   ├── useErrorHandler.js # 错误处理
│   │   ├── useRequest.js     # 请求封装
│   │   └── useTabVisibility.js # 标签页可见性
│   ├── utils/            # 工具函数
│   │   ├── request.js    # Axios 封装
│   │   └── security.js   # 安全工具
│   ├── workers/          # Web Workers
│   │   └── markdown-processor.worker.js
│   └── main.js           # 应用入口
├── public/               # 静态资源
├── vite.config.js        # Vite 配置
└── package.json          # 项目配置

当前项目状态分析

已有的资源:

  • ✅ Vue3 + Vite 项目结构已配置
  • ✅ Element Plus UI 组件库已集成
  • ✅ API 接口封装已定义

缺失的部分:

  • ❌ 认证状态管理实现
  • ❌ 登录组件和路由配置

🧭 操作步骤

步骤 1:项目结构说明

项目已包含完整的前端后台管理配置,直接使用即可。

步骤 2:主要功能模块
  • 后台管理: 文章管理、用户管理、分类管理、数据统计
  • Markdown 编辑: 支持富文本编辑和预览
  • 图表展示: 使用 ECharts 展示统计数据
  • 响应式设计: 支持移动端和桌面端
  • 权限控制: 基于角色的访问控制
步骤 3:开发服务器配置

Vite 配置已包含:

  • 路径别名 : @ 指向 src/, @blog/common 指向共享模块
  • 代理配置: 开发环境 API 代理到后端服务
  • 构建优化: 代码分割、压缩、Tree Shaking
步骤 4:从零开始创建(可选)

如果需要重新创建前端项目,以下是具体的创建步骤:

bash 复制代码
# 1. 初始化 Vue3 + Vite 项目
mkdir packages/frontend
cd packages/frontend
pnpm create vite . --template vue
# 选择: Use rolldown-vite (Experimental)?: Yes
# 选择: Install with pnpm and start now? Yes

# 2. 安装核心依赖
pnpm add vue-router@4 pinia @element-plus/icons-vue element-plus axios
pnpm add echarts @sunny-117/text-image lodash-es md-editor-v3 vditor

# 3. 安装开发依赖
pnpm add -D @vitejs/plugin-vue eslint eslint-plugin-vue prettier
pnpm add -D husky lint-staged sass rollup-plugin-visualizer terser
pnpm add -D unplugin-auto-import unplugin-vue-components

# 4. 创建项目结构
mkdir -p src/{api,views,router,stores,utils,composables,workers}
mkdir -p src/views/{admin,frontend}

# 然后创建相应的文件和组件

# 5. 配置 Vite 别名
# 修改 vite.config.js 添加路径别名配置
```javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
      '@blog/common': fileURLToPath(new URL('../common', import.meta.url))
    }
  },
  // 明确指定环境文件搜索路径
  envDir: '../../'
})

# 6. 配置 Pinia + 导入vue-router入口文件
# 修改 main.js 添加路径别名配置
```javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';

import './style.css'
import App from './App.vue'
import router from './router'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.use(router)
app.use(ElementPlus, { locale: zhCn });

// 等待路由就绪后再挂载应用, 发现问题: 应用还没等路由准备好就 mount,初次导航被打断,界面表现成刷新/闪一下。
router.isReady().then(() => {
  app.mount('#app');
});

// app.mount('#app');
步骤 5:核心组件创建

创建登录组件 touch src/views/admin/Login.vue

javascript 复制代码
<template>
  <div class="login-container">
    <el-form :model="form" @submit.prevent="handleLogin">
      <el-form-item label="用户名">
        <el-input v-model="form.username" />
      </el-form-item>
      <el-form-item label="密码">
        <el-input v-model="form.password" type="password" />
      </el-form-item>
      <el-button type="primary" native-type="submit" :loading="loading">
        登录
      </el-button>
    </el-form>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../../stores/auth'
import { ElMessage } from 'element-plus'

const form = ref({ username: '', password: '' })
const loading = ref(false)
const router = useRouter()
const authStore = useAuthStore()

const handleLogin = async () => {
  if (!form.value.username || !form.value.password) {
    ElMessage.error('请输入用户名和密码')
    return
  }
  
  loading.value = true
  try {
    await authStore.login(form.value)
    ElMessage.success('登录成功')
    router.push('/')
  } catch (error) {
    ElMessage.error(error.response?.data?.message || '登录失败')
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.login-container {
  max-width: 400px;
  margin: 100px auto;
  padding: 20px;
}
</style>
步骤 6:路由配置

创建 touch src/router/index.js

javascript 复制代码
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/login',
    component: () => import('../views/admin/Login.vue')
  }
]

export default createRouter({
  history: createWebHistory(),
  routes
})
步骤 7:API接口封装

创建 touch src/api/auth.js

javascript 复制代码
import request from '../utils/request'

export const login = (data) => request.post('/auth/login', data)
export const logout = () => request.post('/auth/logout')
步骤 8:状态管理

创建 touch src/stores/auth.js

javascript 复制代码
import { defineStore } from 'pinia'
import { login } from '../api/auth'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: localStorage.getItem('token')
  }),
  
  actions: {
    async login(credentials) {
      const res = await login(credentials)
      this.token = res.data.token
      this.user = res.data.user
      localStorage.setItem('token', this.token)
    },
    
    logout() {
      this.user = null
      this.token = null
      localStorage.removeItem('token')
    }
  }
})
步骤9: Axios请求封装

创建 touch src/utils/request.js

javascript 复制代码
import axios from 'axios'

// 创建 axios 实例
const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
request.interceptors.request.use(
  config => {
    // 从 localStorage 获取 token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
request.interceptors.response.use(
  response => {
    return response
  },
  error => {
    if (error.response?.status === 401) {
      // token 过期,清除本地存储并跳转到登录页
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default request
步骤10: 修改 App.js

先删除不用的components文件, 如:HelloWorld.vue rm src/components/HelloWorld.vue

修改App.vue

vue 复制代码
<template>
  <router-view />
</template>

<script setup>
// App 根组件

// 处理全局 locale 配置(可选): 引入Element Plus 中文语言包
// import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#app {
  min-height: 100vh;
}
</style>
✅ 验证方式
bash 复制代码
cd packages/frontend

# 1. 检查 request.js 文件是否创建成功
ls -la src/utils/request.js

# 2. 测试前端开发服务器启动
pnpm run dev
# 应显示: Local: http://localhost:5173/

# 3. 验证环境变量配置
# 创建 .env.development 文件(如果不存在)
# echo "VITE_API_BASE_URL=http://localhost:3001/api" > .env.development

# 4. 测试 API 请求(需要先启动后端服务)
# 在浏览器开发者工具中测试:
# fetch('http://localhost:5173/api/health')
# 应该能看到请求发送到后端

# 5. 检查登录功能
# 访问 http://localhost:5173/login
# 输入用户名: admin, 密码: admin123
# 点击登录按钮,观察网络请求和响应

# 6. 验证 token 存储
# 登录成功后,检查 localStorage 中是否有 token
# 在浏览器控制台输入: localStorage.getItem('token')

# 7. 测试请求拦截器
# 打开浏览器开发者工具 Network 标签
# 观察所有 API 请求是否自动添加了 Authorization 头

阶段九:Nuxt SSR 前台(可选)

🎯 本阶段目标

实现首页 SSR,提升首屏加载性能和 SEO。

✅ 预期结果

  • Nuxt3 开发服务器成功启动
  • 首页实现服务端渲染(SSR)
  • SEO 优化配置正确(meta 标签等)
  • 路由系统按配置正确渲染
  • API 集成正常工作

📏 验证标准

检查项 验证命令 期望结果
SSR 渲染 查看页面源码 包含完整 HTML 结构
开发服务器 cd packages/nuxt-ssr && pnpm run dev 服务器启动在 3000 端口
页面访问 浏览器访问 http://localhost:3000 显示 SSR 渲染的页面
SEO 优化 查看页面 meta 标签 包含标题、描述等 SEO 信息
路由渲染 访问不同页面 按 routeRules 配置渲染模式
API 集成 服务端数据获取 数据正确获取和渲染
构建测试 pnpm run build 生成 .output 目录
生产预览 pnpm run preview 生产构建正确运行

技术栈详情

  • 后端框架: ???

📦 环境依赖清单

工具 版本要求 安装方式 验证命令
Nuxt 3.12.0 pnpm add nuxt 已安装
Vue 3.4.0 随 Nuxt 安装 已安装
Pinia 2.1.7 pnpm add @pinia/nuxt 已安装
Element Plus 2.11.9 pnpm add @element-plus/nuxt 已安装

| Day.js | 1.11.19 | 日期处理 |

📁 项目结构概览

bash 复制代码
packages/nuxt-ssr/
├── pages/                 # 页面路由(文件系统路由)
│   └── index.vue          # 首页
├── components/            # 组件
├── layouts/               # 布局
│   └── default.vue        # 默认布局
├── composables/           # 组合式函数
├── stores/                # Pinia 状态管理
├── server/
│   ├── api/              # 服务端 API 路由
│   └── middleware/       # 服务端中间件
├── public/               # 静态资源
└── nuxt.config.ts        # Nuxt 配置

当前项目状态分析

已有的资源:

  • ✅ Nuxt3 项目结构已配置
  • ✅ SSR 路由规则已定义
  • ✅ 运行时配置已设置

缺失的部分:

  • ❌ 服务端 API 路由实现
  • ❌ 页面组件和布局

🧭 操作步骤

步骤 1:项目结构说明

项目已包含完整的 Nuxt SSR 配置,直接使用即可。

Nuxt 项目结构:

bash 复制代码
packages/nuxt-ssr/
├── pages/               # 页面路由(文件系统路由)
│   └── index.vue       # 首页(SSR)
├── components/          # 组件
├── layouts/             # 布局
│   └── default.vue     # 默认布局
├── composables/         # 组合式函数
│   ├── useArticleApi.js # 文章 API
│   └── useRequest.js    # 请求封装
├── stores/              # Pinia 状态管理
│   └── auth.js         # 认证状态
├── server/              # 服务端代码
│   ├── api/            # 服务端 API 路由
│   └── middleware/     # 服务端中间件
├── plugins/             # 插件
│   └── element-plus.ts # Element Plus 插件
├── public/              # 静态资源
├── nuxt.config.ts       # Nuxt 配置(已完整配置)
├── tsconfig.json        # TypeScript 配置
└── package.json         # 项目配置
步骤 2:核心配置特性

已配置的 Nuxt 特性:

  • SSR 配置: 默认开启 SSR,仅首页 SSR,其他页面 CSR
  • 路由规则: 基于页面重要性配置渲染模式
  • 开发代理: 开发环境自动代理 API 请求到后端
  • Element Plus: 完整集成,包括图标和主题
  • TypeScript: 支持 .ts 配置文件
  • 环境变量: 支持运行时配置

路由规则配置:

typescript 复制代码
routeRules: {
  '/': { ssr: true },           // 首页 SSR,提升 SEO
  '/article/**': { ssr: false }, // 文章详情 CSR,提升交互性
  '/category/**': { ssr: false }, // 分类页 CSR
  '/search': { ssr: false },     // 搜索页 CSR
  '/admin/**': { ssr: false }    // 管理后台 CSR
}

运行时配置:

typescript 复制代码
runtimeConfig: {
  public: {
    apiBase: process.env.NUXT_PUBLIC_API_BASE_URL,
    siteBase: process.env.NUXT_PUBLIC_SITE_BASE,
    siteName: process.env.NUXT_PUBLIC_SITE_NAME
  }
}
步骤 3:SEO 和性能优化
  • 服务端渲染: 首页 SSR,提升首屏加载和 SEO
  • 预渲染: 支持静态生成(可选)
  • 代码分割: 自动代码分割和懒加载
  • 优化字体: 字体优化和预加载
  • 图片优化: 自动图片优化
步骤 4:从零开始创建(可选)

如果需要重新创建 Nuxt SSR 项目,以下是具体的创建步骤:

bash 复制代码
# 1. 初始化 Nuxt3 项目
mkdir packages/nuxt-ssr
cd packages/nuxt-ssr
npx nuxi@latest init .
# 这会创建一个基础的 Nuxt3 项目
# 选择1: minimal -- Minimal setup for Nuxt 4
# 选择2: Override its contents
# 选择3: package manager: pnpm
# 选择4: Initialize git repository: Yes
# 选择5: Dependencies isntalled: Yes、 Git repository initialized: Yes、 
# 选择6: official modules: No

# 2. 安装核心依赖
pnpm add @element-plus/nuxt @element-plus/icons-vue element-plus @pinia/nuxt pinia
pnpm add dayjs echarts vditor vue-router

# 3. 安装开发依赖
# pnpm add -D eslint eslint-plugin-vue prettier sass
pnpm add -D unplugin-auto-import unplugin-vue-components

# 4. 配置 Nuxt 特性
# 修改 nuxt.config.ts 添加 SSR 配置、路由规则、运行时配置等

# 5. 创建项目结构
mkdir -p pages components layouts composables stores server/api server/middleware plugins public
# 然后创建相应的文件和组件

# 6 修改 `nuxt.config.ts`:
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ['@element-plus/nuxt', '@pinia/nuxt'],
  runtimeConfig: {
    public: {
      apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:3001/api'
    }
  }
})
步骤 5:完善 docker-compose.yml 和 docker-compose.dev.yml

docker-compose.yml 和 docker-compose.dev.yml

yaml 复制代码
services:
  # Nuxt3 SSR 服务(首页)
  nuxt:
    dns:
      - 8.8.8.8
      - 1.1.1.1
    build:
      context: ..
      dockerfile: docker/nuxt/Dockerfile
      args:
        NUXT_PUBLIC_API_BASE_URL: ${NUXT_PUBLIC_API_BASE_URL:-http://backend:3001/api}
    container_name: blog-nuxt
    environment:
      NODE_ENV: production
      NUXT_PUBLIC_API_BASE_URL: ${NUXT_PUBLIC_API_BASE_URL:-http://backend:3001/api}
    ports:
      - "${NUXT_PORT:-3000}:3000"
    depends_on:
      backend:
        condition: service_healthy
    networks:
      - blog-network # 仅关联公共网络,不访问数据库
    healthcheck:
      # test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
      # 优化健康检查逻辑,增加错误捕获
      test: ["CMD", "node", "-e", "const http = require('http'); const req = http.get('http://localhost:3000', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on('error', () => { process.exit(1); });"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped

✅ 验证方式

bash 复制代码
cd packages/nuxt-ssr

# 安装依赖(如果未安装)
pnpm install

# 启动开发服务器
pnpm run dev

# 验证启动成功
# 应显示: Local: http://localhost:3000

# 浏览器访问 http://localhost:3000
# 应能看到 SSR 渲染的首页(查看源码应包含完整 HTML)

# 测试构建
pnpm run build
# 应成功构建生产版本

# 测试预览
pnpm run preview
# 应能预览生产构建

阶段十:Docker 化部署(可选)

🎯 本阶段目标

将整个应用容器化,实现生产环境部署。 Dockerfile 是多阶段构建的,有 builder 阶段 和 生产阶段, 这里主要是做 生产阶段 的工作.

注1: 为了生产阶段的工作顺利, 最好把项目根目录下的 .env.development 复制一份并命名为 .env, 同时 docker目录下的 docker-compose.dev.yml也是 复制一份并命名为 docker-compose.yml.

✅ 预期结果

  • 所有服务成功容器化并可独立运行
  • Nginx 反向代理正确配置路由
  • 容器间网络通信正常
  • 数据持久化配置生效
  • 生产环境配置正确加载

📏 验证标准

检查项 验证命令 期望结果
容器构建 pnpm run docker:build 所有镜像构建成功
服务启动 pnpm run docker:up 所有容器状态为 Up/Healthy
Nginx 代理 访问 http://localhost 正确路由到对应服务
API 访问 访问 http://localhost/api/health 返回完整的健康状态
前端访问 访问 http://localhost/admin 显示后台管理界面
SSR 前台 访问 http://localhost 显示 SSR 渲染页面
容器日志 pnpm run docker:logs 无错误日志
数据持久化 重启容器 数据正确保留

环境依赖清单

工具 版本要求 安装方式 验证命令
Docker ≥ 24.0.0 已安装 docker --version
Docker Compose ≥ 2.0.0 已安装 docker compose version
Node.js ≥ 18.0.0 已安装 node --version
pnpm ≥ 9.0.0 已安装 pnpm --version

📋 技术栈详情

  • 容器化: Docker + Docker Compose

📁 项目结构概览

bash 复制代码
docker/
├── docker-compose.yml     # 生产环境完整编排
├── docker-compose.dev.yml # 开发环境编排
├── backend/
│   └── Dockerfile         # 后端容器配置
├── frontend/
│   └── Dockerfile         # 前端容器配置
├── nuxt/
│   └── Dockerfile         # Nuxt 容器配置
└── nginx/
    └── nginx.conf         # Nginx 反向代理配置

当前项目状态分析

已有的资源:

  • ✅ 数据持久化配置已完整
  • ✅ 服务架构说明已完整
  • ✅ 部署流程说明已完整
  • ✅ 验证方式已完整

缺失的部分:

  • ❌ Dockerfile 配置
    • 后端服务容器配置: backend/Dockerfile
    • 前端后台管理容器配置:frontend/Dockerfile
    • Nuxt SSR 前台容器配置:nuxt/Dockerfile
    • Nginx 反向代理配置文件:nginx/nginx.conf
  • ❌ docker-compose.dev.yml 配置
  • ❌ 数据持久化配置
  • ❌ 服务架构说明
  • ❌ 部署流程说明
  • ❌ 验证方式说明

容器化服务架构

yaml 复制代码
┌─────────────────────────────────────────────────┐
│                 Nginx (Port 80)                 │
│  ┌─────────────────────────────────────────────┐ │
│  │  /api/* → Backend Service (Port 3001)      │ │
│  │                                             │ │
│  │  /* → Nuxt SSR Service (Port 3000)          │ │
│  │                                             │ │
│  │  /admin/* → Frontend SPA (Port 80)          │ │
│  └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
                    │          │          │
                    ▼          ▼          ▼
        ┌───────────┴───────────┴───────────┴───────────┐
        │                                               │
        │           Docker Network (blog-network)       │
        │                                               │
        │  ┌─────────────┐  ┌─────────────┐             │
        │  │   MySQL     │  │    Redis    │             │
        │  │ (Port 3306) │  │ (Port 6379)│             │
        │  └─────────────┘  └─────────────┘             │
        └───────────────────────────────────────────────┘

🧭 操作步骤

步骤 1:主要功能模块
  • 多服务架构: 后端 API、前端管理后台、Nuxt SSR 前台、Nginx 反向代理
  • 数据持久化: MySQL 数据库和 Redis 缓存的卷挂载配置
  • 网络通信: Docker 内部网络配置,确保服务间安全通信
  • 反向代理: Nginx 配置实现路径路由和负载均衡
  • 环境隔离: 生产环境和开发环境的配置分离
  • 日志管理: 容器日志收集和监控
  • 健康检查: 服务状态监控和自动重启机制
  • 安全配置: 非 root 用户运行、端口映射、安全加固
步骤 2:从零开始创建

Docker 服务器配置:

2.1 后端 Dockerfile (docker/backend/Dockerfile)

创建: touch docker/backend/Dockerfile:

dockerfile 复制代码
# 基础镜像选择
#   使用 Node.js LTS 版本的 Alpine Linux 镜像
#     Alpine 是一个轻量级 Linux 发行版,镜像体积小,安全性高
#     lts 表示长期支持版本,稳定性更好
FROM node:lts-alpine

# 设置工作目录: 在容器内创建并切换到 /app 目录, 后续所有命令(如 COPY、RUN)都会在这个目录下执行
WORKDIR /app

# 安装系统依赖: 安装 curl 工具,用于健康检查, --no-cache 参数:不缓存包索引,减小镜像体积
# 在 Alpine 镜像中(node:lts-alpine) 安装 curl 工具.
RUN apk add --no-cache curl

# 安装包管理器: 全局安装 pnpm(Performant Node Package Manager), pnpm 比 npm/yarn 更快,磁盘占用更少,支持 workspace 功能
RUN npm install -g pnpm@10.24.0

# 复制 workspace 配置文件(依赖安装优化层)
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

# 创建 monorepo 所需的目录结构: packages/common: 共享代码模块, packages/backend: 后端服务代码
RUN mkdir -p packages/common packages/backend

# 复制共享模块的依赖文件: 先复制 common 模块的 package.json,用于依赖解析, 这样可以正确安装 workspace 内部的依赖关系
COPY packages/common/package.json ./packages/common/

# 复制后端模块的依赖文件: package*.json 通配符匹配 package.json 和 package-lock.json
COPY packages/backend/package*.json ./packages/backend/

# 安装生产环境依赖
RUN pnpm install --frozen-lockfile --only=production


# 复制源代码: 在依赖安装完成后再复制源代码, 这样代码变更不会影响依赖层的缓存,提高构建效率
# 复制: 后端服务源代码 + 共享模块源代码
COPY packages/backend/ ./packages/backend/
COPY packages/common/ ./packages/common/

# 切换到后端服务目录: 将工作目录切换到后端服务目录, 后续命令(如 CMD)将在此目录下执行
WORKDIR /app/packages/backend

# 创建日志目录: 创建日志文件存储目录, 确保应用运行时可以正常写入日志文件
RUN mkdir -p logs

# 暴露端口: 声明容器运行时监听的端口3001(后端 API 服务端口)
EXPOSE 3001

# ----------------------------------------------------------------------------
# 健康检查配置
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:3001/api/health || exit 1

# 启动命令 在 /app/packages/backend
CMD ["node", "app.js"]
2.2 前端 Dockerfile (docker/frontend/Dockerfile)

创建: touch docker/frontend/Dockerfile:

dockerfile 复制代码
# 第一阶段:构建阶段(Builder Stage): 此阶段负责安装依赖、编译源代码,生成静态资源文件

# 构建阶段基础镜像
FROM node:lts-alpine AS builder

# 设置构建工作目录: 在容器内创建并切换到 /app 目录
WORKDIR /app

# 安装包管理器
# 全局安装 pnpm(Performant Node Package Manager), pnpm 比 npm/yarn 更快,磁盘占用更少,支持 workspace 功能
# -p 参数:如果目录已存在则不报错,支持递归创建
RUN npm install -g pnpm@10.24.0

# 复制 workspace 配置文件(依赖安装优化层)
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

# 创建 packages 目录结构
# 创建 monorepo 所需的目录结构 (单体仓库, 无论是前端应用、后端服务、公共组件库、工具函数,还是文档,都可以放在同一个仓库里管理)
RUN mkdir -p packages/common packages/frontend

# 复制共享模块的依赖文件
COPY packages/common/package.json ./packages/common/

# 复制前端模块的依赖文件
COPY packages/frontend/package.json ./packages/frontend/

# 安装所有依赖(包括开发依赖)
RUN pnpm install --frozen-lockfile

# 复制源代码
COPY packages/frontend/ ./packages/frontend/
# 复制共享模块源代码
COPY packages/common/ ./packages/common/

# 构建前端应用
# 切换到前端应用目录
WORKDIR /app/packages/frontend

# 执行构建命令,生成静态资源文件到 dist 目录
# 构建产物包括:HTML、CSS、JavaScript、图片等静态资源
RUN pnpm run build

# 第二阶段:生产阶段(Production Stage)

# 生产阶段基础镜像
FROM nginx:alpine

# 复制构建产物
COPY --from=builder /app/packages/frontend/dist /usr/share/nginx/html

# 复制 Nginx 配置文件
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf

# 暴露端口
EXPOSE 80

# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
2.3 Nuxt SSR Dockerfile (docker/nuxt/Dockerfile)

创建: touch docker/nuxt/Dockerfile:

dockerfile 复制代码
# 第一阶段:构建阶段(Builder Stage): 此阶段负责安装依赖、编译源代码,生成 Nuxt3 构建产物
FROM node:lts-alpine AS builder

# 设置构建工作目录: 在容器内创建并切换到 /app 目录
WORKDIR /app

# 安装包管理器: 全局安装 pnpm(Performant Node Package Manager), pnpm 比 npm/yarn 更快,磁盘占用更少,支持 workspace 功能
RUN npm install -g pnpm@10.24.0

# 复制 workspace 配置文件(依赖安装优化层): 先复制依赖管理文件(package.json、pnpm-lock.yaml、pnpm-workspace.yaml),利用 Docker 层缓存机制
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

# 创建 monorepo 所需的目录结构: packages/common: 共享代码模块, packages/nuxt-ssr: Nuxt3 项目目录
# -p 参数:如果目录已存在则不报错,支持递归创建
RUN mkdir -p packages/common packages/nuxt-ssr

# 复制共享模块的依赖文件: 先复制 common 模块的 package.json,用于依赖解析, 这样可以正确安装 workspace 内部的依赖关系
COPY packages/common/package.json ./packages/common/

# 复制 Nuxt 项目依赖文件: 复制 packages/nuxt-ssr 的 package.json
COPY packages/nuxt-ssr/package.json ./packages/nuxt-ssr/

# 安装所有依赖(包括开发依赖): --frozen-lockfile 严格使用 pnpm-lock.yaml,不更新锁文件, 确保构建的一致性和可重现性
RUN pnpm install --frozen-lockfile

# 复制源代码: 在依赖安装完成后再复制源代码, 这样代码变更不会影响依赖层的缓存,提高构建效率
# 复制: Nuxt3 项目源代码 + 共享模块源代码
COPY packages/nuxt-ssr/ ./packages/nuxt-ssr/
COPY packages/common/ ./packages/common/

# 切换到 Nuxt 项目目录: 将工作目录切换到 Nuxt 项目目录, 后续构建命令将在此目录下执行
WORKDIR /app/packages/nuxt-ssr

# 设置构建时环境变量: ARG 用于构建时传入参数, ENV 设置运行时环境变量
ARG NUXT_PUBLIC_API_BASE_URL
ENV NUXT_PUBLIC_API_BASE_URL=${NUXT_PUBLIC_API_BASE_URL:-http://backend:3001/api}

# 构建 Nuxt3 应用: 执行构建命令,生成 SSR 应用构建产物到 .output 目录
#   构建产物包括: 服务端代码(.output/server)、客户端代码(.output/public)、路由配置等
RUN pnpm run build


# 第二阶段:生产阶段(Production Stage): 此阶段只包含运行所需的文件,使用轻量级的 Node.js 镜像

# 生产阶段基础镜像: 使用 Node.js LTS 版本的 Alpine Linux 镜像作为运行环境
# ✅ 重新开始的好处:干净的基础镜像, 这是 Docker 多阶段构建的常见做法
FROM node:lts-alpine AS production
# FROM builder  # ❌ 不推荐

# 设置构建时环境变量(在生产阶段也需要定义这个 ARG)
ARG NUXT_PUBLIC_API_BASE_URL

# 设置工作目录: 在容器内创建并切换到 /app 目录. 注: 与上面的不是同一个目录, 各自在不同的 镜像中创建的 `/app` 目录
WORKDIR /app

# 安装包管理器: 全局安装 pnpm, 用于安装生产依赖
RUN npm install -g pnpm@10.24.0

# 复制 workspace 配置文件(依赖安装优化层): 先复制依赖管理文件,利用 Docker 层缓存机制
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

# 创建 monorepo 所需的目录结构: packages/common: 共享代码模块, packages/nuxt-ssr: Nuxt3 项目目录
RUN mkdir -p packages/common packages/nuxt-ssr

# 复制共享模块的依赖文件: 先复制 common 模块的 package.json,用于依赖解析
COPY packages/common/package.json ./packages/common/

# 复制 Nuxt 项目依赖文件: 复制 packages/nuxt-ssr 的 package.json
COPY packages/nuxt-ssr/package.json ./packages/nuxt-ssr/

# 安装生产环境依赖: --frozen-lockfile 严格使用 pnpm-lock.yaml,不更新锁文件, 确保构建的一致性和可重现性
RUN pnpm install --frozen-lockfile --only=production

# 从构建阶段复制构建产物: 从 builder 阶段复制 Nuxt3 构建好的 .output 目录
COPY --from=builder /app/packages/nuxt-ssr/.output ./packages/nuxt-ssr/.output

# 复制必要的配置文件: 复制 Nuxt 配置文件和 package.json(运行时可能需要)
COPY packages/nuxt-ssr/nuxt.config.ts ./packages/nuxt-ssr/
COPY packages/nuxt-ssr/package.json ./packages/nuxt-ssr/

# 复制共享模块源代码: 复制共享模块源代码(如果 Nuxt 应用运行时需要)
COPY packages/common/ ./packages/common/

# 切换到 Nuxt 项目目录: 将工作目录切换到 Nuxt 项目目录, 后续命令(如 CMD)将在此目录下执行
WORKDIR /app/packages/nuxt-ssr

# 设置运行时环境变量
ENV NODE_ENV=production
ENV NUXT_PUBLIC_API_BASE_URL=${NUXT_PUBLIC_API_BASE_URL:-http://backend:3001/api}

# 暴露端口: 声明容器运行时监听的端口3000(Nuxt3 SSR 服务端口)
EXPOSE 3000

# 健康检查配置: 配置容器健康检查机制
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

# 启动命令 在 /app/packages/nuxt-ssr
CMD ["node", ".output/server/index.mjs"]
2.4 Nginx 反向代理配置文件 (docker/nginx/nginx.conf)

创建: touch docker/nginx/nginx.conf:

conf 复制代码
# 运行 Nginx 的用户和组, 使用 nginx 用户运行,提高安全性(非 root)
user nginx;

# Worker 进程数, auto: 自动设置为 CPU 核心数,充分利用多核性能
worker_processes auto;

# 错误日志配置
#   /var/log/nginx/error.log: 错误日志文件路径
#   warn: 只记录警告及以上级别的日志
error_log /var/log/nginx/error.log warn;

# PID 文件路径, 存储主进程 ID,用于进程管理
pid /var/run/nginx.pid;

# 事件模块配置
events {
  worker_connections 1024;
}

# HTTP 模块配置
http {
  # MIME 类型配置, 包含所有标准的 MIME 类型映射(如 text/html, application/json 等)
  include /etc/nginx/mime.types;
  
  # 默认 MIME 类型, 当无法识别文件类型时,使用 application/octet-stream(二进制流)
  default_type application/octet-stream;

  # 日志格式定义
  log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';

  # 访问日志配置, 使用上面定义的 main 格式记录所有访问日志
  access_log /var/log/nginx/access.log main;

  # 性能优化配置
  
  # 启用 sendfile
  sendfile on;
  
  # TCP_NOPUSH 选项, 与 sendfile 配合使用,等待数据包填满后再发送,减少网络包数量
  tcp_nopush on;
  
  # TCP_NODELAY 选项, 禁用 Nagle 算法,立即发送小数据包,降低延迟, 适用于需要低延迟的场景(如 API 请求)
  tcp_nodelay on;
  
  # Keep-Alive 超时时间, 65 秒:保持连接打开的时间,减少连接建立开销
  keepalive_timeout 65;
  
  # 类型哈希表最大大小, 2048: 提高 MIME 类型查找效率
  types_hash_max_size 2048;
  
  # 客户端请求体最大大小
  #   20M: 允许上传最大 20MB 的文件(如文章中的图片)
  client_max_body_size 20M;

  # 启用 Gzip 压缩
  gzip on;
  
  # 添加 Vary: Accept-Encoding 响应头, 告诉缓存服务器根据 Accept-Encoding 头缓存不同版本
  gzip_vary on;
  
  # 代理请求的压缩策略, any: 对所有代理请求启用压缩(包括已压缩的响应)
  gzip_proxied any;
  
  # 压缩级别: 6: 平衡压缩率和 CPU 消耗(1-9,9 压缩率最高但 CPU 消耗最大)
  gzip_comp_level 6;
  
  # 压缩的文件类型, 只压缩文本类文件,图片和视频等二进制文件通常已经压缩过
  gzip_types text/plain text/css text/xml text/javascript 
  application/json application/javascript application/xml+rss 
  application/rss+xml font/truetype font/opentype 
  application/vnd.ms-fontobject image/svg+xml;
  
  # 禁用 Gzip 的浏览器, msie6: 禁用 IE6 的 Gzip(IE6 对 Gzip 支持有问题)
  gzip_disable "msie6";
  
  # 最小压缩长度, 1000: 只压缩大于 1000 字节的文件,小文件压缩可能反而增加体积
  gzip_min_length 1000;

  # 代理缓存配置
  # 配置 API 响应的缓存存储
  proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=100m inactive=60m use_temp_path=off;

  # 上游服务器配置(Upstream)
  
  # 后端 API 服务配置
  upstream backend {
    # 后端服务地址
    #   backend:3001: Docker Compose 中的服务名,端口 3001
    #   使用服务名而非 IP,由 Docker 网络 DNS 解析
    server backend:3001;
    
    # Keep-Alive 连接数
    #   32: 保持 32 个空闲连接,减少连接建立开销
    keepalive 32;
  }

  # Nuxt3 SSR 服务配置
  upstream nuxt {
      # Nuxt3 SSR 服务地址
    #   nuxt:3000: Docker Compose 中的服务名,端口 3000
    server nuxt:3000;
    
    # Keep-Alive 连接数
    keepalive 32;
  }

  # HTTP 服务器配置
  server {
    # 监听端口, 80: HTTP 标准端口
    listen 80;
    
    # 服务器名称, localhost: 本地访问,生产环境应改为实际域名
    server_name localhost;
    
    # 网站根目录
    root /usr/share/nginx/html;
    
    # 默认首页文件, 确保 index.html 可以被正确访问
    index index.html;

    # 内容安全策略(CSP)头
    # 防止 XSS 攻击,限制资源加载来源, 适配 ECharts noeval 版本,无 unsafe-eval,提升安全性
    add_header Content-Security-Policy "
      default-src 'self';                                    # 默认只允许同源资源
      script-src 'self';                                     # 脚本来源(不允许内联和 eval,使用 ECharts noeval 版本)
      style-src 'self' 'unsafe-inline';                      # 样式来源(允许内联样式,ECharts 样式需要)
      img-src 'self' data: blob:;                            # 图片来源(允许 data URI 和 blob,ECharts 图表导出需要)
      font-src 'self' data:;                                  # 字体来源(允许 data URI)
      connect-src 'self';                                     # 连接来源(API 请求)
      object-src 'none';                                      # 禁止插件
      base-uri 'self';                                        # 限制 base 标签的 URL
      frame-src 'none';                                       # 禁止 iframe
    " always;

    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    location /api {
      proxy_pass http://backend;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection 'upgrade';
      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;
      proxy_cache_bypass $http_upgrade;
      proxy_cache api_cache;
      proxy_cache_valid 200 60m;
      proxy_cache_valid 404 1m;
      proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
      proxy_cache_background_update on;
      proxy_cache_lock on;
      proxy_cache_methods GET HEAD;
      proxy_no_cache $request_method;
    }

    # Nuxt3 静态资源代理
    # 代理 Nuxt3 的静态资源(CSS、JS 等),^~ : 前缀匹配,优先级高于正则匹配,阻止后续正则匹配
    location ^~ /_nuxt/ {
      # 代理到 Nuxt3 服务
      proxy_pass http://nuxt;
      
      # HTTP 版本和请求头
      proxy_http_version 1.1;
      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;
      
      # 静态资源长期缓存: 1y: 缓存 1 年, immutable: 标记为不可变资源,浏览器可永久缓存
      expires 1y;
      add_header Cache-Control "public, immutable";
    }

    # Vue3 SPA 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
      # 长期缓存
      expires 1y;
      add_header Cache-Control "public, immutable";
      
      # 关闭访问日志
      #   静态资源访问量大,不记录日志减少 I/O
      access_log off;
    }

    # Nuxt3 SSR 首页
    location = / {
      # 代理到 Nuxt3 SSR 服务
      proxy_pass http://nuxt;
      
      # HTTP 版本和 WebSocket 支持
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection 'upgrade';
      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;
      proxy_cache_bypass $http_upgrade;
      
      add_header Cache-Control "no-cache, no-store, must-revalidate";
    }

    location / { 
      add_header Cache-Control "no-cache, no-store, must-revalidate";
    }
    
    # 添加了 location = /index.html:确保回退到 /index.html 时能正确访问
    #   当 try_files 回退到 /index.html 时,这个 location 确保文件能被正确返回
    location = /index.html {
      root /usr/share/nginx/html;
      add_header Cache-Control "no-cache, no-store, must-revalidate";
    }

    location /health {
      # 不记录访问日志
      access_log off;
      
      # 直接返回 200 状态码和 "healthy" 文本
      return 200 "healthy\n";
      
      # 设置响应类型
      add_header Content-Type text/plain;
    }

    # 5xx 错误:返回 50x.html 错误页面
    error_page 500 502 503 504 /50x.html;
    
    # 50x.html 文件位置
    location = /50x.html {
      root /usr/share/nginx/html;
    }
  }
}
2.5 添加 docker-compose.yml

创建: touch docker/docker-compose.yml:

yaml 复制代码
  services:
  mysql:
    image: mysql:8.0
    container_name: blog-test-mysql2
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-blog123}
      MYSQL_DATABASE: ${DB_NAME:-blog_db}
      MYSQL_USER: ${DB_USER:-blog_root}
      MYSQL_PASSWORD: ${DB_PASSWORD:-blog123}
    # 👇 dev模式, 临时暴露端口用于本地开发
    ports:
      - "${DB_PORT:-3306}:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    command: --default-authentication-plugin=caching_sha2_password # 设置默认 认证插件为 caching_sha2_password
    networks:
      - blog-network
      - db-network  # 关联db-network,实现网络隔离
    healthcheck:
      # test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD:-blog123}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:8.4.0
    # image: redis:7.2-alpine # 修正Redis镜像版本为有效稳定版
    container_name: blog-test-redis2
    environment:
      - REDIS_PASSWORD=${REDIS_PASSWORD:-redis123} # 显式定义环境变量,避免 command 中解析失败
    ports:
      - "${REDIS_PORT:-6379}:6379"  # 暴露端口供外部访问
    volumes:
      - redis_data:/data
    networks:
      - blog-network
      - db-network  # 关联db-network,实现网络隔离
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis123}
    healthcheck:
      # 改用 --password 参数,兼容 Alpine 版本
      # test: ["CMD", "redis-cli", "--password", "$REDIS_PASSWORD", "ping"]
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis123}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # 后端服务
  backend:
    build:
      context: ..
      dockerfile: docker/backend/Dockerfile
    container_name: blog-backend
    environment:
      NODE_ENV: production
      PORT: 3001
      DB_HOST: mysql
      DB_PORT: 3306
      DB_NAME: ${DB_NAME:-blog_db}
      DB_USER: ${DB_USER:-blog_user}
      DB_PASSWORD: ${DB_PASSWORD:-blog123}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: ${REDIS_PASSWORD:-redis123}
      JWT_SECRET: ${JWT_SECRET:-your_jwt_secret_key_change_in_production}
      JWT_EXPIRES_IN: 7d
      CORS_ORIGIN: http://localhost
    ports:
      - "${BACKEND_PORT:-3001}:3001"
    volumes:
      - backend_logs:/app/logs
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - blog-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped

  # Nuxt3 SSR 服务(首页)
  nuxt:
    dns:
      - 8.8.8.8
      - 1.1.1.1
    build:
      context: ..
      dockerfile: docker/nuxt/Dockerfile
      args:
        NUXT_PUBLIC_API_BASE_URL: ${NUXT_PUBLIC_API_BASE_URL:-http://backend:3001/api}
    container_name: blog-nuxt
    environment:
      NODE_ENV: production
      NUXT_PUBLIC_API_BASE_URL: ${NUXT_PUBLIC_API_BASE_URL:-http://backend:3001/api}
    ports:
      - "${NUXT_PORT:-3000}:3000"
    depends_on:
      backend:
        condition: service_healthy
    networks:
      - blog-network # 仅关联公共网络,不访问数据库
    healthcheck:
      # test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
      # 优化健康检查逻辑,增加错误捕获
      test: ["CMD", "node", "-e", "const http = require('http'); const req = http.get('http://localhost:3000', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on('error', () => { process.exit(1); });"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped

  # 前端服务(Vue3 SPA,其他页面)
  frontend:
    build:
      context: ..
      dockerfile: docker/frontend/Dockerfile
    container_name: blog-frontend
    ports:
      - "${FRONTEND_PORT:-80}:80"
    depends_on:
      - backend
    networks:
      - blog-network # 仅关联公共网络,不访问数据库
    restart: unless-stopped

# 顶级 volumes 定义, 优点:数据不会随容器删除; 更安全、可复用;
volumes:
  mysql_data:
  redis_data:
  backend_logs:

networks:
  blog-network:
    driver: bridge
  db-network: # 仅数据库/backend 可访问
    driver: bridge

注: 创建完成后, 使用 docker compose config 来检查配置语法.

2.6 修改根目录中的packages.json, 添加 build 相关的 脚本命令:
json 复制代码
{
  "name": "blog-ssr",
  "version": "1.0.0",
  "description": "全栈博客系统,支持 SSR 和后台管理",
  "private": true,
  "packageManager": "pnpm@9.0.0",
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "docker-dev:up:redis": "cd docker && docker compose -f docker-compose.dev.yml --env-file ../.env.development up -d redis",
    "docker-dev:up:mysql": "cd docker && docker compose -f compose docker-compose.dev.yml --env-file ../.env.development up -d mysql",
    "docker-dev:up:nuxt": "cd docker && docker compose -f compose docker-compose.dev.yml --env-file ../.env.development up -d nuxt",
    "docker-dev:up:frontend": "cd docker && docker compose -f compose docker-compose.dev.yml --env-file ../.env.development up -d frontend",
    "docker-dev:up:backend": "cd docker && docker compose -f compose docker-compose.dev.yml --env-file ../.env.development up -d backend",
    
    "docker-dev:stop": "cd docker && docker compose -f compose docker-compose.dev.yml --env-file ../.env.development stop",
    "docker-dev:stop:frontend": "cd docker && docker compose -f compose docker-compose.dev.yml --env-file ../.env.development stop frontend",
    "docker-dev:stop:backend": "cd docker && docker compose -f compose docker-compose.dev.yml --env-file ../.env.development stop backend",
    "docker-dev:stop:nuxt": "cd docker && docker compose -f compose docker-compose.dev.yml --env-file ../.env.development stop nuxt",

    "dev:frontend": "pnpm run docker-dev:stop:frontend || true && cd packages/frontend && pnpm run dev",
    "dev:backend": "pnpm run docker-dev:stop:backend || true && cd packages/backend && pnpm run dev",
    "dev:nuxt": "pnpm run docker-dev:stop:nuxt || true && cd packages/nuxt-ssr && pnpm run dev",
    "kill:frontend": "kill-port 5173 --force",
    "kill:backend": "kill-port 3001 --force",

    "docker:logs": "cd docker && docker compose logs -f",

    "docker:build": "pnpm run docker:down && cd docker && docker compose up -d --build",
    "backend:init-db": "cd packages/backend && npm run init-db",

    "dev": "pnpm run docker-dev:up:redis && pnpm run docker-dev:up:mysql && pnpm docker-dev:stop && concurrently \"pnpm run dev:frontend\" \"pnpm run dev:backend\" \"pnpm run dev:nuxt\"",
    
    "build": "pnpm run docker:build && pnpm run backend:init-db",

    "docker:up": "pnpm run docker:down && cd docker && docker compose up -d",
    "docker:down": "cd docker && docker compose down"
  },
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=6.11.0"
  },
  "author": "henrypt",
  "license": "MIT",
  "dependencies": {
    "markdown-it": "^14.1.0"
  },
  "devDependencies": {
    "eslint": "^9.39.2",
    "husky": "^9.1.7",
    "lint-staged": "^16.2.7",
    "prettier": "^3.7.4",
    "sass": "^1.97.2",
    "concurrently": "^9.2.1",
    "kill-port": "^2.0.1"
  }
}
2.7 全局依赖安装
bash 复制代码
pnpm install
命令 构建镜像 启动服务 开发模式 适用环境
pnpm run dev ✅ (本地) 热重载开发 本地开发
pnpm run docker:up ✅ (容器) 生产模式 已部署环境
pnpm run docker:build ✅ (容器) 生产模式 首次部署/代码更新

pnpm run dev 用于 开发 & 测试 阶段 pnpm run docker:uppnpm run docker:build 前, 建议先执行 docker compose down

使用建议

  • 开发时:pnpm run dev
  • 部署新版本:pnpm run docker:build
  • 启动现有部署:pnpm run docker:up / pnpm run docker:down
2.8 添加 .env

项目根目录 创建: touch .env:

bash 复制代码
# 后端服务配置
NODE_ENV=production
HOST=0.0.0.0
PORT=3001

# 数据库配置(Docker 容器)
DB_HOST=localhost
DB_PORT=3306
DB_NAME=blog_db
DB_USER=blog_root
DB_PASSWORD=blog123

# Redis 配置(Docker 容器)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=redis123

# JWT 配置
JWT_SECRET=dev_jwt_secret_key_for_development_only
JWT_EXPIRES_IN=7d

# CORS 配置(允许前端访问)
CORS_ORIGIN=http://localhost

# Nuxt SSR 配置
NUXT_PUBLIC_API_BASE_URL=http://localhost:3001/api
NUXT_PUBLIC_SITE_BASE=http://localhost:3000
NUXT_PUBLIC_SITE_NAME=个人博客

# Docker 服务端口配置
BACKEND_PORT=3001
FRONTEND_PORT=80
NUXT_PORT=3000
注意事项:
  • 环境变量优先级:environment 中的环境变量会覆盖 Dockerfile 中设置的环境变量
  • 构建参数:build.args 中的参数只在构建时有效,不会作为环境变量传递到运行的容器中
  • 多阶段构建:当前的 Nuxt Dockerfile 已经是多阶段构建,包含了构建阶段和生产阶段,这是最佳实践
步骤 4:部署流程
bash 复制代码
# 1. 进入docker目录, 检查docker配置语法的正确性:
`docker-compose config`

# 2. 构建并启动所有服务
# 返回项目根目录
pnpm run build

# 注: build 脚本会先停止现有容器,构建新镜像,启动服务,然后初始化数据库
# 注: build 里集成了docker的build + init-db(数据库初始化工作)

# 查看服务状态
docker compose ps

# 查看服务日志
pnpm run docker:logs

✅ 验证方式

bash 复制代码
# 1. 进入docker目录, 检查docker配置语法的正确性:
`docker-compose config`

# 2. 检查服务状态
docker compose ps
# 应显示所有服务状态为 Up/Healthy

# 3. 测试健康检查
curl http://localhost/api/health | jq
# 应返回完整的健康状态信息,包含数据库、Redis、内存等状态

# 4. 测试前台首页
curl http://localhost/
# 应返回完整的 HTML 页面(SSR)

# 5. 测试后台管理
curl http://localhost/admin/
# 应返回前端应用

# 6. 查看服务日志
pnpm run docker:logs:backend
pnpm run docker:logs:nuxt

# 7. 检查容器资源使用
docker stats

# 8. 服务健康检查
```bash
curl http://localhost:3001/api/health

# 9. 登录检查
curl -v -X POST http://localhost:3001/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'

✅ 补充排查点

bash 复制代码
# 1. 网络 / 权限问题:db-network 网络是否创建成功?Redis 容器是否有权限写入 redis_data 卷?
  # 检查网络
  docker network ls | grep db-network
  # 检查卷权限
  docker volume inspect redis_data

# 2. 端口冲突:宿主机 6379 端口是否被其他进程占用?
  # 运行
  lsof -i :6379  # Mac/Linux
  netstat -ano | findstr :6379  # Windows

相关推荐
C_心欲无痕18 小时前
ts - 交叉类型
前端·git·typescript
全栈前端老曹18 小时前
【前端路由】React Router 权限路由控制 - 登录验证、私有路由封装、高阶组件实现路由守卫
前端·javascript·react.js·前端框架·react-router·前端路由·权限路由
zhuà!18 小时前
uv-picker在页面初始化时,设置初始值无效
前端·javascript·uv
Amumu1213818 小时前
React应用
前端·react.js·前端框架
摸鱼的春哥18 小时前
实战:在 Docker (Windows) 中构建集成 yt-dlp 的“满血版” n8n 自动化工作流
前端·javascript·后端
小酒星小杜18 小时前
在AI时代,技术人应该每天都要花两小时来构建一个自身的构建系统
前端·vue.js·架构
测试游记18 小时前
基于 FastGPT 的 LangChain.js + RAG 系统实现
开发语言·前端·javascript·langchain·ecmascript
阿奇__18 小时前
elementUI table 多列排序并保持状态样式显示正确(无需修改源码)
前端·vue.js·elementui
zhengxianyi51518 小时前
数据大屏-单点登录ruoyi-vue-pro
前端·javascript·vue.js