部署指南 / Deployment
在自己的机器或内网用 Docker / 云托管跑起 ERD Online;官方试用拓扑与镜像发布流程也写在本文。若只想最快起栈,先看 五分钟自托管。
官方托管拓扑(无 VPS)
决策见 ADR-0018。项目方不买生产 VPS、不托管用户生产数据;公开表面用 GitHub + Cloudflare 免费档。
文档站 ──► Cloudflare Pages(erdonline-docs)→ https://doc.erdonline.com [唯一公开]
静态 demo ─► Cloudflare Pages(erdonline-demo)
env-config.js ← Variables: DEMO_API_URL
└─► 指向 Railway 公网后端(ADR-0019)
官方 API + MCP ─► Railway(App + Node MCP sidecar + MySQL 8 + Redis)
https://api.erdonline.com → REST
https://api.erdonline.com/mcp → Streamable HTTP MCP
镜像:ghcr.io/erdonline/erdonline-backend:latest
备选(CN):Zeabur,同镜像思路
运行时镜像 ─► GHCR
ghcr.io/erdonline/erdonline-backend
ghcr.io/erdonline/erdonline-frontend
自托管数据面 ─► 用户自己的 docker compose(MySQL/Redis + 上列镜像)
← 用户生产仍走这条;Railway 只服务官方试用
| 表面 | 工作流 | 所需配置 |
|---|---|---|
| 文档 | .github/workflows/docs-site.yml(仅 deploy-cloudflare) | 见下清单;须 CLOUDFLARE_PAGES_DEPLOY=true |
| 静态 demo | .github/workflows/frontend-demo-site.yml | 同上 + 可选 Variable DEMO_API_URL |
| 发版镜像 | .github/workflows/release.yml(tag v*,job ghcr) | GITHUB_TOKEN + packages:write(通常无需额外 Secret) |
GitHub Actions × Cloudflare Pages 配置
一次性清单(复制勾选)。配置完成后 push main 才会真正跑部署 job。
1. Cloudflare API Token
- Cloudflare Dashboard → My Profile → API Tokens → Create Token
- 选用模板 Edit Cloudflare Workers(含 Workers / Pages 写权限)
- Account Resources:Include → All accounts(或指定本账号)
- Zone Resources:All zones,或留空不限(Direct Upload 不绑域名亦可)
- Client IP Address Filtering:不填;TTL:空(不过期)
- 建议改名:
erdonline-pages-deploy→ Create Token → 立刻复制(只显示一次)
2. Account ID
Workers & Pages 左侧边栏底部(或 Overview)复制 Account ID。
3. 创建两个 Pages 项目(Direct Upload)
Workers & Pages → Create → Pages → Upload assets / Direct Upload(不要接 Git 仓库;由 Actions + Wrangler 推送):
| 项目名(须一字不差) | 用途 | 工作流 |
|---|---|---|
erdonline-docs | Docusaurus 文档(主) | docs-site.yml → pages deploy … --project-name=erdonline-docs |
erdonline-demo | 前端静态 demo | frontend-demo-site.yml → --project-name=erdonline-demo |
4. GitHub Secrets / Variables
仓库 Settings → Secrets and variables → Actions:
| Name | 类型 | 值 |
|---|---|---|
CLOUDFLARE_PAGES_DEPLOY | Variable | true(门闸;未设则不部署文档站) |
CLOUDFLARE_API_TOKEN | Secret | 步骤 1 的 Token |
CLOUDFLARE_ACCOUNT_ID | Secret | 步骤 2 的 Account ID |
DEMO_API_URL | Variable(可选) | 公网 API 根 URL(官方 demo:https://api.erdonline.com);未设则 env-config.js API 为空(落地页可访问,完整试用待后端) |
5. 不要启用 GitHub Pages
文档只在 https://doc.erdonline.com。仓库 Settings → Pages 应保持 None(已用 API 关闭)。不要重新 Source = GitHub Actions,否则 github.io 会再长出第二套站。erdonline-docs.pages.dev 只是自定义域 CNAME 目标,不要当文档链接用。
6. 远程与触发
git remote -v # 须指向将跑 Actions 的 GitHub 仓库
git push origin main
docs-site.yml:push到main且改动docs/**/website/**/ 本 workflow 时构建;CF 部署须CLOUDFLARE_PAGES_DEPLOY=truefrontend-demo-site.yml:同样门闸;可workflow_dispatch手动跑- 无
git remote/ 未 pushmain→ Actions 不会跑
7. 验收 URL
| 表面 | URL |
|---|---|
| 文档(唯一公开地址) | https://doc.erdonline.com |
| 静态 demo(产品 URL) | https://www.erdonline.com |
| 静态 demo(CF Pages 默认别名,运维) | https://erdonline-demo.pages.dev |
Actions 页确认 Docs site / Frontend demo site 对应 job 绿。
8. GHCR(镜像,与 Pages 无关)
- 触发:推送 tag
v*(或release.yml的workflow_dispatch) - 权限:workflow 已声明
packages: write;登录用GITHUB_TOKEN,通常不必再配 Secret - 镜像:
ghcr.io/erdonline/erdonline-backend、ghcr.io/erdonline/erdonline-frontend
自托管者拉取 GHCR(推荐)
发版 tag(如 v5.0.1)后镜像推送到 GHCR。在目标机:
cp .env.example .env # 改密码;按需设 ERD_IMAGE_TAG=v5.0.1
docker compose pull # 拉 ghcr.io/erdonline/erdonline-{backend,frontend}
docker compose up -d
./scripts/verify-self-deploy.sh
公开包通常可读;若组织策略要求登录:
echo "$GHCR_TOKEN" | docker login ghcr.io -u YOUR_GITHUB_USER --password-stdin
本地改源码时不要依赖远程镜像,显式构建:
docker compose build backend frontend
docker compose up -d
Railway 部署官方 demo
决策见 ADR-0019。项目方用 Railway 单项目跑官方试用后端(真 MySQL 8 + Redis);用户生产仍用下方 Docker Compose。
成本:Hobby 量级约 $5–10/月(App + MySQL + Redis,以 Railway 定价 账单为准)。
Dashboard 五步(最短路径)
构建失败先看这里:MCP sidecar 加入镜像后,Docker build context 必须同时看见
backend/与mcp/。Railway 的 Root Directory 必须是仓库根.,再由backend/railway.toml指向backend/Dockerfile;继续用旧值backend会在COPY mcp/...时失败。另 :ghcr.io/erdonline/erdonline-backend在首次打v*tag 跑release.yml之前不存在(404)——在此之前请用 Dockerfile 从 GitHub 构建,不要选 Docker Image。
-
New Project → Deploy from GitHub(选
erdonline/erdonline)。不要先选 Docker Image(镜像尚未发布时会拉取失败)。 -
打开 App 服务 → Settings,按下面三项改(改完会触发重建):
设置项 必填值 说明 Root Directory .构建上下文 = 仓库根,供 Dockerfile 同时 COPY backend/与mcp/Config as Code / Railway config file backend/railway.toml强制 DOCKERFILEbuilder,并指向backend/DockerfileWatch Paths(可选) /backend/**,/mcp/**后端或 MCP sidecar 变更都触发部署;toml 已声明 确认 Builder 为 Dockerfile、
Dockerfile路径为backend/Dockerfile(相对仓库根)。 -
Add Plugin → MySQL(MySQL 8)与 Add Plugin → Redis;等插件 Ready。
-
在 MySQL 上建业务库
erd并导入 schema(见下方「Railway MySQL 正确接法」;插件默认库常名railway,不够)。 -
在 App 服务 → Variables 按「MySQL / Redis 正确接法」写入变量(Variable Reference,勿手抄密码)。
-
Settings → Networking → Public Networking 生 成
*.up.railway.appHTTPS。容器入口已读平台PORT(backend/Dockerfile);不必再手填 9502。验收:curl -sS https://YOUR-APP.up.railway.app/actuator/health/liveness# 期望 {"status":"UP"} (部署门禁;railway.toml 也指向此路径)curl -sS https://YOUR-APP.up.railway.app/actuator/health# 期望 {"status":"UP"} (含 db/redis;未接线时 503,业务未就绪)curl -sS -X POST https://api.erdonline.com/mcp \-H 'Content-Type: application/json' \-H 'Accept: application/json, text/event-stream' \-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'# 期望响应 serverInfo.name = erdonline随后在 GitHub Actions Variables 设
DEMO_API_URL=https://api.erdonline.com(无尾斜杠),重跑frontend-demo-site.yml,CF Pages 静态 demo 即指向该 API。
可选(首个 v* release 且 GHCR 已有包之后):空项目 → Add service → Docker Image → ghcr.io/erdonline/erdonline-backend:latest,跳过本地 Dockerfile 构建。
Healthcheck 连续失败(立刻自查)
healthcheckTimeout = 300(5 分钟)内 Attempt #1–#8 全是 service unavailable ≈ 2 分钟仍无人听端口,不是「JVM 慢一点」。正常 Boot 冷启约 30–90s;超过约 2–3 分钟日志里还没有 Started ErdOnlineApplication → 卡在 DB/Redis 或 prod 缺环境变量,进程在崩溃重试。
现在就看:Deployments → 当前部署 → View logs,搜这些关键词:
| 日志关键词 | 含义 | 怎么修 |
|---|---|---|
Could not find … base-logback.xml / No appenders | Logback include 失败(已修:改 include 根路径);不阻断启动,但后面真实错误可能看不见 | 拉含本修复的 commit 后 Redeploy;仍失败再往下看 |
Started ErdOnlineApplication | 进程已起来 | 再 curl /actuator/health/liveness;若公网仍 502 → Networking/域名 |
HikariPool.checkFailFast / PrimaryDatasource / Cannot resolve … erdSqlSessionFactory | JDBC 打不开(host/creds/库名)→ DS / MyBatis 级联失败 | App 须拿到插件注入的 MYSQLHOST/MYSQLUSER/MYSQLPASSWORD/MYSQLDATABASE;建库并导入 db/init schema |
Communications link failure / Connection refused / Unknown database | MySQL 未通或库名不对(插件默认常为 railway) | 确认 MYSQLHOST;MYSQLDATABASE 与已 init 的库一致(本地默认 erd) |
Unable to connect to Redis … localhost/127.0.0.1:6379 | 未注入 REDISHOST(或仍指望 REDIS_URL/SPRING_DATA_REDIS_URL) | Link Redis 或设 REDISHOST/REDISPORT/REDISPASSWORD;Redeploy |
NOAUTH Authentication required | 主机通但未带密码 | 确认 REDISPASSWORD 已注入;日志 password=missing |
WRONGPASS invalid username-password pair | 密码错,或空串被当成密码(旧镜像) | 用插件 REDISPASSWORD;本地无密码勿设假密码(Normalizer 把空串置 null) |
Could not resolve placeholder 'MYSQLUSER' / REDISPASSWORD / JWT_SECRET / ERD_UI_URL | prod fail-fast 缺变量 | Link 插件或手填;compose 无 Redis 密码时 REDISPASSWORD=(空);JWT_SECRET 见 .env.example;UI/CORS/SocketIO 设 ERD_UI_URL |
OIDC RSA private key missing in prod / ERD_OIDC_RSA_* | 未配置 RSA PEM/路径/keystore | 设 ERD_OIDC_RSA_PRIVATE_KEY 或 ERD_OIDC_RSA_PRIVATE_KEY_PATH(compose:.secrets/oidc-rsa-private.pem);与会话 JWT_SECRET 分离 |
martin.socketio.origin is blank or * / must not be * in prod | SocketIO/CORS 通配或空 | 设明确 ERD_UI_URL;勿 * / 空串 |
完全没有 Java/Tomcat started | 镜像未真正跑起来 / 入口错 | 确认 Root Directory=.、Config file=backend/railway.toml、Builder=Dockerfile |
容器内(Railway Shell):
echo "PORT=$PORT"
curl -sS -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:${PORT}/actuator/health/liveness"
curl -sS "http://127.0.0.1:${PORT}/actuator/health"
说明:
- 部署门禁用
/actuator/health/liveness(进程存活即可;railway.toml已改)。 - 业务就绪仍看
/actuator/health(含 db/redis)。不要靠management.health.db.enabled=false瞒过接线问题——优先把 MySQL/Redis 变量与erdschema 灌好。 - Dockerfile 已
--server.port=${PORT:-9502};Security 已放行/actuator/**。连续失败时优先查日志与 Variables,而不是改路径。
环境变量对照(Spring Boot)
与根目录 .env.example、docker-compose.yml、application.yml / application-prod.yml 对齐。
怎么加(Railway App 服务 → Variables):点 Add Variable → Add Variable Reference。服务名以 Canvas 上为准(常见 MySQL / Redis);引用语法 ${{ServiceName.VAR}}。
Railway MySQL 正确接法(唯一推荐)
为什么不能只靠一条 SPRING_DATASOURCE_URL:应用仍有两套自定义前缀(spring.datasource.martin + spring.datasource.erd,过渡期双 SqlSessionFactory,见 ADR-0020),Boot 的 SPRING_DATASOURCE_URL / 插件 MYSQL_URL 不会绑到这两前缀。Spring yml 直接读插件离散变量(每项单一 env,无 ${A:${B}} 嵌套)。
插件官方变量(Railway MySQL)。Link MySQL → App 后下列名称会出现在 App 环境(与插件同名,无需再改名为 DB_*):
| Spring 读取(App 环境名) | 含义 | 本地默认 |
|---|---|---|
MYSQLHOST | 私网主机(*.railway.internal) | localhost |
MYSQLPORT | 端口 | 3306 |
MYSQLUSER | 用户(常为 root) | erd |
MYSQLPASSWORD | 密码 | erd(仅非 prod) |
MYSQLDATABASE | 业务库名(插件常为 railway) | erd |
MYSQL_USE_SSL | JDBC useSSL | 本地/dev 默认 false;prod 默认 true |
MYSQL_REQUIRE_SSL | JDBC requireSSL(仅 prod URL) | prod 默认 true |
MYSQL_ALLOW_PUBLIC_KEY_RETRIEVAL | JDBC allowPublicKeyRetrieval | 本地默认 true;prod 默认 false |
MYSQL_URL | 连接串 | 应用不读(留给 init / 客户端) |
JDBC TLS(R-CFG-03):martin / erd 双 DS 同一 jdbc-url 模板。公网 / Railway demo(SPRING_PROFILES_ACTIVE=prod)默认开 SSL 并 requireSSL,关掉 allowPublicKeyRetrieval。本地 dev-ensure(dev profile)与 docker-compose(compose 显式注入 MYSQL_USE_SSL=false)保持明文 JDBC,避免无 TLS 的官方 MySQL 镜像打不开池。若私网插件握手失败可临时 MYSQL_USE_SSL=false(并配对 MYSQL_REQUIRE_SSL=false),但公网可达实例勿关。
Dashboard:
- Link MySQL 插件到 App(Variables 里应直接看到
MYSQLHOST等) - 库名策略二选一:App 显式设
MYSQLDATABASE=erd并跑下方 init;或对插件默认库灌 schema 且不覆盖MYSQLDATABASE - 删掉旧的
DB_HOST/DB_NAME/DB_USERNAME/DB_PASSWORD(Spring 已不读) - schema 灌好后再 Redeploy(Flyway 启动灌种子)
建库 + schema(插件空实例必做一次;种子由 App Flyway V3+ 写入):
用户不在
db/init:admin等系统用户 →backend/.../db/migration/erd/V3__system_baseline_seed.sql;公开 demo 项目 →V5__public_demo.sql;E2E 账号 →V6__e2e_users.sql(App Redeploy / 启动时ErdFlywayConfig自动打;勿手灌进 init)。
与本地 compose 的区别:
docker-compose已把db/init挂到 MySQL 空 data 卷首启;本地不必再跑本脚本。Railway / 远程插件库无该挂载,须用下列脚本或手工导入 仅 schema。
# 一键 schema(公网 URL;密码用环境变量,勿提交仓库)
MYSQL_URL="mysql://root:${MYSQLPASSWORD}@HOST:PORT/railway" ./scripts/railway-mysql-init.sh
# Docker 方式(本机只需 Docker)
MYSQL_URL="mysql://root:${MYSQLPASSWORD}@HOST:PORT/railway" ./scripts/railway-mysql-init.docker.sh
# 手工:
# mysql … < db/init/01_create_database.sql
# mysql … < db/init/02_tables.sql
验收 SQL(若选 MYSQLDATABASE=erd):
SHOW DATABASES LIKE 'erd';
SHOW TABLES FROM erd LIKE 'sys_user';
SELECT COUNT(*) FROM erd.sys_user;
SELECT MAX(version) FROM erd.flyway_schema_history WHERE success=1;
期望 Deploy 日志:Started ErdOnlineApplication,无 HikariPool.checkFailFast / Unknown database。
| 现象 | 含义 |
|---|---|
Connection refused / host=localhost | App 未拿到 MYSQLHOST(未 Link / 未注入) |
Unknown database 'erd' | 未跑 schema init,或未把 MYSQLDATABASE 指到已建库 |
Unknown database 'martin' | 仍为旧双库镜像;Redeploy 新版本并单库 init |
Access denied | MYSQLUSER / MYSQLPASSWORD 不对 |
只设了 MYSQL_URL / SPRING_DATASOURCE_URL | 无效(自定义前缀不读这两项) |
SSL connection required / Communications link failure(TLS) | 库未开 TLS 但 prod 默认 MYSQL_USE_SSL=true |
Railway Redis 正确接法(唯一推荐)
Spring yml 直接读插件离散变量(与 MySQL 同理) :
| Spring 读取 | 含义 | 本地默认 |
|---|---|---|
REDISHOST | 私网主机 | localhost |
REDISPORT | 端口 | 6379 |
REDISUSER | ACL 用户(常有 default) | 空 → null |
REDISPASSWORD | 密码 | 空 → null(无 AUTH) |
REDIS_URL / REDIS_PUBLIC_URL | 连接串 | 应用不读 |
Dashboard:
- Link Redis 插件到 App(应直接看到
REDISHOST/REDISPORT/REDISPASSWORD/REDISUSER) - 删掉旧的
SPRING_DATA_REDIS_URL/ 裸REDIS_URL映射(yml 已不依赖) - Redeploy
期望日志:
Redis bound host=….railway.internal port=6379 database=0 url=missing password=set
| 现象 | 含义 |
|---|---|
host=内网 + password=set | 正确 |
host=localhost | 未注入 REDISHOST |
内网 host + password=missing | 缺 REDISPASSWORD → NOAUTH |
其它必填变量
| 应用变量(填这个名) | Variable Reference 示例 | 说明 |
|---|---|---|
SPRING_PROFILES_ACTIVE | prod(手填) | 生产 fail-fast;须显式给齐凭证 |
JWT_SECRET | 随机 ≥32 字节(手填) | 必改;勿用仓库默认值 |
ERD_DB_CONFIG_SECRET | 随机 ≥32 字节(手填,与 JWT_SECRET 不同值) | 必改;data_sources.username/password 落库加密密钥(R-DATA-06,AES-256-GCM);勿用仓库默认值;缺失或用默认值 prod fail-fast |
ERD_OIDC_RSA_PRIVATE_KEY 或 _PATH / keystore | PKCS#8/PKCS#1 PEM 或 PKCS12 | prod 必填;OIDC id_token RS256;公钥经 /.well-known/jwks.json;勿提交私钥;compose 挂载 .secrets/ |
ERD_OIDC_ISSUER | 可选;无尾斜杠 | 空则 issuer=ERD_UI_URL;直连 API 时设 API 公网根 |
JWT_EXPIRES_IN | 604800(7 天) | 可选;会话 JWT 秒数。无 UI refresh;公网可收紧(如 86400) |
ERD_E2E_ACCOUNTS_ENABLED | false | 公网禁止 e2e 弱口令 |
ERD_ALLOW_DEMO_ADMIN | false | 公网禁止 admin/123456 种子口令;改密后不受影响 |
ERD_ALLOW_OPEN_REGISTER | false | 公网禁止匿名开放注册;本地/E2E 靠 dev profile;逃生阀显式 true(2026-08-05 前 prod 该逃生阀因 YAML 字面量覆盖失效,见 R-CFG-07,现已修复);联邦首次建号也走此门控;确认生效看后端日志 federate rejected open register ... allow-open-register=...(拒绝时打印,不含密钥) |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / GITHUB_REDIRECT_URI | 通常不设 | ADR-0021:GitHub OAuth App;三项齐才启用;回调例 https://API/auth/federate/github/callback |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_REDIRECT_URI | 通常不设 | ADR-0021:Google OIDC 登录;三项齐才启用;回调例 https://API/auth/federate/google/callback |
WECHAT_APP_ID / WECHAT_APP_SECRET / WECHAT_REDIRECT_URI | 通常不设 | ADR-0021:微信开放平台网站应用扫码;三项齐才启用 |
ERD_FEDERATE_SUCCESS_PATH | /login/federate | 联邦回调后 UI 落点路径(拼在 ERD_UI_URL 后) |
ERD_CATALOG_API_URL | 通常不设(空 = 仅本地模板种子) | ADR-0028:可选远程 catalog 合并;自托管 offline 默认可用 |
ERD_CATALOG_MAINTAINER_USERNAMES | prod 必填(逗号分隔,如 alice,bob) | ADR-0028:可审核 /catalog/review 的维护者账号;默认空,不设则无人可审核社区提交;勿依赖仓库 admin 种子 |
MYSQL_USE_SSL / MYSQL_REQUIRE_SSL / MYSQL_ALLOW_PUBLIC_KEY_RETRIEVAL | Railway 默认勿设(走 prod 开 SSL);compose 已关 | 见「Railway MySQL」TLS 段;无 TLS 插件须显式关 |
ERD_UI_URL | 双源(推荐):https://app.erdonline.com,https://www.erdonline.com(无尾斜杠、逗号无空格);单 UI 时可只写一项 | prod 必填:CORS(martin.ui.url)拿到全部逗号分隔条目;SocketIO origin 仅此一键(c15de0c 后勿再设 SOCKETIO_ORIGIN/CORS_ALLOWED_ORIGINS);禁 * / 空串;OIDC issuer 只取第一个合法 http(s) 条目(iss 须单值,跳过畸形/通配);任一条目缺 http(s):// 前缀(如打字漏字母的 ttps://)prod 直接 fail-fast,报错点名具体值 |
OSS_ENDPOINT / OSS_ACCESS_KEY / OSS_SECRET_KEY | 通常不设 | 可选 MinIO;未设 endpoint = 不建客户端;启用时须非 minio/minio123(OssCredentialGuard) |
SOCKETIO_PORT | 9092 | Presence 与 HTTP(9502/PORT)分离;勿对公网裸放 9092(防火墙/安全组仅内网,或经受控反代);单公网 HTTP 口时浏览器常连不上,demo 可先忽略 |
生成 OIDC RSA 私钥(Railway / prod)
OidcRsaKeySupport 接受 PKCS#8(-----BEGIN PRIVATE KEY-----)或 PKCS#1(-----BEGIN RSA PRIVATE KEY-----)RSA 私钥 PEM;2048 位;无需单独配置公钥——运行时从私钥推导公钥并发布到 GET /.well-known/jwks.json(kid 默认为公钥 JWK thumbprint,可 选覆盖 ERD_OIDC_RSA_KID)。与会话 JWT_SECRET 分离;勿提交进 git。
本机生成(推荐 PKCS#8,与 dev 自动落盘格式一致):
mkdir -p ~/.erdonline
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ~/.erdonline/oidc-rsa-private.pem
chmod 600 ~/.erdonline/oidc-rsa-private.pem
Railway Variables(二选一):
| 变量名 | 用法 |
|---|---|
ERD_OIDC_RSA_PRIVATE_KEY | 变量类型选 Secret;值 = PEM 整段(含首尾行与换行),Dashboard 多行粘贴即可;勿把换行压成一行或包一层引号 |
ERD_OIDC_RSA_PRIVATE_KEY_PATH | 仅当容器内确有挂载 PEM 文件路径时用(compose 常用 .secrets/oidc-rsa-private.pem);Railway 无文件卷时优先用上行的 _PRIVATE_KEY |
改密钥或补密钥后 Redeploy;验收 GET https://<API>/.well-known/jwks.json 返回 RSA 公钥 JWK。
MySQL / Redis:详见上两节。Link 插件后用原生
MYSQL*/REDIS*;MYSQL_URL/REDIS_URL/SPRING_DATASOURCE_URL/SPRING_DATA_REDIS_URL不是本应用主接线路径。
本地 / compose 默认监听 9502。Railway 会注入 PORT:backend/Dockerfile 入口为 java … --server.port=${PORT:-9502},与公网代理对齐。仓库提交了 backend/railway.toml(Dockerfile builder + /actuator/health/liveness);Dashboard 须设 Root Directory = . 与 Config file = backend/railway.toml,让镜像同时构建 backend/ 与 mcp/。Docker / Railway 构建走 Maven Central(不 COPY .mvn/settings.xml 阿里云镜像;国内本机仍可用该 settings)。
接 CF Pages / 自定义域名
两边必须对着写(UI ≠ API):
| 侧 | 变量 | 值 |
|---|---|---|
| Railway(后端) | ERD_UI_URL | 浏览器打开的前端 Origin;生产 + CF Pages demo 共用同一 API 时用逗号双源(见下) |
| CF Pages / 自定义域前端 | API_URL + ERD_API_URL(demo workflow 用 Variable DEMO_API_URL 写入二者) | 公网 API 根:https://api.erdonline.com(无尾斜杠) |
勿把 API_URL 设成 UI 域名(如误设 https://app.erdonline.com)——那会让前端把 API 指回自己。
CORS(Railway 后端必配):浏览器从 Cloudflare Pages / 自定义域前端跨域调用 Railway API 时,后端 ERD_UI_URL 须设为前端 Origin(无尾斜杠;不是 Railway API 域名)。仓库内 config.prod.ts / .env 的 API_URL 只指后端;CORS 仅在 Railway App Variables 配置,缺则 prod 启动 fail-fast 或浏览器预检 403(Invalid CORS request、无 Access-Control-Allow-Origin)。
生产 UI + 官方 demo 共用 Railway API(公网 API 根:https://api.erdonline.com):
ERD_UI_URL=https://app.erdonline.com,https://www.erdonline.com
改完后须 Redeploy(Variables 变更不会热加载)。验收:
# 期望 HTTP 200 + Access-Control-Allow-Origin: https://www.erdonline.com
curl -sI -X OPTIONS 'https://api.erdonline.com/auth/federate/providers' \
-H 'Origin: https://www.erdonline.com' \
-H 'Access-Control-Request-Method: GET'
Railway CLI(项目已 railway link 且已登录时):
railway variables set ERD_UI_URL='https://app.erdonline.com,https://www.erdonline.com'
railway up # 或 Dashboard → Redeploy
改 ERD_UI_URL 后 Redeploy 反而崩容器(日志形如 Error creating bean with name 'oidcIdTokenService': Invocation of init method failed → UnsatisfiedDependency → patAuthenticationFilter → Unable to start embedded Tomcat):
- 不是
ERD_UI_URL多源 CSV 本身的问题 —— CORS(CrossOriginPolicy.resolveHttpAllowedOrigins)与 OIDC issuer(OidcConfig.resolveIssuer)都已按逗号拆分,issuer 固定取第一个合法 http(s) 条目,见OidcConfigTest/CrossOriginPolicyTest回归 - 真根因两种,且常与「顺手改了
ERD_UI_URL」同时发生(Railway Redeploy 会重新走 fail-fast 校验,旧容器可能是在校验加固前起的):ERD_OIDC_RSA_PRIVATE_KEY(或_PATH/_KEYSTORE_PATH)未设:OidcIdTokenService的@PostConstruct经OidcRsaKeySupport.load在 prod 强制要求 RS256 私钥,缺失即 fail-fast,报错信息含OIDC RSA private key missing in prod;这条 异常沿OAuthApiClientServiceImpl → patAuthenticationFilter的构造依赖链向上抛,最终表现为「Tomcat 起不来」——看似跟ERD_UI_URL无关,实为同一次 Redeploy 顺带触发。修复:在 Railway Variables 补回该私钥(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048生成后整段贴入,或挂_PATH到 Secret 文件),RedeployERD_UI_URL条目本身打字错误(如漏字母的ttps://...):这类值不是合法 http(s) Origin,本仓库已加固为 prod fail-fast(CrossOriginPolicy.assertWellFormed),报错信息会直接点出具体畸形值,而不是绕到 OIDC 链路——按提示修正 Origin 拼写即可
- 排查顺序:
./backend/dev-ensure.sh --logs(本地复现)或 Railway Deploy Logs 找Error creating bean with name后第一行Caused by:;含ERD_OIDC_RSA_PRIVATE_KEY→ 走情形 1;含malformed origin→ 走情形 2
- Railway liveness 绿,且
actuator/health为 UP(或至少能登录/注册) - 仓库 Settings → Secrets and variables → Actions → Variable
DEMO_API_URL=https://api.erdonline.com(或 Pages 项目 Variables 直接写API_URL/ERD_API_URL) - 跑
frontend-demo-site.yml(workflow_dispatch或 push);自定义域app.erdonline.com绑到对应 Pages 项目后同样依赖上述 API 注入 - 打开正式 UI(
https://app.erdonline.com/auth/login)或 demo(https://www.erdonline.com),Network 确认请求打到 Railway,而非 UI 自身
Zeabur 备选(中国区)
国内网络下可用 Zeabur 作非默认备选;官方默认仍以 Railway 为准(ADR-0019)。
这个 URL 是什么:Zeabur 服务 = 后端 API only,不是完整产品站。浏览器打开 https://xxx.zeabur.app/ 看到 404 常常正常(Spring Boot 无落地页)。前端试用站仍是 Cloudflare Pages,靠 DEMO_API_URL 指向此 API。
预期 404 vs 真挂了
| 现象 | 含义 | 你该做什么 |
|---|---|---|
/ → 404,但 /actuator/health → {"status":"UP"} | API 已通 | 设 DEMO_API_URL,用 CF Pages 前端 |
/、/actuator/health、/doc.html 全部 404(空 body、server: Caddy) | 公网没打到 Boot(常见:Root Directory 仍是仓库根,zbpack 误检前端) | 按下方 Dashboard 必改重建 |
| health 502 / 连不上 | 未听 PORT,或缺 DB/Redis 启动失败 | 看「日志」;补 MySQL/Redis 与环境变量 |
curl -sS -D- -o /dev/null https://YOUR.zeabur.app/ # 可为 404
curl -sS https://YOUR.zeabur.app/actuator/health # 期望 {"status":"UP"}
Dashboard 必改(monorepo)
仓库根有 frontend/、无根级 Dockerfile。Root Directory 留空时 Zeabur 常按 Node 前端构建 → Dashboard「运行中」但 API 全 404。
- Deploy from GitHub(
erdonline/erdonline) - 设置 → Root Directory =
.(仓库根,供镜像同时读取backend/与mcp/);改完会重建 - 确认走 Dockerfile,路径为
backend/Dockerfile;若仍误检,环境变量加ZBPACK_DOCKERFILE_PATH=backend/Dockerfile - 网络绑定公网域名。入口已读平台
PORT(--server.port=${PORT:-9502}),不必手填 9502 - 首个
v*且 GHCR 有包后,也可改用镜像ghcr.io/erdonline/erdonline-backend:latest
MySQL + Redis + 环境变量
与上节 Railway 同一张表(SPRING_PROFILES_ACTIVE=prod、MYSQL*、REDIS*、JWT_SECRET、ERD_OIDC_RSA_PRIVATE_KEY 或 _PATH、ERD_UI_URL、ERD_E2E_ACCOUNTS_ENABLED=false;勿为占位乱填 OSS_*,除非真要启用 MinIO)。
- 同项目添加 MySQL 8 + Redis
- 建单一业务库并导入 schema(种子由 App Flyway 写入):
再导入CREATE DATABASE IF NOT EXISTS erd DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
db/init/02_tables.sql;或跑scripts/railway-mysql-init.sh - 把平台 MySQL/Redis 的 host/port/密码注入为
MYSQLHOST/MYSQLDATABASE=erd/REDISHOST/REDISPASSWORD等 ERD_UI_URL(必填)= 前端 Origin:生产自定义域用https://app.erdonline.com;官方 demo 用https://www.erdonline.com(CF Pages 项目erdonline-demo的自定义域;默认别名erdonline-demo.pages.dev仍可用但产品面统一 www);禁*- health 绿后:GitHub Actions Variable
DEMO_API_URL=https://YOUR.zeabur.app(无尾斜杠)→ 重跑frontend-demo-site.yml(前端 API ≠ UI 域名)
最短路径
- Root Directory=
.+ Dockerfile=backend/Dockerfile+ MySQL + Redis + 上表变量 → 域名 PROVISIONED curl …/actuator/health见UP(预览窗打开/的 404 可忽略)DEMO_API_URL→ 打开 CF Pages demo 试用
Docker Compose(推荐 · 用户自托管 / 生产)
cp .env.example .env # 修改端口 / 密码;可选 ERD_IMAGE_TAG
docker compose pull # 优先使用 GHCR 预构建镜像
docker compose up -d # mysql + redis + backend + frontend
# 或本地构建:docker compose build && docker compose up -d
docker compose logs -f backend # 查看后端日志
Schema 双源(自部署必读)
| 来源 | 何时生效 | 说明 |
|---|---|---|
db/init/(schema-only) | MySQL 空 data 卷首次启动 | 仅建库 + CREATE TABLE;卷已存在时不会再跑 |
Flyway(backend/.../db/migration/erd/) | 后端每次启动(ErdFlywayConfig) | 增量 schema 与种子的真相源 |
新变更只加 Flyway。本地从旧双库升级:docker compose down -v 后重建(ADR-0020)。
数据库连接凭证加密(R-DATA-06):data_sources.username/password 落库前用 AES-256-GCM 加密(密文前缀 enc:v1:),密钥取 ERD_DB_CONFIG_SECRET。首次部署必改;沿用仓库默认值 prod 直接 fail-fast。改密钥即丢失旧密文解密能力——轮 换前须先用旧密钥把所有连接批量重新保存(触发重新加密)或手动导出/重建连接,见 security-model.md。
访问:
- 前端 http://localhost:8000
- 后端 API http://localhost:9502
健康检查 / 版本信息(自部署验收)
后端已挂 Spring Actuator,仅暴露 health 与 info(匿名可读;不暴露 env/beans/metrics)。已开 Boot probes。compose 或独立 jar 拉起后:
# 部署门禁 / 存活(不含 db/redis)
curl -sS http://localhost:9502/actuator/health/liveness
# 期望 {"status":"UP"}
# 业务就绪 (含 db/redis;依赖挂则 503)
curl -sS http://localhost:9502/actuator/health
# 应用名 + 版本(本地 classpath 常为 "dev";正式 jar 为 Manifest 版本)
curl -sS http://localhost:9502/actuator/info
# 期望含 "app":{"name":"erd-online","version":"..."}
未暴露的 actuator 子路径返回 404(勿再伪装成 500「操作发生错误」)。
一键验收脚本
栈拉起后(docker compose up -d 或 本地 mysql/redis + ./backend/dev-ensure.sh + yarn start)跑:
./scripts/verify-self-deploy.sh
# 期望:health UP、info 含 erd-online、/actuator/env → 404、前端 / → 200;
# 若存在容器 erd-mysql,再断言 erd.flyway_schema_history 有成功版本
可选环境变量:API_BASE / FE_BASE / SKIP_FE=1 / SKIP_FLYWAY=1 / MYSQL_CONTAINER。
升级路径演练(已有 data 卷)
自部署升级不重跑 db/init/(卷已存在时 MySQL 入口脚本不会再次执行)。增量 schema 靠后端启动时 Flyway 打到 erd 库。
# 1) 备份(示例:具名卷)
docker compose stop
docker run --rm -v erdonline_erd-mysql-data:/v -v "$(pwd)":/b alpine \
tar czf /b/erd-mysql-backup-$(date +%Y%m%d).tgz -C /v .
# 2) 拉新代码 / 新镜像(优先 GHCR;无对应 tag 再本地 build)
git pull
# docker compose build backend frontend # 仅本地改源码时
docker compose pull
docker compose up -d
# 3) 验收(含 Flyway 最新成功版本号)
./scripts/verify-self-deploy.sh
# 4) 可选:看迁移历史
docker exec erd-mysql mysql -uerd -perd erd \
-e "SELECT installed_rank,version,description,success FROM flyway_schema_history ORDER BY installed_rank;"
本地开发(非 compose 全栈)升级同样只需 ./backend/dev-ensure.sh --restart(classpath 有新 V*__*.sql 即 migrate),再跑同一验收脚本。
服务说明
| 服务 | 端口 | 说明 |
|---|---|---|
| frontend | 8000 | Nginx 托管前端静态资源,反代 /api、/ncnb 到后端 |
| backend | 9502 | Spring Boot 单体 |
| mysql | 3306 | 数据库(erd + martin) |
| redis | 6379 | token / 缓存 |
| MinIO(可选) | 9000 | 对象存储;非 compose 默认依赖 |
MinIO(可选)
默认 docker compose 不含 MinIO。Word 导出与「下载默认模板」使用后端 classpath 内置模板(templates/word/defaultWorldTemplate.docx),无 MinIO 亦可导出。
需要上传自定义 Word 模板或把默认模板托管到对象存储时,再配置:
# 环境变量示例(需非空 OSS_ENDPOINT 才建 MinioClient;密钥走嵌套 martin.oss.minio.*)
OSS_ENDPOINT=http://localhost:9000
OSS_ACCESS_KEY=minio
OSS_SECRET_KEY=... # prod 禁止仍用 minio123
application.yml 已声明嵌套占位(空默认);本地只需导出上述环境变量即可,例如:
martin:
oss:
minio:
endpoint: ${OSS_ENDPOINT:}
accessKey: ${OSS_ACCESS_KEY:}
secretKey: ${OSS_SECRET_KEY:}
未配置时:gendocx / downloadWordTemplate 降级走内置模板;uploadWordTemplate 返回明确错误(提示配置 MinIO),不再 NPE。
数据库连接驱动(系统驱动 + 官方连接器包 + 扩展连接器包,ADR-0029)
默认镜像内置 MySQL/MariaDB、PostgreSQL、Oracle、SQL Server 四种驱动,docker compose up -d 拉起即可用,不需要任何额外配置——这四型与设计器里逆向解析/同步/在线预览的支持面完全一致。四型内部分两类(对使用者无感知,仅维护者构建产物治理用):
- 系统驱动:MySQL/MariaDB——应用自举必需(连接自身
erd/martin系统库),永久打入backend模块本身 - 官方连接器包:PostgreSQL、Oracle、SQL Server——服务用户数据源;目标状态是从
backend模块编译依赖中移出,只在镜像构建阶段合并进默认发布镜像,与backend应用本体解耦(迁移未完成前,现状仍随backend一起编译,行为对使用者无差异)
未来若要接入这四型以外的库类型(DB2、ClickHouse 等扩展连接器包),走部署时二选一,均由运维在起栈前决定,不会出现"运行中要求终端用户上传驱动/填驱动类名才能继续"的情况:
- 切换到包含该扩展包的备选镜像 tag(若已发布),或
- 自行构建镜像时把驱动 jar 编译进对应可选模块
尚未发布任何扩展连接器包(当前只有默认镜像四型);三层模型的安全与许可评估见 ADR-0029「驱动管理」章节。
生产建议
- 修改
.env中所有默认密码(含admin);prod即使未改密也会拒绝admin/123456登录(erd.security.allow-demo-admin=false) - 删除或改密种子账号
e2e0..e2e15、e2e-serial(弱口令仅供本地/CI;prod默认拒绝登录,仍建议删库内记录) - 勿设置
ERD_E2E_ACCOUNTS_ENABLED=true、ERD_ALLOW_DEMO_ADMIN=true或ERD_ALLOW_OPEN_REGISTER=true到公网环境 - SocketIO(9092):与 HTTP API 分离监听;自托管/PaaS 不要把
9092裸映射到公网安全组。需 Presence 时仅内网可达,或经 TLS 反代并限制来源;单 HTTP 口平台上浏览器常连不上 9092(demo 可忽略) - 后端 jar 单独部署时,通过环境变量覆盖数据源/redis 配置(见
application-prod.yml) - 前端可将
dist/部署到任意静态服务器 / CDN,运行时通过env-config.js注入API_URL
手动构建产物
# 后端 jar
cd backend && mvn clean package -DskipTests # 产物:target/*.jar
# 前端 dist(生产:先写 env-config.js)
cd frontend && yarn && API_URL= ERD_API_URL= yarn build:prod # 产物:dist/
前端 env-config.js(构建时 vs 运行时)
| 场景 | 做法 |
|---|---|
本地 yarn start | env.local.sh → public/env-config.js(默认开发代理;可选 gitignored frontend/.env;CI 不读) |
| 静态 CDN / CF Pages | CI 设 API_URL/ERD_API_URL(或 DEMO_API_URL)后 yarn build:prod,配置打进 dist/env-config.js |
| Docker / Nginx 同源 | 镜像内可空;容器启动 docker-entrypoint.sh 按环境变量重写 env-config.js |
浏览器读 window._env_.API_URL(见 frontend/src/utils/request.js)。静态站 没有同源反代时必须填可公网访问的后端根 URL(Railway/Zeabur),不要填 UI 域名(app.erdonline.com / *.pages.dev);留空则仅适合落地/文档类页面。
百度统计
站点 ID bd50dd978c8d8d94792f4e987c4a7aaf 硬编码于 frontend/config/config.ts(BAIDU_TONGJI_ID);prod 构建(UMI_ENV=prod)经 headScripts 动态注入 hm.baidu.com/hm.js,并在运行时 跳过 localhost / 127.0.0.1 / [::1],避免本地 yarn build:prod && serve 或文档站 website/yarn serve 污染线上 PV。文档站见 website/docusaurus.config.js + src/clientModules/baiduAnalytics.js(同源守 卫)。
| 场景 | 行为 |
|---|---|
本地 yarn start | 不加载(UMI_ENV=dev) |
本地 yarn build(UMI_ENV=dev) | 不加载 |
| 本地 serve prod 产物(localhost) | 守卫跳过 hm.js / CF beacon |
CF Pages / yarn build:prod / Docker 前端镜像 | 公网 hostname 自动加载 hm.js |
本项目路由为 hash 模式(config.ts hash: true),hm.js 会监听 hash 变化自动上报 PV,无需额外 onRouteChange 钩子。未配置 CSP script-src 白名单时默认可加载 hm.baidu.com。
控制台降噪(历史数据):排除主机名 localhost / 127.0.0.1;排除来源 URL 含 tongji.baidu.com、accounts.google.com;排除页面 URL 含 /login/federate。
Cloudflare Web Analytics
Token 4df015bf119f48ff9b03f302f6a3e40a 硬编码于 frontend/config/config.ts(CLOUDFLARE_WEB_ANALYTICS_TOKEN + prod 专用 headScripts 内联 bootstrap),与百度统计并列;仅 UMI_ENV=prod 构建注入(Umi 对带 JSON 的 data-cf-beacon script 属性会拆坏,故用 IIFE 动态 append <script type="module">)。
| 场景 | 行为 |
|---|---|
本地 yarn start / yarn build(UMI_ENV=dev) | 不加载 |
CF Pages / yarn build:prod / Docker 前端镜像 | 自动加载 beacon |
验收:curl -sL https://www.erdonline.com | grep -E 'cloudflareinsights|4df015bf119f48ff9b03f302f6a3e40a' 应命中;浏览器 Network 可见 beacon.min.js。(CF 默认别名 erdonline-demo.pages.dev 亦可,自定义域 www 为产品 URL。)
文档站 SEO / 爬虫索引(doc.erdonline.com)
文档由 Cloudflare Pages 项目 erdonline-docs 托管(docs-site.yml)。GSC 里 doc.erdonline.com/docs/faq、/docs/quick-start/* 等是 2026-03~05 旧 IA,当前 Docusaurus 没有这些路径,直出 404/域名 500 时 Google 会标「无法编入索引」。
| 产物 | 作用 |
|---|---|
Docusaurus sitemap.xml | 现有 /docs/guide/*、ADR、首页;<loc> 一律尾斜杠(trailingSlash: true,与 CF Pages 308 一致);不含 /search 与 blog 标签/归档页 |
website/static/robots.txt | 覆盖 CF 默认 Content-Signal 模板;Allow: / + Sitemap: |
website/static/_redirects | 旧路径 301 到现行指南(/docs/faq → /docs/guide/intro/,目标带尾斜杠,避免再 308) |
website/functions/_middleware.js | erdonline-docs.pages.dev 301 到 doc.erdonline.com,无扩展名路径补尾斜杠(_redirects 绑不了默认 pages.dev 宿主) |
website/static/_headers | robots.txt / sitemap.xml 的 Content-Type |
自定义域(GSC 能抓到的前提)
- Cloudflare Dashboard → Pages →
erdonline-docs→ Custom domains → 添加doc.erdonline.com - DNS:
docCNAME →erdonline-docs.pages.dev(橙云代理;等 SSL 变为 Active) - 验收:
curl -sI https://doc.erdonline.com/→ 200(不是 TLS 失败 / 500) - Search Console 提交
https://doc.erdonline.com/sitemap.xml,并对首页与/docs/guide/intro请求编入索引
旧 URL 不会作为独立页面再进索引;301 后 Google 会把权重收到新路径。勿在 GSC 对已 301 的旧 URL 反复「请求编入索引」。
部署后验收
curl -sL https://doc.erdonline.com/robots.txt | grep Sitemap
curl -sL https://doc.erdonline.com/sitemap.xml | grep doc.erdonline.com | head
curl -sI https://doc.erdonline.com/docs/faq/ # 301 → /docs/guide/intro/
curl -sI https://doc.erdonline.com/docs/guide/intro # 308 → /docs/guide/intro/
curl -sI https://doc.erdonline.com/docs/guide/intro/ # 200
# sitemap <loc> 必须带尾斜杠,且不含 /search
# robots.txt 须含中文 sitemap 与 https://doc.erdonline.com/en/sitemap.xml
主站 SEO / 爬虫索引(www.erdonline.com)
主站 SPA 由 Cloudflare Pages 项目 erdonline-demo 托管(工作流 frontend-demo-site.yml → pages deploy frontend/dist)。构建后 frontend/scripts/gen-seo-static.mjs 在 dist/ 写入:
| 产物 | 作用 |
|---|---|
sitemap.xml | 公开落地路径:/、/compare、/catalog、/demo 及 /en/* 英文镜像(ADR-0034) |
robots.txt | 项目自控(覆盖 CF 注入的默认 robots);含 Sitemap: 绝对 URL;Disallow 登录区/设计器等 |
dist/<path>/index.html | 公开营销路径的独立 SPA 壳(/catalog、/compare、/demo、/en、/en/catalog、/en/compare、/en/demo)。爬虫首屏 <title> / canonical / hreflang 指向该路径,不是首页 Draw-ERD。JSON-LD:/ = WebApplication;/catalog /en/catalog = CollectionPage;官方 /catalog/:id = ItemPage;其余营销页 = WebPage(不得再套首页 WebApplication + url /)。由 PRERENDER_PAGES 驱动。/demo 真人仍会 hydrate 后跳到 /s/public-demo。官方模板另有 dist/catalog/<id>/index.html。未知详情 200 到 /catalog/ 列表壳。无 /en/catalog/:id。 |
_redirects | 仅列尚无静态壳的 SPA pathname 的 200 反代到 /(勿写 /index.html,CF 会 308 到根);禁止把已 prerender 的精确路径(/catalog、/compare、/en…)再写回 /;禁止 /* / 200 通配;禁止 /catalog/*(splat 会匹配 /catalog/ 空段,把列表壳盖成首页)。详情:官方 ID 先 identity-200(含尾斜杠),再 /catalog/:id /catalog/:id/ 200 到 /catalog/;/catalog/_item 301 到 /catalog/(不要 200 到 / 或 _item) |
404.html | 与构建出的根 index.html 同壳;配合 CF「有顶层 404.html 则关闭自动 SPA 模式」,未知路径返回 HTTP 404 |
_headers | CF Pages 响应头;含 sitemap.xml / robots.txt 的 Content-Type |
站点根 URL 由环境变量 ERD_SITE_URL(或 SEO_BASE_URL)决定 ,默认 https://www.erdonline.com。新增公开路由时同步改 frontend/scripts/seo-config.mjs(sitemap 列表 + PRERENDER_PAGES + _redirects + nginx map)。/ 仍用 document.ejs 英文 SERP;hydrate 后 usePageSeo 再改中文。本地门禁:cd frontend && yarn test:seo-static(无 umi);yarn build 后 node scripts/assert-seo-static.mjs。
百度站长验证:平台下发的 baidu_verify_*.html 放 frontend/public/(构建后位于站点根),验证通过后勿删;勿写入 sitemap 或 _redirects SPA 规则。
Cloudflare Pages 行为(已验证官方文档)
- 无顶层
404.html时:Pages 假定 SPA,所有未命中静态文件的路径 200 + index.html(软 404)。 - 有顶层
404.html时:关闭自动 SPA;未匹配静态文件且无_redirects规则的路径 → 404 + 404.html。 - 静态
robots.txt/sitemap.xml在构建产物根目录时 优先于 平台默认 robots 注入。 _redirects中 200 代理仅对显式列出的 pathname 生效;勿恢复 catch-all。
Docker / 自托管 nginx(frontend/nginx.conf):map $uri $spa_fallback 与 seo-config.mjs 对齐;未知路径 return 404 + error_page 404 /404.html(仍渲染 SPA 404 页)。./scripts/verify-self-deploy.sh 会顺带断言 sitemap.xml、robots.txt、未知路径 404、已知 SPA 路由 200。
部署后验收(须等新版本上线后再跑)
# 真 sitemap(Content-Type 含 xml,body 含 <urlset)
curl -sI https://www.erdonline.com/sitemap.xml
curl -sL https://www.erdonline.com/sitemap.xml | head
# 项目 robots(非 CF Content-Signal 模板)
curl -sL https://www.erdonline.com/robots.txt
# 软 404 已修复:未知路径须 404(非 200)
curl -sI https://www.erdonline.com/__seo_health_nonexistent_path__
# 已知公开路由仍 200;首屏 title/canonical 必须是该路径(不是首页 Draw-ERD)
curl -sI https://www.erdonline.com/compare
curl -sI https://www.erdonline.com/catalog
curl -s https://www.erdonline.com/catalog | grep -E '<title>|rel="canonical"'
curl -s https://www.erdonline.com/catalog/demo-authz | grep -E '<title>|rel="canonical"'
curl -s https://www.erdonline.com/en/catalog | grep -E '<title>|rel="canonical"'
curl -sI https://www.erdonline.com/demo
curl -sI https://www.erdonline.com/login
curl -sI 'https://www.erdonline.com/s/public-demo'
# 每日巡检(本地或 CI)
node scripts/seo-index-health.mjs
本地构建验收:cd frontend && yarn test:seo-static;yarn build 后 dist/ 应含 sitemap/robots/_redirects/404.html 以及 catalog/index.html、compare/index.html、en/catalog/index.html 等;node scripts/assert-seo-static.mjs。
MCP(agent / CLI,ADR-0013)
官方远程 Streamable HTTP 入口是 https://api.erdonline.com/mcp。当前 Railway / Docker 后端镜像已内置 Node sidecar,由 Spring 在同一公网端口反代 /mcp;PAT 按请求放在 Authorization: Bearer …,不写入镜像。
从源码使用 stdio fallback:
cd mcp && yarn install && yarn build
export ERD_API_URL=https://your-api.example.com # 或 http://127.0.0.1:9502
export ERD_PAT=erd_pat_… # POST /auth/personal-access-tokens 铸造
node dist/index.js # stdio fallback
# 可选 Streamable HTTP:
# yarn start -- --http # 默认 http://127.0.0.1:3920/mcp
@erdonline/mcp 当前 private: true、未发布 npm,不要宣传 npx @erdonline/mcp。六种客户端的远程配置见 API / MCP 指南。不要把 PAT 写进镜像、compose 默认值、deeplink 或仓库配置。写版本需铸造含 versions:write 的 PAT。