---
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: "server"
    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. 彩色终端输出
