--- author: Peanut created_at: 2026-04-26 updated_at: 2026-04-26 purpose: 设计并优化情绪博物馆跨平台服务管理脚本,修复硬编码路径、中文乱码、依赖等待逻辑等问题 --- # 情绪博物馆 - 跨平台服务管理脚本设计 ## 概述 创建一个 Python 脚本 (`manage.py`) 及配套的 YAML 配置文件 (`manage.conf.yaml`),用于统一管理情绪博物馆项目的多个服务。支持跨平台运行(Windows / Linux / macOS),提供服务编排、依赖管理、健康检查、日志聚合等企业级功能。 ## 架构设计 ### 文件结构 ``` 项目根目录/ ├── manage.py # 主入口脚本 (CLI) ├── manage.conf.yaml # 服务配置文件 └── tools/ └── service_manager.py # 核心服务管理模块 ``` ### 模块职责 | 模块 | 职责 | 依赖 | |------|------|------| | `manage.py` | CLI 入口,参数解析,命令分发 | `argparse`, `tools.service_manager` | | `manage.conf.yaml` | 服务定义、端口、启动命令、依赖关系 | 无 | | `tools/service_manager.py` | 进程管理、健康检查、日志读取、依赖安装 | `psutil`, `subprocess`, `requests`, `pyyaml` | ## 服务配置 ### manage.conf.yaml ```yaml # 服务配置文件 # 定义所有可管理的服务及其属性 services: backend: name: "后端服务" dir: "backend-single" type: "java" port: 19089 health_check_path: "/actuator/health" start_cmd: "mvn spring-boot:run" build_cmd: "mvn clean package -DskipTests" build_check: "target/emotion-single-1.0.0.jar" lock_file: "pom.xml" # 用于检测是否需要重新构建 pid_file: ".pid" # 存储在服务目录内: {service.dir}/.pid log_file: "logs/emotion-single-local.log" depends_on: [] env: SPRING_PROFILES_ACTIVE: "local" web: name: "用户前端" dir: "web" type: "node" port: 5173 health_check_path: "/" start_cmd: "npm run dev" install_cmd: "npm install" install_check: "node_modules" lock_file: "package-lock.json" # 用于检测是否需要重新安装 pid_file: ".pid" # 存储在服务目录内: {service.dir}/.pid depends_on: ["backend"] web-admin: name: "管理后台" dir: "web-admin" type: "node" port: 5174 health_check_path: "/" start_cmd: "npm run dev" install_cmd: "npm install" install_check: "node_modules" lock_file: "package-lock.json" # 用于检测是否需要重新安装 pid_file: ".pid" # 存储在服务目录内: {service.dir}/.pid depends_on: ["backend"] mini-program: name: "小程序 H5" dir: "mini-program" type: "node" port: 5175 health_check_path: "/" start_cmd: "npm run dev:h5" install_cmd: "npm install" install_check: "node_modules" lock_file: "package-lock.json" # 用于检测是否需要重新安装 pid_file: ".pid" # 存储在服务目录内: {service.dir}/.pid depends_on: ["backend"] ``` ## 命令设计 ### CLI 接口 ```bash # 启动服务(service 参数可选,默认为 all) python manage.py start # 启动所有服务(按依赖顺序) python manage.py start backend # 启动后端 python manage.py start web # 启动用户前端 # 停止服务(service 参数可选,默认为 all) python manage.py stop # 停止所有服务(逆依赖顺序) python manage.py stop backend # 停止后端 python manage.py stop all # 显式停止所有服务 # 重启服务(必须指定服务名称) python manage.py restart backend # 重启后端 python manage.py restart all # 重启所有服务 # 查看状态 python manage.py status # 显示所有服务状态 # 查看日志(必须指定服务名称) python manage.py logs backend # 查看后端日志 python manage.py logs backend --follow # 实时跟踪日志 python manage.py logs backend --lines 100 # 查看最近 100 行 # 显示访问地址 python manage.py info # 显示所有服务访问地址 # 其他命令 python manage.py clean backend # 清理后端构建产物 python manage.py setup web # 安装前端依赖 ``` ### 输出格式 ``` ╔══════════════════════════════════════════════════════════════╗ ║ 情绪博物馆 - 服务管理器 ║ ╚══════════════════════════════════════════════════════════════╝ [INFO] 启动服务: 后端服务 (backend) [INFO] 检查依赖... [INFO] 依赖检查通过 [INFO] 启动进程: mvn spring-boot:run [INFO] 等待服务就绪... [INFO] ✅ 后端服务 已启动 (PID: 12345, 端口: 19089) ────────────────────────────────────────────────────────────── 服务地址: 🔌 后端 API: http://localhost:19089/api 📊 WebSocket: ws://localhost:19089/ws ────────────────────────────────────────────────────────────── ``` ## 核心功能实现 ### 1. 跨平台进程管理 - **Windows**: 使用 `subprocess.CREATE_NEW_PROCESS_GROUP` + `taskkill` - **Linux/macOS**: 使用 `os.setsid` + `kill` (SIGTERM → 超时 → SIGKILL) - **PID 管理**: 每个服务目录创建 `.pid` 文件,记录进程 ID ### 2. 依赖检测与安装 启动前检查: - Java 服务:检查 `build_check` 路径是否存在,不存在则执行 `build_cmd` - Node 服务:检查 `install_check` 路径是否存在,不存在则执行 `install_cmd` **依赖更新检测**: - Java 服务:比较 `pom.xml` 和 `build_check` 的修改时间,若 `pom.xml` 更新则重新构建 - Node 服务:比较 `package-lock.json`(或 `package.json`)和 `node_modules` 的修改时间,若依赖文件更新则重新安装 - 配置文件中新增 `lock_file` 字段指定锁文件路径 ### 3. 服务启动顺序编排 根据 `depends_on` 字段构建依赖图,按拓扑排序启动: 1. 先启动无依赖的服务(如 backend) 2. 等待后端健康检查通过后,启动前端服务 3. 并行启动无相互依赖的服务(如 web 和 web-admin) **停止顺序**:启动顺序的逆序(反向拓扑排序) 1. 先停止无下游依赖的服务(如 mini-program → web-admin → web) 2. 最后停止根服务(backend) 3. 每个服务停止后等待 2 秒,确保端口释放 **并发控制**: - 最大并发启动数:3(避免同时启动过多服务导致系统负载过高) - 失败策略:某个服务启动失败时,停止已启动的依赖服务,输出错误信息并终止流程 ### 4. 健康检查 - **HTTP 检查**: 向 `http://localhost:{port}{health_check_path}` 发送 GET 请求 - Java 服务(`/actuator/health`):检查返回 JSON 中 `status` 字段是否为 `UP` - Node 前端服务(`/`):检查 HTTP 状态码是否为 2xx(不检查内容,因为 Vite 返回 HTML) - **端口检查**(备选):使用 `socket` 检查端口是否监听,适用于无 HTTP 健康检查端点的服务 - **超时控制**: 默认等待 60 秒,每 2 秒重试一次 - **日志输出**: 显示等待进度 `[=====> ] 50%` ### 5. 日志聚合 - 读取服务日志文件(如 `logs/emotion-single-local.log`) - `--follow` 模式:类似 `tail -f`,实时输出新日志 - `--lines N`:显示最近 N 行日志 ### 6. 优雅停止 1. 读取 `{service.dir}/.pid` 文件获取进程 ID 2. 发送 SIGTERM(或 Windows 的 `taskkill /PID {pid}`) 3. 等待进程退出(最多 10 秒,每 0.5 秒检查一次) 4. 如果未退出,发送 SIGKILL(或 `taskkill /F /PID {pid}`) 5. 删除 `.pid` 文件 ### 7. 后台进程管理(关键设计) **问题**:`mvn spring-boot:run` 和 `npm run dev` 是前台阻塞命令,直接运行会阻塞主脚本。 **解决方案**:使用 `subprocess.Popen` 实现后台运行 - **输出重定向**:将 stdout 和 stderr 重定向到日志文件 - Java: `{service.dir}/logs/manage-{service}.log` - Node: `{service.dir}/logs/manage-{service}.log` - **进程组创建**: - Windows: `creationflags=subprocess.CREATE_NEW_PROCESS_GROUP` - Linux/macOS: `start_new_session=True`(等同于 `os.setsid`) - **环境变量注入**:通过 `subprocess.Popen` 的 `env` 参数传递 - 合并当前环境变量 + 配置文件中定义的 `env` 字段 - 配置文件的 `env` 覆盖系统环境变量 ### 8. 日志聚合 - 读取服务日志文件(如 `logs/emotion-single-local.log` 或 `logs/manage-{service}.log`) - `--follow` 模式:跨平台实现方案 - 使用文件偏移量轮询(非平台特定的 `tail -f`) - 每 0.5 秒检查文件大小变化,读取新增内容 - 支持 Ctrl+C 优雅退出 - `--lines N`:从文件末尾向前读取 N 行(使用 `seek` 到文件末尾,向前扫描换行符) ### 9. 彩色终端输出 - 使用 `colorama` 实现跨平台彩色输出 - 颜色规范: - 绿色 `[INFO]` - 正常信息 - 黄色 `[WARN]` - 警告信息 - 红色 `[ERROR]` - 错误信息 - 蓝色 `[SECTION]` - 分隔标题 - 青色 `[DEBUG]` - 调试信息 ### 10. 优化修复(2026-04-26) **10.1 中文编码修复** 在 `manage.py` 入口添加 UTF-8 编码配置,修复 Windows 控制台中文乱码: ```python import sys, io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') ``` **10.2 动态可执行文件查找** 移除所有硬编码路径,新增 `_find_executable()` 方法动态查找 Node/Maven/npm 路径: - **Node**: `shutil.which('node.exe')` → `F:\Program Files\nodejs` → `C:\Program Files\nodejs` → `%APPDATA%\npm` - **npm**: `shutil.which('npm.cmd')` → 同 Node 路径 - **Maven**: `shutil.which('mvn.cmd')` → `shutil.which('mvn.bat')` → 常见 Maven 安装路径 **10.3 依赖等待逻辑修复** `_start_all` 中不再等待"已加入启动队列但还未就绪"的服务,改为: 1. 先启动依赖服务(如 backend) 2. 等待其健康检查通过 3. 再启动依赖它的服务(如 web、web-admin) **10.4 restart 支持 all 参数** 将 `restart_parser` 的 `service` 参数改为 `nargs="?"`,允许不传值。代码中处理 `all` 时触发全量重启。 **10.5 clean 增强** 清理范围扩展: - **Node 服务**: `node_modules`、`dist`、`.vite` 目录 - **Java 服务**: `target` 目录(优先使用 `mvn clean`,fallback 为直接删除) **10.6 PID 严格验证** 不仅检查 PID 存在,还检查进程的 `cwd`(工作目录)是否匹配服务目录,防止误判复用 PID 的其他进程。 **10.7 进度条显示稳定** 进度更新增加节流:仅在百分比变化 >= 10% 时刷新终端输出,避免频繁抖动。 **10.8 mini-program 补充 log_file 配置** 在 `manage.conf.yaml` 中为 `mini-program` 服务添加 `log_file` 字段。 ## 配置 Schema 校验 配置文件加载后进行严格校验: **必填字段**(每个服务必须包含): - `name` (string): 服务显示名称 - `dir` (string): 服务目录路径(相对于项目根目录) - `type` (string): 服务类型,仅支持 `java` 或 `node` - `port` (integer): 服务端口号(1-65535) - `start_cmd` (string): 启动命令 **可选字段**(有默认值): - `health_check_path` (string): 默认 `/` - `pid_file` (string): 默认 `.pid` - `depends_on` (list): 默认 `[]` - `env` (dict): 默认 `{}` - `lock_file` (string): 默认 `null`(不检测依赖更新) **条件必填字段**: - Java 服务 (`type: java`): 建议配置 `build_cmd` 和 `build_check` - Node 服务 (`type: node`): 建议配置 `install_cmd` 和 `install_check` **校验失败行为**:输出具体错误信息(如 `backend: port 必须是整数`),终止脚本执行 ## 错误处理 | 场景 | 处理方式 | |------|----------| | 服务已运行 | 提示服务已在运行,显示 PID 和端口 | | 端口被占用 | 检查是否为同一服务,否则报错提示 | | 依赖安装失败 | 输出错误日志,终止启动流程 | | 健康检查超时 | 提示超时,建议查看日志排查 | | 进程异常退出 | 记录退出码,提示查看日志 | | 配置文件缺失 | 使用默认配置或报错 | | Python 依赖缺失 | 提示运行 `pip install -r requirements.txt` | | 配置字段缺失 | 启动时校验配置,缺少必填字段(name, dir, type, port, start_cmd)则报错退出 | | 未知配置字段 | 忽略未知字段,输出警告日志 | ## 技术依赖 ### Python 依赖 (requirements.txt) ``` psutil>=5.9.0 pyyaml>=6.0 requests>=2.28.0 colorama>=0.4.6 ``` ### 系统依赖 - Python 3.8+ - Java 17+ (后端服务) - Node.js 18+ (前端服务) - Maven 3.6+ (后端构建) - npm 9+ (前端依赖) ## 测试方案 ### 单元测试 - 测试配置解析 - 测试依赖图拓扑排序 - 测试跨平台命令生成 - 测试健康检查逻辑 ### 集成测试 - 启动单个服务并验证健康检查 - 启动所有服务并验证依赖顺序 - 停止服务并验证进程清理 - 重启服务并验证状态恢复 ### 手动测试 ```bash # Windows 测试 python manage.py start all python manage.py status python manage.py stop all # Linux/macOS 测试 python3 manage.py start backend python3 manage.py logs backend --follow python3 manage.py restart web ``` ### 9. 彩色终端输出