JY 项目一体化手册 Engineering Handbook
JY Engineering

开发、架构、部署与配置,一份手册讲清楚

面向维护者的单文件项目手册:从本地启动、共享 MySQL、静态预渲染,到管理端发布、 GitHub Actions 与 Docker/Nginx 生产落地。

仓库事实已核对 零外部依赖 可离线阅读 生产方案待落地
当前结论

核心发布闭环已经可用,但网站与管理端还不能称为生产完备。 jy-site 的静态生成、搜索、RSS 与主题链路基本打通;jy-admin 可完成登录、文章编辑、分类标签和发布触发。容器化、管理端自动部署、TLS、备份恢复、 回滚、完整 CI、细粒度权限与若干业务能力仍需补齐。

01项目总览

JY 是两个独立 Node 项目共处一个代码目录的博客系统。它们共享 MySQL blog_site,但承担完全不同的生产职责:

Public site jy-site

SvelteKit 5 在开发时实时查库,生产构建时把内容烘焙成纯静态文件,由 Nginx 托管。

Admin service jy-admin

Next.js 15 常驻 Node 服务,负责认证、内容写入、站点设置和触发公开站重新构建。

当前能力分级

内容链路 可用

已实现
写作、发布、预渲染与线上更新主路径存在。

公开站 基本完整

部分实现
阅读体验完整,404、sitemap、PWA 缓存等仍有缺口。

生产运维 未工程化

生产阻塞
容器、TLS、备份、健康检查和自动回滚均未入库。

当前实现:数据库驱动的静态发布链 实线 = 已实现链路
jy-admin Next.js · :5174 · 读写 MySQL blog_site 共享内容与站点设置 jy-site 构建 SvelteKit prerender 写入内容 构建时读取 GitHub Actions repository_dispatch Nginx 静态目录 build/ · rsync --delete 手动 / 自动触发 产出 build/
必须理解的静态边界

生产环境的 jy-site 没有 Node 服务,也不会实时查询 MySQL。数据库内容变更只有经过 build:static 和部署后才会上线;/api/search 仅用于开发态,生产搜索读取构建生成的 JSON。

02快速开始

前置环境

组件 仓库约定 说明
Node.js 22.22.1 根目录 .nvmrc 与 jy-admin engines 一致。
pnpm 10.15.0 两个项目各自维护 lockfile,没有根 workspace。
MySQL Schema 源自 5.7.44 升级到 MySQL 8.x 前应在预发布环境完成导入与查询验证。

1. 初始化数据库

shell · 仓库根目录
mysql -u root -p < jy-site/blog_site.sql
mysql -u root -p blog_site < jy-site/migrations/001_site_deploy_settings.sql
mysql -u root -p blog_site < jy-site/migrations/002_auto_publish_setting.sql
初始化后仍不能直接登录

SQL dump 中的用户密码哈希是占位值,不是有效 bcrypt 凭证。首次启动前必须生成真实哈希并更新 users.password_hash;不要把明文密码写入 SQL 或 Git。

2. 配置两个应用

shell · 复制模板
cp jy-site/.env.example jy-site/.env
cp jy-admin/.env.example jy-admin/.env

# 编辑两个 .env:
# - DATABASE_* 指向同一个 blog_site
# - jy-admin 的 SESSION_SECRET 至少 32 字符
# - 需要发布按钮时再配置 GITHUB_REPOSITORY / GITHUB_TOKEN

3. 安装并启动

终端 A · 公开站

shell
cd jy-site
pnpm install
pnpm dev

默认 http://localhost:5173;端口占用时 Vite 会自动顺延。

终端 B · 管理端

shell
cd jy-admin
pnpm install
pnpm dev

固定 http://localhost:5174。

常用质量命令

项目 命令 用途
jy-site pnpm check SvelteKit sync + svelte-check。
jy-site pnpm lint Prettier 检查 + ESLint。
jy-site pnpm build:static 完整静态构建;不仅是 pnpm build。
jy-admin pnpm check TypeScript --noEmit。
jy-admin pnpm verify 类型检查 + Next.js 生产构建。
jy-admin pnpm dev:reset 清理 .next 后重启,处理 HMR/Server Action 缓存异常。

03仓库与技术栈

“Monorepo” 的准确含义

这里是双应用同仓结构,但根目录没有 package.json 或 pnpm-workspace.yaml。依赖安装、命令和 lockfile 都在两个子项目内独立管理。

目录地图
jy_prj/
├── .github/workflows/
│   ├── ci-jy-admin.yml            # 管理端 TypeScript CI
│   └── deploy-jy-site.yml         # 静态站构建与发布
├── jy-site/
│   ├── src/routes/                # SvelteKit 页面与端点
│   ├── src/lib/                   # 组件、Markdown、主题、数据库读取
│   ├── scripts/                   # 搜索索引、sitemap、构建规范化
│   ├── migrations/                # 手工 SQL 增量
│   ├── static/                    # PWA、图片、搜索索引等
│   ├── blog_site.sql              # 全量 schema + seed
│   └── nginx.conf                 # 当前 HTTP 静态站参考配置
├── jy-admin/
│   ├── app/                       # Next.js App Router
│   ├── components/                # Ant Design 管理组件
│   └── lib/                       # Server Actions、DB、会话、GitHub API
└── README.md                      # 当前高层说明

核心栈

层 技术 职责
公开站 Svelte 5 · SvelteKit 2 · Vite 7 开发态 SSR 与生产静态预渲染。
管理端 Next.js 15.5.14 · React 19.2.4 · Ant Design 5 动态管理后台与发布控制面。
内容渲染 markdown-it · Shiki Markdown、VitePress 容器、双主题代码高亮。
数据层 MySQL · mysql2/promise 无 ORM;两端均使用手写 SQL。
认证 iron-session · bcryptjs 加密 Cookie 会话与密码校验。
发布 GitHub Actions · rsync · Nginx 当前静态站构建与同步。

工作区注意

本次核对时,当前目录未被 git status 识别为 Git worktree。GitHub Actions、 repository_dispatch 与推荐的自动化流程都以“代码已经位于真实 GitHub 仓库”为前提。

04公开站 jy-site

双运行模式

开发模式

  • Vite dev server 默认监听 0.0.0.0:5173。
  • 页面 Server Load 实时读取 MySQL。
  • 搜索调用 GET /api/search?q=...。
  • 更新数据库后刷新页面即可看到内容。

生产模式

  • adapter-static 输出到 jy-site/build/。
  • 所有文章、分类、标签和 RSS 在构建时生成。
  • 搜索读取 /search-index.json。
  • Nginx 只提供静态文件,不连接数据库。

路由清单

路由 职责 数据与预渲染
/ 首页与最新文章 构建时读取最近文章。
/about 建设初衷 静态内容,参与预渲染。
/creed 生活信条 静态页面。
/life 生命刻度 静态页面 + 客户端计时。
/blog 文章列表与分类胶囊 文章、分类和分页设置。
/blog/page/[n] 列表分页 entries() 枚举可用页。
/blog/[slug] 文章详情、目录、上下篇 构建时 Markdown → HTML。
/blog/category/[slug] 分类文章 构建时枚举分类。
/blog/tag/[slug] 标签文章 构建时枚举标签。
/rss.xml 最近 30 篇 RSS 2.0 静态 XML。
/api/search 开发态数据库搜索 纯静态生产环境不可用,属于设计边界。

完整静态构建

  1. 生成搜索索引

    generate-search-index.ts 查询已发布文章并写入 static JSON。

  2. 清理并运行 SvelteKit build

    读取 MySQL,预渲染页面、数据依赖与 RSS。

  3. 生成 sitemap 与 robots

    优先使用数据库 site_url,其次使用 SITE_URL。

  4. 规范化目录入口

    fix-build.ts 把 route.html 补成 route/index.html。

唯一推荐的完整构建入口
cd jy-site
pnpm build:static
pnpm preview

功能状态

能力 状态 说明
Markdown 与代码高亮 已实现 markdown-it + Shiki,支持 tip/warning/danger/details。
分类与标签页面 已实现 详情页可跳转;标签首页导航与分类/标签分页未实现。
搜索 部分实现 开发 API + 生产 JSON;索引失败当前只警告并继续构建。
RSS 已实现 需要有效站点 URL 才能生成正确绝对链接。
Sitemap 部分实现 缺 /creed、/life、分页和 RSS。
PWA 部分实现 有 manifest/SW;固定 cache-first 可能长期返回旧页面。
404 缺失 没有自定义错误页,现有 Nginx 配置引用的 404.html 不存在。
评论、点赞、阅读数写入 缺失 字段或表存在,但公开站没有写入链路。
资源完整性检查

fonts.css 引用了多个 /fonts/* 文件,但当前 static/ 下未发现对应字体。浏览器会回退到系统字体,应在发布前补齐或移除无效引用。

05管理端 jy-admin

模块与路由

路由 功能 保护
/login 用户名/密码登录 已登录用户跳转文章列表。
/articles 文章列表 segment layout 会话守卫。
/articles/new 新建文章与 Markdown 分屏 同上。
/articles/[id]/edit 编辑文章 同上。
/categories 分类 CRUD 同上。
/tags 标签 CRUD 同上。
/settings 站点与部署设置 同上,但没有 admin-only RBAC。
/api/publish 触发静态站构建 API 内检查 session。
/api/publish/status 查询最近 workflow API 内检查 session。

认证模型

  • 登录从 users 读取 bcrypt 哈希并校验 is_active。
  • 会话由 iron-session 存放在加密的 jy_admin_session Cookie。
  • 生产 Cookie 使用 secure=true,因此管理端生产环境必须启用 HTTPS。
  • 会话有效期为 7 天,SESSION_SECRET 必须至少 32 字符。
  • 没有全局 middleware;四个业务 segment 各自通过 layout 守卫。
角色不是权限控制

系统只在登录时允许 admin、author、editor, 登录后的 Server Actions 只检查“是否登录”。当前任意有效角色都能修改分类、标签、站点设置并触发部署。

数据写入结构

调用链
React Client Component
  └─ Server Action ('use server')
      ├─ getSession() / requireUser()
      ├─ getPool() → mysql2/promise
      ├─ 事务 / 参数化 SQL
      ├─ revalidatePath()
      └─ status=published 时可选 triggerSitePublish()

CRUD 覆盖

实体 覆盖 缺口
文章 创建 / 读取 / 更新 没有删除;可用 archived 代替软下线。
分类 完整 CRUD 被文章引用时禁止删除。
标签 完整 CRUD 删除时清理文章关联。
站点设置 部分 KV enable_comments、theme 未暴露。
媒体 仅 URL 没有上传、对象存储或媒体库。
用户 / 评论 未实现 只有登录读取;没有用户管理和评论审核。

生产运行

shell
cd jy-admin
pnpm install --frozen-lockfile
pnpm verify
NODE_ENV=production pnpm start  # :5174

当前仓库没有 PM2、systemd、Dockerfile 或 jy-admin 部署 workflow。仅执行 pnpm start 不能提供进程自愈、TLS、日志轮转和发布回滚。

06数据库与数据流

blog_site 主要实体关系 author_id 在代码中使用,但数据库未建外键
articles content · status · slug · author_id category_id · published_at users 登录与作者资料 categories 1 : N · SET NULL site_settings 独立 KV 配置 article_tags M : N 关联表 tags slug · color · is_active comments Schema 有 · 应用未实现 无 FK

表职责

表 主要职责 应用使用
articles Markdown、状态、分类、作者、计数与发布时间。 admin 写;site 只读 published。
categories 分类、slug、排序和启用状态。 双端使用;site 当前未过滤 is_active。
tags 标签、slug、颜色和启用状态。 双端使用;site 当前未过滤 is_active。
article_tags 文章与标签多对多关联。 admin 事务替换;site 联表读取。
users 管理登录与文章作者资料。 admin 登录;site 显示作者。
site_settings 站点与展示型部署配置。 双端读取不同键。
comments 评论与回复结构。 未被应用代码使用。

Schema 管理现状

  • blog_site.sql 是 DROP/CREATE/INSERT 的全量 dump,不是可重复的日常 migration。
  • 001 与 002 是手工 SQL,没有版本表和自动 runner。
  • 数据库中存在三个 VIEW,但应用继续使用手写 JOIN,没有调用它们。
  • articles.author_id 没有数据库外键,可能出现孤儿作者。
  • 没有备份、恢复、迁移校验或 schema drift 自动化。

07配置契约

应用环境变量

变量 jy-site jy-admin 说明
DATABASE_HOST 必需 必需 生产建议使用私网 DNS,例如 db。
DATABASE_PORT 可选 可选 默认 3306。
DATABASE_USER 必需 必需 不要在生产使用 root。
DATABASE_PASSWORD 按环境 按环境 只进入 secret store,不写入仓库。
DATABASE_NAME 可选 可选 默认 blog_site。
SITE_URL 推荐 — sitemap、robots、RSS 的环境兜底。
SESSION_SECRET — 必需 至少 32 字符,需长期稳定保存。
GITHUB_REPOSITORY — 发布功能 owner/repo。
GITHUB_TOKEN — 发布功能 最小化授权,可 dispatch 且可读 Actions。
安全模板 · 不含真实值
DATABASE_HOST=127.0.0.1
DATABASE_PORT=3306
DATABASE_USER=jy_app
DATABASE_PASSWORD=${JY_DATABASE_PASSWORD}
DATABASE_NAME=blog_site
SITE_URL=https://blog.example.com

# jy-admin only
SESSION_SECRET=${JY_SESSION_SECRET}
GITHUB_REPOSITORY=example-org/jy-prj
GITHUB_TOKEN=${JY_GITHUB_TOKEN}

站点 URL 的优先级

公开站生成绝对链接时优先读取数据库 site_settings.site_url,其次读取 SITE_URL,最终会回退到占位域名。上线前必须至少配置前两者之一并保证一致。

GitHub Actions Secrets

当前 workflow 使用 用途
DATABASE_* GitHub-hosted runner 在构建时直连数据库。
SITE_URL 静态绝对 URL 兜底。
DEPLOY_SSH_HOST rsync 目标主机。
DEPLOY_SSH_USER SSH 用户。
DEPLOY_SSH_KEY 部署私钥。
DEPLOY_SSH_PATH Nginx 静态目录。
立即处理示例凭证

两个 .env.example 当前包含看起来像真实数据库密码的示例值。应替换为明显占位符; 如果该值曾用于任何环境,先轮换凭证,再检查 Git 历史。本文档不会复制该值。

数据库部署设置不是控制面

管理端可保存 deploy_method、Nginx 路径和 SSH 展示字段,但当前 GitHub Actions 不读取这些键。实际部署分支和目标完全由 Actions Secrets 决定,因此两处配置可能漂移。

08内容发布

文章状态语义

状态 管理端 公开站
draft 可保存和继续编辑。 所有公开查询都不展示。
published 首次进入状态时写入 published_at。 下一次构建后进入列表、详情、搜索和 RSS。
archived 用于下线而不物理删除。 下一次构建后不再生成或展示。

线上更新路径

  1. 编辑并保存文章

    jy-admin 在事务中写入 articles 与 article_tags。

  2. 决定是否触发构建

    published 且 auto_publish_on_save=1 时自动 dispatch;也可点击顶栏按钮。

  3. GitHub Actions 预渲染

    runner 连接 MySQL,生成页面、搜索索引、RSS、sitemap 和 robots。

  4. 同步静态产物

    当前使用 rsync --delete 到 Nginx 目录,或回退 gh-pages。

  5. 管理端轮询状态

    构建中每 5 秒、空闲时每 30 秒查询最近 workflow run。

连续保存行为

部署 workflow 设置 cancel-in-progress: true。短时间连续发布会取消前一轮构建, 只保留最新运行,适合单人博客,但构建与部署处于同一 job 时仍应避免在 rsync 阶段强制取消。

09当前部署链

已存在的自动化

  • deploy-jy-site.yml 支持手动触发和 jy-site-publish repository dispatch。
  • 工作流固定 Node 版本文件与 pnpm 10.15.0,执行完整 build-static.ts。
  • 配置 SSH 主机时 rsync 到 Nginx;否则发布到 gh-pages 分支。
  • ci-jy-admin.yml 只运行 TypeScript 检查,不构建也不部署管理端。
当前工作流核心行为 · 摘要
pnpm install --frozen-lockfile
node --import tsx scripts/build-static.ts

rsync -avz --delete \
  jy-site/build/ "${DEPLOY_SSH_USER}@${DEPLOY_SSH_HOST}:${DEPLOY_SSH_PATH}/"

当前链路的生产风险

风险 影响 建议
runner 必须直连 MySQL 可能迫使数据库暴露公网。 改为自托管 runner、VPN/隧道,或推荐的服务器内构建。
Secrets 直接用于 step if GitHub 对 secret 条件表达式支持受限,分支可能不按预期。 使用非敏感 variable/input 选择部署目标。
rsync --delete 原地覆盖 坏构建会立即替换线上且没有版本。 版本目录 + 原子软链 + 保留最近发布。
搜索索引失败只警告 可能部署旧索引或没有搜索。 生产构建应 fail closed,并验证 JSON。
没有部署后 smoke test workflow 成功不等于站点可用。 检查首页、sitemap、搜索索引和代表文章。
Nginx 仅 HTTP 管理端 secure Cookie 无法正常工作,流量未加密。 部署 TLS 并强制 HTTP → HTTPS。

旧脚本不要作为生产入口

deploy.sh、deploy-elegant.sh 和旧部署指南仍执行 pnpm build,并描述已经不存在的 /jysite base path 与缺失的 nginx-fix-403.conf。生产文档和脚本应统一到 pnpm build:static。

10推荐 Docker 生产方案

建议方案,不是仓库现状

下面的 Dockerfile、Compose 与 Nginx 是可落地参考,但当前仓库中不存在这些文件。 实施时应单独评审、写入 deploy/ 并在预发布环境验证。

推荐拓扑:私网数据库 + 服务端静态构建 + 原子发布 虚线 = 触发;实线 = 运行时流量
Internet :443 blog / admin 子域名 Nginx TLS · 静态站 · 反向代理 jy-admin Next.js :5174 MySQL 仅 backend 网络 · 持久卷 site-builder 按需运行 · build:static GitHub Actions 仅 SSH 触发服务器脚本 site-releases/ 版本目录 + current 软链

建议目录

建议新增 · 尚未入库
deploy/
├── compose.production.yml
├── admin.Dockerfile
├── site-builder.Dockerfile
├── nginx.conf
├── deploy-site.sh
├── backup-db.sh
└── env/
    ├── admin.env.example
    ├── database.env.example
    └── site-build.env.example

/srv/jy/
├── app/                 # 真实 Git checkout
├── site-releases/
│   ├── 20260814-abcdef/
│   └── current -> 20260814-abcdef
└── backups/

Compose 参考

yaml · deploy/compose.production.yml(建议模板)
name: jy-production

services:
  db:
    image: mysql:8.4
    restart: unless-stopped
    env_file: ./env/database.env
    volumes:
      - mysql-data:/var/lib/mysql
      - ../jy-site/blog_site.sql:/docker-entrypoint-initdb.d/00-schema.sql:ro
      - ../jy-site/migrations/001_site_deploy_settings.sql:/docker-entrypoint-initdb.d/01-settings.sql:ro
      - ../jy-site/migrations/002_auto_publish_setting.sql:/docker-entrypoint-initdb.d/02-auto-publish.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$$MYSQL_ROOT_PASSWORD --silent"]
      interval: 10s
      timeout: 5s
      retries: 12
    networks: [backend]

  admin:
    build:
      context: ../jy-admin
      dockerfile: ../deploy/admin.Dockerfile
    restart: unless-stopped
    env_file: ./env/admin.env
    environment:
      NODE_ENV: production
      DATABASE_HOST: db
      DATABASE_PORT: 3306
    depends_on:
      db:
        condition: service_healthy
    expose: ["5174"]
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5174/login >/dev/null || exit 1"]
      interval: 20s
      timeout: 5s
      retries: 6
    networks: [frontend, backend]

  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    depends_on:
      admin:
        condition: service_healthy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - /srv/jy/site-releases:/srv/jy/site-releases:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    networks: [frontend]

  site-builder:
    profiles: ["tools"]
    build:
      context: ..
      dockerfile: deploy/site-builder.Dockerfile
    env_file: ./env/site-build.env
    environment:
      DATABASE_HOST: db
      DATABASE_PORT: 3306
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - /srv/jy/site-releases:/releases
    networks: [backend]

volumes:
  mysql-data:

networks:
  frontend:
  backend:
    internal: true
MySQL 版本迁移要求

当前 dump 来自 MySQL 5.7.44。示例使用 8.4 LTS 作为目标,不代表已经兼容验证。 实施前必须在空库导入 schema、运行两端检查与静态构建,并验证字符集、时间戳、ENUM 和 SQL 行为。

管理端镜像参考

dockerfile · deploy/admin.Dockerfile(建议模板)
FROM node:22.22.1-alpine AS deps
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM deps AS build
COPY . .
RUN pnpm build

FROM node:22.22.1-alpine AS runtime
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=node:node /app/package.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/.next ./.next
COPY --from=build --chown=node:node /app/public ./public
USER node
EXPOSE 5174
CMD ["pnpm", "start"]

当前 Next 配置没有 output: 'standalone',因此参考镜像复制完整 node_modules。后续可单独启用 standalone 减小镜像,但必须重新验证 iron-session 与文件追踪。

静态构建镜像参考

dockerfile · deploy/site-builder.Dockerfile(建议模板)
FROM node:22.22.1-alpine
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /workspace/jy-site
COPY jy-site/package.json jy-site/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY jy-site/ ./
CMD ["pnpm", "build:static"]

Nginx 参考

nginx · deploy/nginx.conf(节选建议模板)
server {
  listen 80;
  server_name blog.example.com admin.example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  http2 on;
  server_name blog.example.com;
  ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
  add_header Strict-Transport-Security "max-age=31536000" always;
  add_header X-Content-Type-Options "nosniff" always;

  root /srv/jy/site-releases/current;
  index index.html;

  location / {
    try_files $uri $uri.html $uri/ =404;
  }
  location ^~ /_app/immutable/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
  }
  location = /search-index.json {
    add_header Cache-Control "public, max-age=300";
  }
  location = /healthz {
    access_log off;
    return 200 "ok\n";
  }
}

server {
  listen 443 ssl;
  http2 on;
  server_name admin.example.com;
  ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
  add_header Strict-Transport-Security "max-age=31536000" always;
  add_header X-Content-Type-Options "nosniff" always;

  # 推荐在此加 VPN、SSO 或 allow/deny IP 策略
  location / {
    proxy_pass http://admin:5174;
    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;
  }
}

11自动化与回滚

推荐发布原则

  • GitHub Actions 只负责鉴权和触发,MySQL 不对 GitHub-hosted runner 开放。
  • 构建在服务器内部通过一次性 site-builder 容器完成。
  • 产物写入版本目录,验证成功后原子切换相对软链 current。
  • 任何一步失败都不触碰当前线上版本;保留最近 5 个版本供秒级回滚。

服务器发布脚本参考

shell · deploy/deploy-site.sh(建议模板)
#!/usr/bin/env sh
set -eu

release_id="${1:?release id required}"
release_dir="/srv/jy/site-releases/${release_id}"

mkdir -p "${release_dir}"
docker compose -f deploy/compose.production.yml run --rm \
  --entrypoint sh site-builder -c \
  'pnpm build:static && test -s build/index.html && test -s build/sitemap.xml && test -s build/search-index.json && cp -a build/. "/releases/'"${release_id}"'/"'

ln -sfn "${release_id}" /srv/jy/site-releases/.current.next
mv -Tf /srv/jy/site-releases/.current.next /srv/jy/site-releases/current
docker compose -f deploy/compose.production.yml exec -T nginx nginx -s reload

curl --fail --silent --show-error https://blog.example.com/ >/dev/null
curl --fail --silent --show-error https://blog.example.com/sitemap.xml >/dev/null
脚本落地时要修正路径边界

上例展示发布语义。实际容器内挂载点与宿主机路径必须统一;旧版本清理由独立、受测试的保留策略完成, 并严格限制在 /srv/jy/site-releases。上线前先用临时目录演练。

Actions 触发参考

yaml · 关键步骤(建议模板)
- name: Configure SSH
  env:
    SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
    KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
  run: |
    install -m 700 -d ~/.ssh
    printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
    chmod 600 ~/.ssh/deploy_key
    printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts

- name: Build and atomically publish on server
  env:
    HOST: ${{ secrets.DEPLOY_SSH_HOST }}
    USER: ${{ secrets.DEPLOY_SSH_USER }}
    RELEASE_ID: ${{ github.run_id }}-${{ github.run_attempt }}
  run: |
    ssh -i ~/.ssh/deploy_key "$USER@$HOST" \
      "cd /srv/jy/app && ./deploy/deploy-site.sh '$RELEASE_ID'"

不要使用运行时 ssh-keyscan 自动信任未知主机;把经过人工核验的 host key 存入 DEPLOY_KNOWN_HOSTS。

回滚

shell · 原子切换到指定旧版本
cd /srv/jy/site-releases
test -s "20260813-previous/index.html"
ln -sfn "20260813-previous" .current.next
mv -Tf .current.next current
docker compose -f /srv/jy/app/deploy/compose.production.yml exec -T nginx nginx -s reload
curl --fail https://blog.example.com/

数据库备份与恢复演练

shell · 每日备份参考
umask 077
stamp="$(date +%Y%m%d-%H%M%S)"
mkdir -p /srv/jy/backups

docker compose -f deploy/compose.production.yml exec -T db sh -c \
  'exec mysqldump --single-transaction --routines --triggers \
    -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' \
  | gzip -9 > "/srv/jy/backups/blog_site-${stamp}.sql.gz"

sha256sum "/srv/jy/backups/blog_site-${stamp}.sql.gz" \
  > "/srv/jy/backups/blog_site-${stamp}.sha256"
  • 备份文件应复制到异机或对象存储,不能只放在同一块磁盘。
  • 至少每季度在隔离数据库完成一次恢复演练并记录耗时。
  • 迁移前、批量内容修改前和账号变更前创建额外备份。

12安全与可观测性

生产安全基线

  • 数据库仅加入内部 Docker 网络,不映射宿主机 3306。
  • 管理端使用独立子域名和 HTTPS,并优先置于 VPN、零信任访问或 IP 白名单后。
  • 使用独立最小权限数据库用户;构建只读、管理端读写、备份单独授权。
  • SESSION_SECRET、数据库密码、GitHub PAT 和 SSH 私钥全部由 secret store 注入。
  • 对登录 API 加速率限制、失败审计和告警;高风险操作按角色授权。
  • Markdown 原始 HTML 只允许可信作者;当前公开渲染开启 html: true。
  • 为管理端和 Nginx 设置安全响应头,并关闭不必要的版本暴露。

建议的最小 RBAC

角色 建议权限
admin 全部内容、用户、设置、部署与密钥状态。
editor 编辑和发布所有文章、管理分类标签;不能改系统设置或用户。
author 创建并编辑自己的草稿;发布需 editor/admin 审核。

健康检查与监控

检查 当前 建议目标
公开站 HTTP 无专用端点 Nginx /healthz + 首页与代表文章 synthetic check。
管理端就绪 无专用端点 新增只检查进程的 liveness 与检查 DB 的 readiness。
数据库 连接时才暴露错误 Compose healthcheck、连接池错误率与备份新鲜度告警。
部署 只显示 Actions 结论 构建产物校验、部署后 smoke test、失败自动回滚。
日志 console + Nginx 默认日志 JSON 结构化日志、request ID、轮转与集中保留。

13完备度审计

最终回答:现在还不完备

它适合本地使用、可信维护者的小型博客以及受控的静态站发布;在补齐下列 P0/P1 项之前,不应按“无人值守、可恢复、可审计的生产系统”来承诺。

P0

上线阻塞:先解决安全与可恢复性

  • 替换并轮换示例环境文件中的凭证式值,确认未进入 Git 历史。
  • 建立可用管理员账号生成流程,禁止继续使用占位 hash。
  • 启用 HTTPS,限制管理端访问,并把 MySQL 放入私网。
  • 建立异机数据库备份与恢复演练。
  • 修复 Actions 部署目标分支逻辑,取消数据库公网直连依赖。
  • 确认代码位于真实 Git 仓库并配置受保护默认分支。
P1

生产工程化:让部署可验证、可回滚

  • 落地 Docker/Compose、管理端部署、健康检查和结构化日志。
  • 静态站使用版本目录原子发布,部署后 smoke test,保留最近版本。
  • CI 覆盖 site check/lint/build 与 admin check/build;测试脚本纳入自动化。
  • 搜索索引失败改为构建失败;补齐 404、sitemap 与 PWA 更新策略。
  • 实现 RBAC、登录限流、失败审计;为高风险设置和发布操作加授权。
  • 补齐或移除缺失字体,清理旧部署脚本和过期路径文档。
P2

产品完整性:按真实需求扩展

  • 文章删除/回收站、媒体上传与对象存储。
  • 用户管理、评论审核、浏览/点赞统计。
  • 服务端文章搜索/筛选与大数据量分页。
  • 删除死依赖、未使用 DB VIEW 与无消费者配置,统一默认值。

上线前验收清单

  • 两个项目的 check/build 在干净环境通过。
  • MySQL 迁移、管理员登录、文章草稿/发布/归档均已演练。
  • 首页、文章、分类、标签、搜索、RSS、sitemap 与 404 已验证。
  • 管理端 HTTPS、Cookie、退出登录、RBAC 和限流已验证。
  • 从发布触发到静态上线的整条链路有日志和失败告警。
  • 静态版本回滚与数据库恢复各演练至少一次。
  • 域名、证书续期、备份保留、监控联系人有明确负责人。

14故障排查

现象 优先检查 处理
管理端启动即报 SESSION_SECRET jy-admin/.env 生成并持久保存至少 32 字符 secret,重启进程。
数据库导入后无法登录 users.password_hash seed 是占位 hash;生成 bcrypt hash 并更新用户。
文章已发布但线上没有 Actions 最近 run、文章状态、DB 连通 确认发生了完整 build:static + deploy,而非只保存数据库。
生产搜索为空 build/search-index.json 检查索引生成日志、文件 total 与缓存头;重新完整构建。
RSS/sitemap 出现占位域名 site_settings.site_url、SITE_URL 配置真实 HTTPS origin 后重新构建。
页面字体与设计不一致 Network 中 /fonts/* 404 补齐授权字体文件或删除对应 @font-face。
Next 开发热更新后 500 .next 缓存 运行 pnpm dev:reset。
未知 URL 返回首页而非 404 adapter fallback 与 Nginx try_files 生产 Nginx 使用目录入口并以 =404 收尾。
SSH 部署分支未执行 workflow 的 secret 条件 改用 repository variable/input 选择 deploy target。
部署后静态资源混用旧版 SW、浏览器缓存、原地 rsync 更新 SW 策略并采用原子版本目录,必要时注销旧 SW。

诊断顺序

  1. 先确认是哪一层:浏览器、Nginx、admin Node、构建容器还是 MySQL。
  2. 记录请求 URL、时间、发布 run ID 与对应代码版本。
  3. 只执行不会破坏现场的只读检查;备份后再进行迁移或回滚。
  4. 修复后从用户路径复测,而不是只看进程退出码。

15事实来源

本手册把“仓库已有事实”和“建议生产方案”分开描述。以下文件是核对当前行为的主要来源; 相对链接在以文件方式打开手册时可直接定位到同一工作区文件。

主题 源文件
整体架构 README.md
公开站依赖与命令 jy-site/package.json
静态适配器 jy-site/svelte.config.js
完整静态构建 jy-site/scripts/build-static.ts
搜索索引 jy-site/scripts/generate-search-index.ts
公开站数据库连接 jy-site/src/lib/server/mysql-pool.ts
管理端依赖与命令 jy-admin/package.json
认证会话 jy-admin/lib/session.ts
文章 Server Actions jy-admin/lib/actions/article.ts
GitHub 发布集成 jy-admin/lib/github.ts
数据库 Schema jy-site/blog_site.sql
当前发布 workflow .github/workflows/deploy-jy-site.yml
当前 Nginx 样例 jy-site/nginx.conf
维护规则

当路由、环境变量、schema、构建入口或 workflow 发生变化时,同一 PR 应同步更新本手册。 建议每次生产发布前把“上线前验收清单”作为可执行 checklist 复核。

JY 项目一体化手册 · 工作区事实快照:2026-08-14 · 推荐配置尚未自动写入项目。

↑ ↓ 选择 · Enter 打开 · Esc 关闭