diff --git a/DSH.md b/DSH.md index 798b381..24fb2ef 100644 --- a/DSH.md +++ b/DSH.md @@ -80,12 +80,15 @@ opendance-web/ │ ├── admin/ # React 管理端 │ ├── test/ # 单元测试 + 端到端冒烟 │ └── scripts/ # migrate.mjs / check-contracts.mjs / check-routes.mjs -├── deploy/ # ★ Linux 原生站点部署(不含 MySQL) -│ ├── install.sh # 一键部署(systemd + nginx) +├── deploy/ # ★ 业务服务部署(不含 MySQL、不含反代) +│ ├── install.sh # 一键部署(systemd / OpenRC / SysVinit / 后台进程) │ ├── uninstall.sh +│ ├── ctl.sh # 无 init 系统时的进程控制 │ ├── .env.example │ ├── systemd/opendance-api.service -│ └── nginx/opendance.conf +│ ├── openrc/opendance-api +│ ├── initd/opendance-api +│ └── proxy/ # ★ 反代配置说明(nginx / IIS,仅文档不部署) ├── scripts/package-release.sh # 发布产物打包 └── THIRD_PARTY.md ``` @@ -93,7 +96,9 @@ opendance-web/ ## 6. 硬性约束 1. **不部署 MySQL**。数据库为部署方提供的现有实例,连接信息只经环境变量注入。 - **也不使用 Docker**:以 nginx 站点 + systemd 服务的形式直接跑在 Linux 上。 + **也不使用 Docker**,**也不部署反向代理**:本端只提供业务服务 + (API + 自带的管理端页面),反代由另外的服务负责, + 本项目只在 `deploy/proxy/` 下提供 IIS 与 nginx 的配置说明文档。 2. **`DB_SYNCHRONIZE` 生产必须为 `false`**,所有表结构变更走 migration;每个 migration 必须可逆。 3. **绝不把凭证写入仓库**。`.env` 已在 `.gitignore` 中,只提交 `.env.example`。 diff --git a/README.txt b/README.txt index c5f3ccb..e551bc3 100644 --- a/README.txt +++ b/README.txt @@ -9,18 +9,29 @@ OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 连接信息通过 .env 注入。 ---------------------------------------------------------------- -一、部署到 Linux 服务器(原生站点,不使用 Docker) +一、部署到 Linux 服务器(业务服务,不含反向代理) ---------------------------------------------------------------- -部署形态:一个 nginx 站点同时提供管理端页面与 API。 -Node 进程由 systemd 托管,只监听 127.0.0.1,外部只能经 nginx 访问。 +职责边界 + 本端**只提供业务服务**:NestJS API + 管理端页面。 + **反向代理由另外的服务/团队负责**,本项目不安装、不配置任何反代软件, + 只在 deploy/proxy/ 下提供 IIS 与 nginx 的配置说明文档。 + +部署形态 + Node 业务服务由系统服务托管,同时提供: + / 管理端页面(本服务自带,含 SPA 深链接回退) + /api/ REST API + /health 健康检查 + /static/ 上传文件 + /contracts/ 契约快照 + 因为静态页面也由本服务提供,反向代理**只需一条全匹配转发规则**。 前置要求 - Linux 服务器,有 root 权限(不需要 sudo,直接以 root 执行即可) - Node.js 22 或更高版本 - - nginx(未安装时脚本会跳过站点配置并给出提示) - 进程管理:systemd / OpenRC / SysVinit 均可;**三者都没有也能装** (自动降级为 nohup 后台进程 + PID 文件 + crontab @reboot 开机自启) + - 反向代理:由另外的服务提供,本项目不要求、也不安装 - 一个可连通的现有 MySQL / MariaDB 实例,并已创建数据库与账号: CREATE DATABASE IF NOT EXISTS dpendance @@ -40,24 +51,29 @@ Node 进程由 systemd 托管,只监听 127.0.0.1,外部只能经 nginx 访 sudo bash deploy/install.sh 脚本会依次: - 校验环境与 .env → 创建服务用户与目录 → 安装运行期依赖 → - 执行 TypeORM 迁移 → 注册并启动 systemd 服务 → 等待健康检查 → - 配置 nginx 站点并重载 + 校验环境与 .env → 创建服务用户与目录 → 复制程序文件 → + 安装运行期依赖 → 执行 TypeORM 迁移 → + 注册并启动系统服务(systemd / OpenRC / SysVinit / 后台进程)→ 等待健康检查 -部署完成后( 为服务器 IP) - 管理端首页: http:/// - API 前缀: http:///api - 健康检查: http:///health - 契约快照: http:///contracts/ +部署完成后(直接访问后端端口, 为服务器 IP) + 管理端首页: http://:3000/ + API 前缀: http://:3000/api + 健康检查: http://:3000/health + 契约快照: http://:3000/contracts/ + +对外访问 + 由反向代理把请求转发到 http://<后端地址>:3000 即可。 + 配置样例与说明: + deploy/proxy/nginx.conf.example nginx 站点配置 + deploy/proxy/web.config.example IIS(ARR + URL Rewrite)配置 + deploy/proxy/README.md 完整说明、自检清单与常见故障 可覆盖的安装参数 APP_DIR=/opt/opendance 程序目录 SERVICE_USER=opendance 运行服务的系统用户 STORAGE_DIR=/var/lib/opendance/storage 上传文件目录 - SERVER_NAME=example.com nginx server_name - SKIP_NGINX=1 只装 API 服务,不配置 nginx - 例:sudo SERVER_NAME=dance.example.com bash deploy/install.sh + 例:APP_DIR=/srv/opendance bash deploy/install.sh 升级 解压新版本后重新执行 sudo bash deploy/install.sh 即可(会覆盖程序文件, @@ -85,8 +101,7 @@ Node 进程由 systemd 托管,只监听 127.0.0.1,外部只能经 nginx 访 无 init 系统(降级模式): bash /opt/opendance/deploy/ctl.sh status | restart | logs - nginx 配置校验: - nginx -t && (systemctl reload nginx || rc-service nginx reload || nginx -s reload) + 反代配置:不在本机,无需在此维护 ---------------------------------------------------------------- 二、本地开发 @@ -131,9 +146,14 @@ Node 进程由 systemd 托管,只监听 127.0.0.1,外部只能经 nginx 访 DB_LOGGING 是否打印 SQL(默认 false) PORT API 监听端口(默认 3000) - HOST API 监听地址(原生部署用 127.0.0.1,仅由 nginx 反代访问) + HOST 监听地址。反代与后端同机用 127.0.0.1(推荐); + 反代在别的机器用 0.0.0.0,并用防火墙限制来源 + ADMIN_DIR 管理端静态站点目录(默认 ../admin,即 /opt/opendance/admin) STORAGE_DIR 上传文件存储目录(原生部署默认 /var/lib/opendance/storage) - UPLOAD_MAX_BYTES 单文件上限,默认 536870912(512 MiB) + UPLOAD_MAX_BYTES 单文件上限,默认 536870912(512 MiB)。 + ★ 反向代理层的同名限制必须不小于此值,否则大模型上传 + 会在代理层被拒(nginx client_max_body_size / + IIS requestLimits),详见 deploy/proxy/README.md ASSIMP_BIN 可选:Assimp 可执行文件,FBX/glTF 动作预处理需要 CONTRACTS_DIR 可选:契约目录,设置后 /health 反映实时契约版本 (原生部署默认指向 /opt/opendance/contracts) @@ -177,7 +197,8 @@ Node 进程由 systemd 托管,只监听 127.0.0.1,外部只能经 nginx 访 contracts/ 跨端契约(权威,改接口先改这里) web-server/ NestJS 服务端源码 web-server/admin/ React 管理端 - deploy/ Linux 原生站点部署(nginx + systemd,不含 MySQL) + deploy/ 业务服务部署(系统服务,不含 MySQL、不含反向代理) + deploy/proxy/ 反向代理配置说明(nginx / IIS,仅供参考不部署) THIRD_PARTY.md 第三方开源项目声明 ---------------------------------------------------------------- @@ -186,7 +207,7 @@ Node 进程由 systemd 托管,只监听 127.0.0.1,外部只能经 nginx 访 1. .env 已被 .gitignore 忽略,禁止提交;仓库中只保留 .env.example。 2. 数据库账号使用最小权限,不授予 GRANT OPTION / FILE / SUPER。 -3. 本版本未内置管理端登录认证,请部署在内网,或在 nginx 层加 Basic Auth / SSO。 +3. 本版本未内置管理端登录认证,请部署在内网,或在反向代理层加 Basic Auth / SSO。 4. 上传的模型与动作必须填写来源五元组并通过审核后才对客户端可见。 5. 未使用 SMPL 拓扑,统一 MediaPipe 33 点,规避非商用许可限制。 diff --git a/deploy/.env.example b/deploy/.env.example index 5a2f225..aa0db3a 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -8,8 +8,13 @@ # ---- 服务 ---- NODE_ENV=production PORT=3000 -# 仅监听本机,由 nginx 反向代理对外提供服务(不要直接暴露 Node 进程) -HOST=127.0.0.1 +# 监听地址: +# 反代与后端同机 → 127.0.0.1(后端不直接对外暴露,推荐) +# 反代在别的机器 → 0.0.0.0,并用防火墙把该端口限制为仅反代来源可访问 +HOST=0.0.0.0 + +# 管理端静态站点目录(本服务自带管理端页面,反代只需整体转发到本端口) +ADMIN_DIR=/opt/opendance/admin # ---- 现有 MySQL / MariaDB 实例(本项目不安装、不管理数据库)---- DB_HOST= @@ -24,7 +29,9 @@ DB_LOGGING=false # ---- 文件存储 ---- # 需与 install.sh 的 STORAGE_DIR 一致,且服务用户可写 STORAGE_DIR=/var/lib/opendance/storage -# 单文件上传上限(字节),默认 512 MiB;nginx 的 client_max_body_size 需不小于此值 +# 单文件上传上限(字节),默认 512 MiB。 +# ★ 反向代理层的同名限制必须不小于此值(nginx client_max_body_size / +# IIS requestLimits),否则大模型上传会在代理层被拒(413)。 UPLOAD_MAX_BYTES=536870912 # ---- 可选的 Assimp(FBX/glTF 动作预处理)---- diff --git a/deploy/install.sh b/deploy/install.sh index 7ce76f5..a70813d 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -2,9 +2,9 @@ # ============================================================ # OpenDance Studio Web 服务端 —— Linux 原生站点部署脚本 # -# 部署形态(不使用 Docker): -# nginx ── 静态托管管理端 + 反向代理 API -# └──▶ systemd 托管的 Node 进程(NestJS,仅监听 127.0.0.1) +# 部署形态(不使用 Docker、不部署反向代理): +# 本脚本只负责把 NestJS 业务服务跑起来(systemd / OpenRC / SysVinit / 后台进程)。 +# 该服务自带管理端页面,反向代理由另外的服务负责,配置说明见 deploy/proxy/。 # # 数据库仍是部署方提供的**现有 MySQL / MariaDB 实例**,本脚本不安装、不管理数据库。 # @@ -16,8 +16,7 @@ # APP_DIR 安装目录(默认 /opt/opendance) # SERVICE_USER 运行服务的系统用户(默认 opendance) # STORAGE_DIR 上传文件目录(默认 /var/lib/opendance/storage) -# SERVER_NAME nginx server_name(默认 _) -# SKIP_NGINX=1 只装 API 服务,不配置 nginx +# PORT API 监听端口(默认 3000) # ============================================================ set -euo pipefail # 失败时打印出错行号,避免只看到半截输出 @@ -29,7 +28,6 @@ cd "$SRC_DIR" APP_DIR="${APP_DIR:-/opt/opendance}" SERVICE_USER="${SERVICE_USER:-opendance}" STORAGE_DIR="${STORAGE_DIR:-/var/lib/opendance/storage}" -SERVER_NAME="${SERVER_NAME:-_}" API_HOST="127.0.0.1" API_PORT="${PORT:-3000}" @@ -88,7 +86,7 @@ http_ok() { } # ---------- 0. 前置检查 ---------- -[ "$(id -u)" -eq 0 ] || die "需要 root 权限(安装 systemd 服务与 nginx 站点)。 +[ "$(id -u)" -eq 0 ] || die "需要 root 权限(安装系统服务、创建服务用户)。 请以 root 身份执行,例如: su -c 'bash deploy/install.sh' 或 sudo bash deploy/install.sh" # 缺少 deploy/.env 时,若安装目录里已有上次安装留下的配置,直接复用。 @@ -385,46 +383,31 @@ if [ "$READY" -ne 1 ]; then fi log "API 已就绪:$(http_get "http://${API_HOST}:${API_PORT}/health" | head -c 200)" -# ---------- 5. nginx ---------- -if [ "${SKIP_NGINX:-0}" = "1" ]; then - warn "SKIP_NGINX=1,跳过 nginx 配置。API 仅在 ${API_HOST}:${API_PORT} 提供。" -else - if ! command -v nginx >/dev/null 2>&1; then - warn "未安装 nginx,跳过站点配置。" - warn " 安装后重新执行本脚本,或手动把 deploy/nginx/opendance.conf 放到 nginx 配置目录。" - else - log "配置 nginx 站点" - NGINX_AVAILABLE=/etc/nginx/sites-available - NGINX_ENABLED=/etc/nginx/sites-enabled - # RHEL 系发行版没有 sites-available 约定 - if [ ! -d "$NGINX_AVAILABLE" ]; then - NGINX_AVAILABLE=/etc/nginx/conf.d - NGINX_ENABLED=/etc/nginx/conf.d - fi +# ---------- 5. 反向代理(本脚本**不部署**,仅给出说明) ---------- +# 需求变更:Web 端只负责业务服务,反向代理由另外的服务/团队提供。 +# 本服务已自带管理端页面,因此反代只需把请求**整体转发**到本端口, +# 无需在代理层再托管静态文件、也无需配置 SPA 回退(服务端已处理)。 +cat < "$DEST" +------------------------------------------------------------ +反向代理说明(本脚本不部署反代) +------------------------------------------------------------ +本服务已同时提供 API 与管理端页面,反代只需将请求整体转发至: + http://<本机地址>:${API_PORT} - if [ "$NGINX_ENABLED" != "$NGINX_AVAILABLE" ]; then - ln -sf "$DEST" "$NGINX_ENABLED/opendance.conf" - if [ -e "$NGINX_ENABLED/default" ]; then - rm -f "$NGINX_ENABLED/default" - log "已移除 nginx 默认站点(避免抢占 80 端口)" - fi - fi + 健康检查(可用于反代的存活探测):http://<本机地址>:${API_PORT}/health + 上传上限需在代理层同步放开(本服务为 $(( ${UPLOAD_MAX_BYTES:-536870912} / 1024 / 1024 )) MiB) - if nginx -t; then - systemctl reload nginx 2>/dev/null || systemctl restart nginx 2>/dev/null || nginx -s reload - log "nginx 已重载" - else - die "nginx 配置校验失败,请检查 $DEST" - fi - fi -fi + 配置样例与完整说明: + deploy/proxy/nginx.conf.example nginx 站点配置 + deploy/proxy/web.config.example IIS(ARR + URL Rewrite)配置 + deploy/proxy/README.md IIS / nginx 反代说明文档 + + 注意:若反代与后端不在同一台机器,需保持 HOST=0.0.0.0 并用防火墙限制来源; + 若在同一台机器,建议把 HOST 改为 127.0.0.1。 +------------------------------------------------------------ + +EOF # ---------- 6. 完成 ---------- HOST_HINT="${DEPLOY_HOST:-<服务器IP>}" @@ -456,11 +439,13 @@ esac cat < **职责边界**:本 Web 端**只提供业务服务**(NestJS API + 管理端页面), +> **不部署、不配置反向代理**。反向代理由另外的服务或团队负责,本文档只提供配置说明。 +> +> 安装脚本 `deploy/install.sh` 不会安装或修改任何反代软件。 + +## 一、需要转发的目标 + +本服务**自带管理端页面**,因此反向代理**只需把请求整体转发**到一个地址: + +``` +http://<后端地址>:3000 +``` + +不需要在代理层单独托管静态文件,也不需要配置 SPA 回退(服务端已处理)。 +一个 `location /`(nginx)或一条全匹配规则(IIS)即可。 + +| 路径 | 内容 | 说明 | +|---|---|---| +| `/` | 管理端页面 | 由 Node 服务提供;前端路由深链接(如 `/songs`)由服务端回退到 `index.html` | +| `/assets/*` | 前端静态资源 | 带内容哈希,可长缓存 | +| `/api/*` | REST API | 契约见 `contracts/rest-api.yaml` | +| `/health` | 健康检查 | 可用作反代的存活探测,数据库不可用时返回 **503** | +| `/static/*` | 上传文件 | 模型 / 音频 / 关键点序列 | +| `/contracts/*` | 契约快照 | 只读,便于另两端查阅 | + +> Windows 客户端与 Android 之间的**关键点 WebSocket 链路不经过本服务**, +> 反代无需配置 WebSocket 转发。 + +## 二、后端绑定地址怎么选 + +`deploy/.env` 中的 `HOST`: + +| 部署形态 | 建议值 | 说明 | +|---|---|---| +| 反代与后端**同机** | `127.0.0.1` | 后端不直接对外暴露,最安全 | +| 反代与后端**不同机** | `0.0.0.0` | 必须用防火墙把 3000 端口限制为仅反代来源可访问 | + +改完执行 `systemctl restart opendance-api`(或对应的 `ctl.sh restart`)生效。 + +## 三、必须同步调整的两项 + +1. **上传上限**:本服务默认允许 512 MiB(`UPLOAD_MAX_BYTES`)。 + 代理层默认值通常远小于此(nginx 1 MB、IIS 约 28.6 MB),不改会导致大模型上传失败。 +2. **超时**:上传大文件与下载关键点序列可能较慢,代理的读写超时必须放大。 + +## 四、nginx 配置 + +完整样例见 [`nginx.conf.example`](nginx.conf.example)。核心内容: + +```nginx +upstream opendance_api { + server 127.0.0.1:3000; + keepalive 16; +} + +server { + listen 80; + server_name your.domain.com; + + client_max_body_size 512m; # ← 必须放开,否则 VRM/FBX 上传失败 + client_body_timeout 300s; + + location / { + proxy_pass http://opendance_api; + 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; + proxy_set_header Connection ""; + + proxy_connect_timeout 30s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + proxy_request_buffering off; # 大文件直接透传,不在代理落盘 + } +} +``` + +安装位置与重载: + +```bash +# Debian / Ubuntu +cp nginx.conf.example /etc/nginx/sites-available/opendance.conf +ln -s /etc/nginx/sites-available/opendance.conf /etc/nginx/sites-enabled/ +nginx -t && systemctl reload nginx + +# RHEL 系 +cp nginx.conf.example /etc/nginx/conf.d/opendance.conf +nginx -t && systemctl reload nginx +``` + +## 五、IIS 配置 + +IIS 需要先安装两个模块(**默认都没有**): + +1. **URL Rewrite** — https://www.iis.net/downloads/microsoft/url-rewrite +2. **Application Request Routing (ARR)** — https://www.iis.net/downloads/microsoft/application-request-routing + +装完 ARR 后还需启用代理(否则会报 404.4): + +``` +IIS 管理器 → 选中服务器节点 → Application Request Routing Cache + → 右侧 Server Proxy Settings → 勾选 Enable proxy → 应用 +``` + +完整 `web.config` 样例见 [`web.config.example`](web.config.example),放到站点根目录即可: + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +### IIS 容易踩的三个坑 + +1. **`requestLimits` 与 `serverRuntime` 要一起改**。只改前者,SSL 场景下仍会在 + 读取请求体时失败(`uploadReadAheadSize` 默认仅 48 KB)。 +2. **代理超时改的是服务器级配置**,不是站点级。ARR 默认 120 秒,上传大文件会 + 中断。在 `applicationHost.config` 的 `` 节点调整: + ```xml + + ``` +3. **`preserveHostHeader`** 需与 ARR 的 "Preserve host header" 设置一致, + 否则后端收到的 Host 会变成 `127.0.0.1:3000`,影响日志与重定向。 + +## 六、HTTPS 与访问控制(可选) + +- **HTTPS 终止在反代**:本服务只监听 HTTP,证书配置在 nginx/IIS 侧即可。 +- **访问控制**:本版本**未内置登录认证**,如需公网访问,请在反代层加 + Basic Auth、IP 白名单或对接已有 SSO。未加任何防护时请只在内网开放。 + +## 七、自检清单 + +配置完成后逐项确认: + +```bash +# 1. 后端自身可用(绕过反代) +curl -i http://127.0.0.1:3000/health + +# 2. 经反代可访问 +curl -i http://<反代地址>/health + +# 3. 管理端页面可打开(应返回 HTML) +curl -i http://<反代地址>/ | head -5 + +# 4. 前端深链接可用(应返回同一份 HTML,而不是 404) +curl -i http://<反代地址>/songs | head -5 + +# 5. API 可用 +curl -i http://<反代地址>/api/catalog/version + +# 6. 上传上限已放开:找一个小于 UPLOAD_MAX_BYTES 的文件试传 +# 若返回 413 说明代理层限制没改 +``` + +| 现象 | 原因 | +|---|---| +| 反代 502 / 504 | 后端未启动,或 `HOST` 绑定地址不对(同机应为 `127.0.0.1`,异机应为 `0.0.0.0`) | +| 页面能开、接口 404 | 只转发了 `/`,需要把 `/api/`、`/health`、`/static/` 一并转发(最简单是全匹配) | +| 上传大文件 413 | 代理层上传上限没放开(见第三、四、五节) | +| 上传中途断开 | 代理超时太短 | +| `/songs` 直接访问 404 | SPA 回退被代理拦截;本服务已处理,代理**不要**自己加 `try_files`,全匹配转发即可 | diff --git a/deploy/proxy/nginx.conf.example b/deploy/proxy/nginx.conf.example new file mode 100644 index 0000000..1d14419 --- /dev/null +++ b/deploy/proxy/nginx.conf.example @@ -0,0 +1,122 @@ +# ============================================================ +# OpenDance Studio Web 服务端 —— nginx 反向代理样例 +# +# 说明:本 Web 端不自带反向代理。本文件是**给你的反代服务参考**的配置样例, +# 由部署 `deploy/install.sh` 之外的反代负责人使用。 +# +# 特点:后端(NestJS)自带管理端页面与 SPA 回退,因此这里 +# 只需一个全匹配 location 把请求整体转发过去即可, +# 不需要 root/try_files 托管静态文件。 +# +# 安装: +# Debian/Ubuntu: /etc/nginx/sites-available/opendance.conf(再软链到 sites-enabled/) +# RHEL 系: /etc/nginx/conf.d/opendance.conf +# 校验并重载: nginx -t && systemctl reload nginx +# ============================================================ + +upstream opendance_api { + # 后端地址:同机用 127.0.0.1;异机改成实际 IP,并在后端放防火墙 + server 127.0.0.1:3000; + keepalive 16; +} + +server { + listen 80; + listen [::]:80; + server_name your.domain.com; # 改成实际域名或 _ + + # 字符集:管理端与 API 都是 UTF-8 + charset utf-8; + + # ★ 上传上限:后端默认允许 512 MiB,nginx 默认仅 1 MB, + # 不改会导致 VRM / FBX 等大文件上传直接 413。 + client_max_body_size 512m; + client_body_timeout 300s; + + access_log /var/log/nginx/opendance.access.log; + error_log /var/log/nginx/opendance.error.log; + + # ---- 全部请求转发给后端 ---- + # 管理端页面、前端路由深链接、/api、/health、/static、/contracts + # 全部由后端处理,所以这里只需一条规则。 + location / { + proxy_pass http://opendance_api; + 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; + # 配合 upstream keepalive,避免 Connection: close + proxy_set_header Connection ""; + + # ★ 超时:大文件上传与关键点序列下载都可能较慢 + proxy_connect_timeout 30s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + # 大文件直接透传,不在代理层落盘 + proxy_request_buffering off; + proxy_buffering off; + } + + # 健康检查可单独放开日志噪音(可选) + location = /health { + proxy_pass http://opendance_api/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + access_log off; + } + + gzip on; + gzip_types text/plain text/css application/json application/javascript + application/x-ndjson text/xml application/xml image/svg+xml; + gzip_min_length 1024; + + # 若前端资源由后端返回,代理层不要缓存 HTML,避免升级后页面不更新 + location ~* \.html$ { + proxy_pass http://opendance_api; + proxy_http_version 1.1; + proxy_set_header Host $host; + add_header Cache-Control "no-cache"; + } +} + +# ============================================================ +# 可选:HTTPS 终止在反代(后端只监听 HTTP) +# +# server { +# listen 443 ssl; +# http2 on; +# server_name your.domain.com; +# +# ssl_certificate /etc/nginx/ssl/opendance.crt; +# ssl_certificate_key /etc/nginx/ssl/opendance.key; +# ssl_protocols TLSv1.2 TLSv1.3; +# +# client_max_body_size 512m; +# +# location / { +# proxy_pass http://opendance_api; +# 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; +# proxy_send_timeout 300s; +# proxy_read_timeout 300s; +# proxy_request_buffering off; +# } +# } +# ============================================================ + +# ============================================================ +# 可选:本服务未内置登录认证,如需公网访问请在反代加一层防护 +# +# location / { +# auth_basic "OpenDance Studio"; +# auth_basic_user_file /etc/nginx/.htpasswd; # htpasswd -c 生成 +# proxy_pass http://opendance_api; +# ... +# } +# ============================================================ diff --git a/deploy/proxy/web.config.example b/deploy/proxy/web.config.example new file mode 100644 index 0000000..690e636 --- /dev/null +++ b/deploy/proxy/web.config.example @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project.yaml b/project.yaml index 52eef43..801dd16 100644 --- a/project.yaml +++ b/project.yaml @@ -61,11 +61,12 @@ tasks: deliverable: artifact: opendance-web-v0.1.0.tar.gz - packaging: Linux 原生站点(nginx + systemd,不含 MySQL,不使用 Docker) + packaging: Linux 业务服务(系统服务托管,不含 MySQL、不含反向代理、不使用 Docker) includes: - web-server/ # NestJS 源码 + 构建产物 - admin/ # React 管理端构建产物 - - deploy/ # install.sh / nginx / systemd / .env.example + - deploy/ # install.sh / systemd / openrc / initd / ctl.sh / .env.example + - deploy/proxy/ # 反向代理说明(nginx / IIS,仅供参考不部署) - contracts/ # 当前版本契约快照 - THIRD_PARTY.md # 开源声明 - README.txt # 使用说明 diff --git a/scripts/package-release.sh b/scripts/package-release.sh index 303cb8c..3185119 100755 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -112,7 +112,9 @@ for required in \ "opendance-web-v${VERSION}/admin/index.html" \ "opendance-web-v${VERSION}/deploy/install.sh" \ "opendance-web-v${VERSION}/deploy/ctl.sh" \ - "opendance-web-v${VERSION}/deploy/nginx/opendance.conf" \ + "opendance-web-v${VERSION}/deploy/proxy/README.md" \ + "opendance-web-v${VERSION}/deploy/proxy/nginx.conf.example" \ + "opendance-web-v${VERSION}/deploy/proxy/web.config.example" \ "opendance-web-v${VERSION}/deploy/systemd/opendance-api.service" \ "opendance-web-v${VERSION}/contracts/VERSION" \ "opendance-web-v${VERSION}/THIRD_PARTY.md" \ diff --git a/tasks/008-admin-ui-songs.md b/tasks/008-admin-ui-songs.md index 9132979..b13a014 100644 --- a/tasks/008-admin-ui-songs.md +++ b/tasks/008-admin-ui-songs.md @@ -49,5 +49,5 @@ npm run build # tsc -b && vite build 必须成功 ## 构建与运行 - 开发:`npm run dev`(5173,`/api` 与 `/health` 代理到 `localhost:3000`) -- 生产:`npm run build` 产物由 nginx 直接托管在同一站点的根路径下,`/api` 反代到本机 Node 进程(见 `deploy/nginx/opendance.conf`) +- 生产:`npm run build` 产物由**后端服务自身**托管(`ADMIN_DIR`,含 SPA 回退),反向代理只需整体转发到后端端口(见 `deploy/proxy/README.md`) - `VITE_API_BASE` 可覆盖 API 基地址;留空表示同源 diff --git a/tasks/010-deploy.md b/tasks/010-deploy.md index 4e27a93..bf110cb 100644 --- a/tasks/010-deploy.md +++ b/tasks/010-deploy.md @@ -23,26 +23,34 @@ deploy/install.sh deploy/uninstall.sh deploy/.env.example deploy/systemd/opendance-api.service -deploy/nginx/opendance.conf +deploy/proxy/README.md ``` ## 部署形态 +**职责边界**:本端只提供**业务服务**;反向代理由另外的服务/团队负责。 + ``` -浏览器 ──HTTP:80──▶ nginx 站点 - ├── / 管理端静态站点(React 构建产物) - ├── /assets/ 带哈希的静态资源(长缓存) - ├── /api/ ─┐ - ├── /health ─┼─▶ 反向代理 127.0.0.1:3000 - └── /static/─┘ │ - ▼ - systemd: opendance-api - (Node 进程,仅监听本机) - │ - ▼ - 现有 MySQL / MariaDB 实例 +浏览器 ──▶ 反向代理(外部提供,IIS / nginx / 网关…) + │ 整体转发,不需要托管静态文件 + ▼ + Node 业务服务 :3000 + ├── / 管理端页面(本服务自带,含 SPA 深链接回退) + ├── /assets/ 前端静态资源 + ├── /api/ REST API + ├── /health 健康检查 + ├── /static/ 上传文件 + └── /contracts/ 契约快照 + │ + ▼ + 现有 MySQL / MariaDB 实例 ``` +本服务同时提供 API 与管理端页面(`ADMIN_DIR`),因此反代**只需一条全匹配转发规则**, +既不用在代理层托管静态文件,也不用配置 SPA 回退。 + +`deploy/install.sh` **不安装、不配置任何反向代理软件**。 + ## 交付物 | 文件 | 说明 | @@ -54,7 +62,7 @@ deploy/nginx/opendance.conf | `deploy/openrc/opendance-api` | OpenRC 服务脚本 | | `deploy/initd/opendance-api` | SysVinit / LSB 启动脚本(转调 ctl.sh) | | `deploy/ctl.sh` | 无 init 系统时的进程控制(nohup + PID 文件),start/stop/restart/status/logs | -| `deploy/nginx/opendance.conf` | 站点配置:SPA 回退、API 反代、上传上限 512 MiB、gzip | +| `deploy/proxy/README.md` | 站点配置:SPA 回退、API 反代、上传上限 512 MiB、gzip | | `web-server/admin/nginx.conf` | 管理端容器内配置(保留给需要独立静态托管的场景) | ## 进程管理:四层降级 diff --git a/web-server/.env.example b/web-server/.env.example index 385a7e6..1b95885 100644 --- a/web-server/.env.example +++ b/web-server/.env.example @@ -7,9 +7,13 @@ NODE_ENV=production PORT=3000 # 监听地址。本地开发用 0.0.0.0(便于同网段设备联调); -# 生产原生部署应设为 127.0.0.1,仅由 nginx 反向代理访问 +# 生产环境若反代与后端同机,建议改为 127.0.0.1 HOST=0.0.0.0 +# 管理端静态站点目录(默认取 ../admin)。本服务自带管理端页面, +# 反向代理只需把请求整体转发到本端口,见 deploy/proxy/README.md +ADMIN_DIR=../admin + # ---- 现有 MySQL 实例(外部提供,本项目不部署 MySQL)---- # 当前部署:MariaDB 10.11 @ 192.168.150.10 DB_HOST=192.168.150.10 diff --git a/web-server/admin/README.md b/web-server/admin/README.md index 386b0e2..77f90f7 100644 --- a/web-server/admin/README.md +++ b/web-server/admin/README.md @@ -61,24 +61,21 @@ npm run build # 产物在 dist/ `vite.config.ts` 使用 `base: './'`,产物使用相对路径引用,可挂在任意子路径下。 -**生产部署(Linux 原生站点,不使用 Docker)** +**生产部署(业务服务自带管理端,不需要反代托管静态文件)** -构建产物 `dist/` 由同一站点的 nginx 直接托管,API 由 nginx 反向代理到本机 Node 进程。 -整个过程由仓库根目录的 `deploy/install.sh` 一键完成: +构建产物 `dist/` 会被安装到 `$APP_DIR/admin`,并由 **NestJS 服务自身**托管 +(含 SPA 深链接回退)。因此反向代理只需要把请求**整体转发**到后端端口即可, +不需要在代理层再配置静态站点或 `try_files`。 ```bash -# 仓库根目录 -sudo bash deploy/install.sh +# 仓库根目录,一键部署后端(含管理端) +bash deploy/install.sh ``` -对应的 nginx 站点配置见 `deploy/nginx/opendance.conf`: - -- `root` 指向安装后的 `admin/` 目录,`try_files $uri $uri/ /index.html` 支持 SPA 路由; -- `/api/`、`/health`、`/static/` 反向代理到 `127.0.0.1:3000`(Node 只监听回环); -- `client_max_body_size 512m` 与后端 `UPLOAD_MAX_BYTES` 保持一致。 - -本目录下的 `nginx.conf` 仅用于「把管理端单独托管到另一个 nginx 实例」的场景, -其中的 `proxy_pass http://api:3000` 需要按实际情况改成后端地址。 +- 后端通过 `ADMIN_DIR` 定位管理端产物,默认 `../admin` +- 反代配置样例:`deploy/proxy/nginx.conf.example`(nginx)、 + `deploy/proxy/web.config.example`(IIS) +- 完整说明:`deploy/proxy/README.md` ## 目录结构 diff --git a/web-server/admin/nginx.conf b/web-server/admin/nginx.conf deleted file mode 100644 index 6d626da..0000000 --- a/web-server/admin/nginx.conf +++ /dev/null @@ -1,39 +0,0 @@ -server { - listen 80; - server_name _; - - root /usr/share/nginx/html; - index index.html; - - gzip on; - gzip_types text/plain text/css application/javascript application/json image/svg+xml; - gzip_min_length 1024; - - # SPA 路由回退 - location / { - try_files $uri $uri/ /index.html; - } - - # 上传大文件(模型 / 动作)时不要过早断开 - client_max_body_size 512m; - - location /api/ { - proxy_pass http://api:3000; - 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; - proxy_read_timeout 600s; - proxy_send_timeout 600s; - } - - location /health { - proxy_pass http://api:3000; - 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; - } -} diff --git a/web-server/src/config/configuration.ts b/web-server/src/config/configuration.ts index 3e3bb85..ae0753d 100644 --- a/web-server/src/config/configuration.ts +++ b/web-server/src/config/configuration.ts @@ -34,8 +34,9 @@ export class AppConfig { } /** - * 监听地址。原生部署(nginx 前置)时设为 127.0.0.1, - * 仅允许本机反代访问,不直接暴露 Node 进程。 + * 监听地址。 + * 反向代理与后端同机时建议设为 127.0.0.1(仅本机可访问); + * 代理在其他机器上时必须为 0.0.0.0,并用防火墙限制来源。 */ get bindHost(): string { return this.config.get('bindHost') ?? '0.0.0.0'; @@ -45,6 +46,15 @@ export class AppConfig { return this.config.get('storageDir') ?? resolve(process.cwd(), 'storage'); } + /** + * 管理端静态站点目录。 + * 本服务自带管理端页面,反向代理只需把请求整体转发到本端口即可, + * 不需要代理层再单独托管静态文件(见 deploy/proxy/README.md)。 + */ + get adminDir(): string { + return this.config.get('adminDir') || resolve(process.cwd(), '..', 'admin'); + } + get uploadMaxBytes(): number { return this.config.get('uploadMaxBytes') ?? 512 * 1024 * 1024; } @@ -85,6 +95,7 @@ export const configuration = () => ({ port: Number(process.env.PORT ?? 3000), bindHost: process.env.HOST ?? '0.0.0.0', storageDir: process.env.STORAGE_DIR ?? resolve(process.cwd(), 'storage'), + adminDir: process.env.ADMIN_DIR ?? resolve(process.cwd(), '..', 'admin'), uploadMaxBytes: Number(process.env.UPLOAD_MAX_BYTES ?? 512 * 1024 * 1024), assimpBin: process.env.ASSIMP_BIN ?? 'assimp', contractsDir: process.env.CONTRACTS_DIR ?? '', diff --git a/web-server/src/main.ts b/web-server/src/main.ts index 930f785..8208ee7 100644 --- a/web-server/src/main.ts +++ b/web-server/src/main.ts @@ -1,8 +1,9 @@ import { Logger, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; +import { NextFunction, Request, Response } from 'express'; import { existsSync, mkdirSync } from 'node:fs'; -import { resolve } from 'node:path'; +import { extname, join, resolve } from 'node:path'; import { AppModule } from './app.module'; import { AppConfig, configuration, validateEnv } from './config/configuration'; @@ -51,6 +52,39 @@ async function bootstrap(): Promise { app.useStaticAssets(resolve(config.contractsDir), { prefix: '/contracts/', index: false }); } + // ---- 管理端静态站点 ---- + // 本服务同时提供管理端页面,因此反向代理只需要把全部请求转发到本端口, + // 不需要在代理层再单独配置一个静态站点(IIS / nginx 示例见 deploy/proxy/)。 + const adminDir = resolve(config.adminDir); + const adminIndex = join(adminDir, 'index.html'); + if (existsSync(adminIndex)) { + app.useStaticAssets(adminDir, { index: 'index.html', fallthrough: true }); + + // SPA 深链接回退:/songs、/models 这类前端路由在磁盘上没有对应文件, + // 需要返回 index.html 交给前端路由处理。 + // 注意必须放行后端路径,否则会吞掉 API 请求。 + app.use((req: Request, res: Response, next: NextFunction) => { + if (req.method !== 'GET' && req.method !== 'HEAD') return next(); + const path = req.path; + if ( + path.startsWith('/api/') || + path === '/health' || + path.startsWith('/static/') || + path.startsWith('/contracts/') + ) { + return next(); + } + // 带扩展名的请求视为静态资源,未命中就让它 404,不要回退成 HTML + if (extname(path) !== '') return next(); + res.sendFile(adminIndex); + }); + } else { + logger.warn( + `未找到管理端构建产物(${adminIndex}),本服务仅提供 API。` + + '如需管理界面,请部署 admin/ 目录并设置 ADMIN_DIR。', + ); + } + app.enableShutdownHooks(); const db = config.db; @@ -60,6 +94,11 @@ async function bootstrap(): Promise { logger.log(`健康检查:http://${config.bindHost}:${config.port}/health`); logger.log(`数据库:${db.host}:${db.port}/${db.name}(现有实例,本服务不管理 MySQL)`); logger.log(`文件存储:${storageDir}`); + logger.log( + existsSync(adminIndex) + ? `管理端站点:http://${config.bindHost}:${config.port}/(由本服务直接提供)` + : '管理端站点:未部署(仅 API)', + ); logger.log(`契约版本:${JSON.stringify(config.contractVersions)}`); }