feat(web): 移除 Docker 与反代部署,改为 Linux 业务服务并自带管理端托管

需求变更:Web 端只保留业务需求,反向代理交由另外的服务处理,
本项目仅提供 IIS / nginx 反代说明文档,不部署反代。

主要改动:
- web-server: 新增管理端静态托管(ADMIN_DIR、SPA 深链接回退),
  反代因此只需一条全匹配转发规则;新增 adminDir 配置项
- deploy/install.sh: 删除 nginx 安装与站点配置逻辑,改为原生 Linux
  服务部署(systemd → OpenRC → SysVinit → 后台进程四级降级),
  打印反代交接指引
- deploy/ctl.sh: 启停/状态/日志脚本
- deploy/proxy/: 新增反代说明文档(README.md、nginx.conf.example、
  web.config.example),标注上传上限与超时必须同步调整
- 删除 deploy/nginx/、web-server/admin/nginx.conf、Dockerfile、
  docker-compose.yml、.dockerignore
- scripts/package-release.sh: 锚定排除规则 + 产物完整性校验
- 文档同步:README.txt、DSH.md、project.yaml、tasks/008、tasks/010
This commit is contained in:
2026-09-15 01:25:19 +08:00
parent 653d9d168c
commit 8c76b0a599
17 changed files with 581 additions and 244 deletions
+13 -2
View File
@@ -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<string>('bindHost') ?? '0.0.0.0';
@@ -45,6 +46,15 @@ export class AppConfig {
return this.config.get<string>('storageDir') ?? resolve(process.cwd(), 'storage');
}
/**
* 管理端静态站点目录。
* 本服务自带管理端页面,反向代理只需把请求整体转发到本端口即可,
* 不需要代理层再单独托管静态文件(见 deploy/proxy/README.md)。
*/
get adminDir(): string {
return this.config.get<string>('adminDir') || resolve(process.cwd(), '..', 'admin');
}
get uploadMaxBytes(): number {
return this.config.get<number>('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 ?? '',
+40 -1
View File
@@ -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<void> {
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<void> {
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)}`);
}