feat:CISD配置初始化
This commit is contained in:
@@ -15,9 +15,12 @@ cd tms-framework
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
## Jar Deployment (External Config)
|
||||
## Jar 包部署(外置配置)
|
||||
|
||||
1. Build jar:
|
||||
完整部署手册:
|
||||
- [docs/deployment/tms-deployment.md](/Users/waner/Work/CISD/文档/tms-framework/docs/deployment/tms-deployment.md)
|
||||
|
||||
1. 构建 jar 包:
|
||||
|
||||
```bash
|
||||
cd tms-framework
|
||||
@@ -25,17 +28,19 @@ mvn -q -DskipTests package
|
||||
cp target/tms-framework-*.jar /home/tms/tms-framework.jar
|
||||
```
|
||||
|
||||
2. Prepare deployment directory on CentOS:
|
||||
2. 在 CentOS 上准备部署目录:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /home/tms/{config,scripts,logs,run}
|
||||
sudo mkdir -p /home/tms/{bin,config,scripts,logs,run}
|
||||
sudo chown -R "$(whoami)":"$(whoami)" /home/tms
|
||||
cp scripts/tms.sh /home/tms/scripts/tms.sh
|
||||
cp scripts/standard-init/*.sh /home/tms/bin/
|
||||
cp config/application.yml.example /home/tms/config/application.yml
|
||||
chmod +x /home/tms/bin/*.sh
|
||||
chmod +x /home/tms/scripts/tms.sh
|
||||
```
|
||||
|
||||
3. Start/Stop/Status:
|
||||
3. 启动 / 停止 / 状态:
|
||||
|
||||
```bash
|
||||
/home/tms/scripts/tms.sh start
|
||||
@@ -43,15 +48,18 @@ chmod +x /home/tms/scripts/tms.sh
|
||||
/home/tms/scripts/tms.sh stop
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Script default `APP_HOME=/home/tms`, and loads external config from `/home/tms/config/` via `--spring.config.additional-location=optional:file:<app_home>/config/`.
|
||||
- `/home/tms/config/application.yml` is required by default.
|
||||
- Default profile is `prod`; override with `SPRING_PROFILES_ACTIVE=dev /home/tms/scripts/tms.sh start`.
|
||||
- Override jar path with `JAR_PATH=/home/tms/tms-framework.jar`.
|
||||
- If deployment path is not `/home/tms`, use `APP_HOME=/your/path /your/path/scripts/tms.sh start`.
|
||||
说明:
|
||||
- 脚本默认 `APP_HOME=/home/tms`,并通过 `--spring.config.additional-location=optional:file:<app_home>/config/` 从 `/home/tms/config/` 读取外部配置。
|
||||
- 默认要求 `/home/tms/config/application.yml` 存在。
|
||||
- 默认 profile 为 `prod`;可通过 `SPRING_PROFILES_ACTIVE=dev /home/tms/scripts/tms.sh start` 覆盖。
|
||||
- 可通过 `JAR_PATH=/home/tms/tms-framework.jar` 覆盖 jar 路径。
|
||||
- 如果部署路径不是 `/home/tms`,使用 `APP_HOME=/your/path /your/path/scripts/tms.sh start`。
|
||||
- 标准 CISD 初始化辅助脚本位于 `scripts/standard-init/`,部署时需复制到 `/home/tms/bin/`。
|
||||
- `apply_standard_db.sh` 依赖预置环境变量,例如 `DB_USER`、`DB_PASSWORD`,不要直接写入 `application.yml`。
|
||||
- 运行目录结构、配置项说明、文件上传 `fileId` 流程和故障排查,请查看上面的完整部署手册。
|
||||
|
||||
Runtime recommendation:
|
||||
- Use JDK 17 for local build/test to match project and CI runtime.
|
||||
运行时建议:
|
||||
- 本地构建和测试统一使用 JDK 17,与项目和 CI 运行时保持一致。
|
||||
|
||||
Open:
|
||||
- Swagger UI: http://localhost:8080/swagger-ui.html
|
||||
@@ -69,7 +77,7 @@ Open:
|
||||
|
||||
- One deployable Spring Boot application.
|
||||
- Module-first packaging under `modules/*`.
|
||||
- Default module layout: `controller/internal + service + repository + entity + dto`.
|
||||
- Default module layout: `controller + service + repository + entity + dto`.
|
||||
- `sign` module has extra `controller/openapi`, `dto/openapi`, and `support`.
|
||||
- Cross-cutting concerns moved to top-level `security` and `integration`.
|
||||
|
||||
@@ -101,11 +109,11 @@ src/main/java/com/cisd/tms
|
||||
│ └── openapi
|
||||
└── modules
|
||||
├── system
|
||||
│ ├── controller/internal
|
||||
│ ├── controller
|
||||
│ ├── service
|
||||
│ └── dto
|
||||
├── sign
|
||||
│ ├── controller/internal
|
||||
│ ├── controller
|
||||
│ ├── controller/openapi
|
||||
│ ├── service
|
||||
│ ├── dto/internal
|
||||
@@ -139,7 +147,7 @@ src/main/resources
|
||||
|
||||
## API Boundary Rules
|
||||
|
||||
- Internal API controllers only in `*/controller/internal`, path prefix `/api/**`.
|
||||
- `/api/**` controllers live under `controller`.
|
||||
- External API controllers only in `*/controller/openapi`, path prefix `/openapi/**`.
|
||||
- Internal and external DTOs are separated.
|
||||
- Cross-module calls must go through `service`, not `repository`.
|
||||
|
||||
@@ -17,16 +17,29 @@ tms:
|
||||
product-type: ENTERPRISE
|
||||
version: V1.3.2
|
||||
source: CONFIG
|
||||
file:
|
||||
storage:
|
||||
upload-base-dir: /home/tms/uploads
|
||||
max-file-size-bytes: 104857600
|
||||
init:
|
||||
executor:
|
||||
mode: LOCAL
|
||||
timeout-sec: 600
|
||||
log-dir: /tmp/tms-init-logs
|
||||
log-dir: /home/tms/tmp/tms-init-logs
|
||||
standard-home-dir: /home/cemp4i
|
||||
standard-cpconfig-path: /home/cemp4i/cpconfig.cfg
|
||||
standard-db-username: root
|
||||
standard-db-password: change-me
|
||||
standard-db-apply-command: /home/tms/bin/apply_standard_db.sh
|
||||
standard-db-entry-sql-file-name: CMEP.sql
|
||||
standard-nginx-target-path: /usr/local/nginx/conf/nginx.conf
|
||||
standard-web-target-dir: /usr/local/nginx/html
|
||||
standard-web-organization-json-path: /usr/local/nginx/html/organization.json
|
||||
standard-run-user: cmep4i
|
||||
standard-app-start-command: /home/tms/bin/start_standard_apps.sh
|
||||
standard-nginx-start-command: /home/tms/bin/start_standard_nginx.sh
|
||||
standard-post-check-command: /home/tms/bin/check_standard_runtime.sh
|
||||
upload-base-dir: /home/tms/uploads
|
||||
staging-root-dir: /tmp/tms-init-staging
|
||||
standard-receiver-license-target-path: /home/cemp4i/cmep/license/cmep.license
|
||||
direct-receiver-license-target-path: /home/cmep4d/cmep/license/cmep.license
|
||||
standard-tlq-license-target-path: /home/tlq/tlqconf.zip
|
||||
direct-tlq-license-target-path: /home/tlq/TLQ8/license.dat
|
||||
@@ -34,9 +47,6 @@ tms:
|
||||
direct-tlq-init-cfg-dir: /opt/CAE_Install_App_Resource/CAE
|
||||
rabbit-admin-username: admin
|
||||
rabbit-admin-password: admin
|
||||
standard-rabbit-setup-script-path: /home/cemp4i/cpackage/rabq/setuprabq.sh
|
||||
standard-rabbit-queue-script-path: /home/cemp4i/cpackage/rabq/addrabq_R_01.sh
|
||||
standard-rabbit-queue-script-path-02: /home/cemp4i/cpackage/rabq/addrabq_R_02.sh
|
||||
rabbit-vhost: /RQ
|
||||
standard-rabbit-credential-encrypt-enabled: true
|
||||
standard-rabbit-credential-secret-key: ABCDEFGHIJKLMN1234567890
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
# TMS 部署手册
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文档用于说明如何在 CentOS Linux 主机上按标准运行路径 `/home/tms` 部署 TMS,以及如何准备 CISD 初始化所需的相关脚本、目录、文件和配置。
|
||||
|
||||
本文档覆盖以下内容:
|
||||
- TMS jar 包部署
|
||||
- `/home/tms` 运行目录结构
|
||||
- 外部配置文件
|
||||
- `/home/tms/bin` 下的标准初始化辅助脚本
|
||||
- 文件上传与 `fileId` 存储规则
|
||||
- 启动、停止、状态检查
|
||||
- 初始化前置检查和常见故障排查
|
||||
|
||||
本文档不替代厂商提供的 CIPS 部署手册。中间件和标准收发器基础安装仍属于预置工作。
|
||||
|
||||
## 2. 部署边界
|
||||
|
||||
TMS 负责:
|
||||
- 提供 internal/openapi HTTP 服务
|
||||
- 管理 CISD 初始化任务
|
||||
- 渲染客户化配置
|
||||
- 调用已配置的初始化后置脚本
|
||||
- 按 `fileId` 解析上传文件
|
||||
|
||||
预置层负责:
|
||||
- JDK 安装
|
||||
- TiDB/MySQL 客户端可用
|
||||
- RabbitMQ/TLQ/nginx 安装
|
||||
- 标准收发器介质解压到 `/home/cemp4i`
|
||||
- 直参版介质解压到厂商约定目录
|
||||
- 数据库凭据和 RabbitMQ 管理员凭据
|
||||
|
||||
## 3. 环境要求
|
||||
|
||||
### 3.1 操作系统
|
||||
|
||||
推荐环境:
|
||||
- CentOS 或兼容 Linux 发行版
|
||||
|
||||
### 3.2 Java
|
||||
|
||||
运行时要求:
|
||||
- JDK 17
|
||||
|
||||
检查命令:
|
||||
|
||||
```bash
|
||||
java -version
|
||||
```
|
||||
|
||||
### 3.3 网络与端口
|
||||
|
||||
TMS 默认监听端口:
|
||||
- `8080`
|
||||
|
||||
检查命令:
|
||||
|
||||
```bash
|
||||
ss -lntp | grep 8080
|
||||
```
|
||||
|
||||
### 3.4 建议具备的系统命令
|
||||
|
||||
建议系统具备以下命令:
|
||||
- `java`
|
||||
- `bash`
|
||||
- `curl`
|
||||
- `jq`
|
||||
- `mysql`
|
||||
- `pgrep`
|
||||
- `unzip`
|
||||
|
||||
可选但推荐:
|
||||
- `ss`
|
||||
- `lsof`
|
||||
|
||||
## 4. 标准运行目录结构
|
||||
|
||||
TMS 统一部署到 `/home/tms`:
|
||||
|
||||
```text
|
||||
/home/tms
|
||||
├── tms-framework.jar
|
||||
├── bin
|
||||
│ ├── apply_standard_db.sh
|
||||
│ ├── start_standard_apps.sh
|
||||
│ ├── start_standard_nginx.sh
|
||||
│ └── check_standard_runtime.sh
|
||||
├── config
|
||||
│ └── application.yml
|
||||
├── logs
|
||||
│ ├── tms-framework.out.log
|
||||
│ └── tms-framework.err.log
|
||||
├── run
|
||||
│ └── tms-framework.pid
|
||||
├── scripts
|
||||
│ └── tms.sh
|
||||
├── tmp
|
||||
│ └── tms-init-logs
|
||||
└── uploads
|
||||
└── <fileId>
|
||||
└── <originalFilename>
|
||||
```
|
||||
|
||||
目录用途:
|
||||
- `/home/tms/bin`:初始化后置辅助脚本目录,由 `tms.init.executor.*command` 引用
|
||||
- `/home/tms/config`:Spring Boot 外部配置目录
|
||||
- `/home/tms/logs`:TMS 运行日志
|
||||
- `/home/tms/run`:PID 文件目录
|
||||
- `/home/tms/scripts`:TMS 启停控制脚本目录
|
||||
- `/home/tms/tmp`:初始化执行日志和临时目录
|
||||
- `/home/tms/uploads`:上传文件目录,初始化阶段按 `fileId` 解析
|
||||
|
||||
## 5. 部署物来源映射
|
||||
|
||||
| 部署目标 | 仓库来源 |
|
||||
|---|---|
|
||||
| `/home/tms/tms-framework.jar` | `target/tms-framework-*.jar` |
|
||||
| `/home/tms/scripts/tms.sh` | `scripts/tms.sh` |
|
||||
| `/home/tms/bin/apply_standard_db.sh` | `scripts/standard-init/apply_standard_db.sh` |
|
||||
| `/home/tms/bin/start_standard_apps.sh` | `scripts/standard-init/start_standard_apps.sh` |
|
||||
| `/home/tms/bin/start_standard_nginx.sh` | `scripts/standard-init/start_standard_nginx.sh` |
|
||||
| `/home/tms/bin/check_standard_runtime.sh` | `scripts/standard-init/check_standard_runtime.sh` |
|
||||
| `/home/tms/config/application.yml` | 由 `config/application.yml.example` 复制生成 |
|
||||
|
||||
## 6. 部署步骤
|
||||
|
||||
### 6.1 构建 jar 包
|
||||
|
||||
```bash
|
||||
cd /Users/waner/Work/CISD/文档/tms-framework
|
||||
mvn -q -DskipTests package
|
||||
```
|
||||
|
||||
### 6.2 创建运行目录
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /home/tms/{bin,config,scripts,logs,run,tmp,uploads}
|
||||
sudo chown -R "$(whoami)":"$(whoami)" /home/tms
|
||||
```
|
||||
|
||||
### 6.3 拷贝运行文件
|
||||
|
||||
```bash
|
||||
cp target/tms-framework-*.jar /home/tms/tms-framework.jar
|
||||
cp scripts/tms.sh /home/tms/scripts/tms.sh
|
||||
cp scripts/standard-init/*.sh /home/tms/bin/
|
||||
cp config/application.yml.example /home/tms/config/application.yml
|
||||
chmod +x /home/tms/scripts/tms.sh
|
||||
chmod +x /home/tms/bin/*.sh
|
||||
```
|
||||
|
||||
### 6.4 检查部署结果
|
||||
|
||||
```bash
|
||||
ls -l /home/tms/tms-framework.jar
|
||||
ls -l /home/tms/scripts/tms.sh
|
||||
ls -l /home/tms/bin
|
||||
ls -l /home/tms/config/application.yml
|
||||
```
|
||||
|
||||
## 7. 外部配置文件
|
||||
|
||||
TMS 从以下位置加载外部配置:
|
||||
- `/home/tms/config/application.yml`
|
||||
|
||||
运行控制脚本:
|
||||
- [tms.sh](/Users/waner/Work/CISD/文档/tms-framework/scripts/tms.sh)
|
||||
|
||||
### 7.1 最低必配项
|
||||
|
||||
至少需要检查并设置:
|
||||
|
||||
```yaml
|
||||
tms:
|
||||
security:
|
||||
internal-token: change-me-internal-token
|
||||
file:
|
||||
storage:
|
||||
upload-base-dir: /home/tms/uploads
|
||||
max-file-size-bytes: 104857600
|
||||
init:
|
||||
executor:
|
||||
mode: LOCAL
|
||||
log-dir: /home/tms/tmp/tms-init-logs
|
||||
upload-base-dir: /home/tms/uploads
|
||||
staging-root-dir: /home/tmp/tms-init-staging
|
||||
standard-db-apply-command: /home/tms/bin/apply_standard_db.sh
|
||||
standard-app-start-command: /home/tms/bin/start_standard_apps.sh
|
||||
standard-nginx-start-command: /home/tms/bin/start_standard_nginx.sh
|
||||
standard-post-check-command: /home/tms/bin/check_standard_runtime.sh
|
||||
```
|
||||
|
||||
### 7.2 必须保持一致的配置
|
||||
|
||||
以下两个路径必须保持一致:
|
||||
- `tms.file.storage.upload-base-dir`
|
||||
- `tms.init.executor.upload-base-dir`
|
||||
|
||||
如果两者不一致,会出现“上传成功,但初始化执行阶段找不到文件”的问题。
|
||||
|
||||
### 7.3 标准版初始化关键配置
|
||||
|
||||
标准版初始化重点配置项:
|
||||
- `standard-home-dir`:默认 `/home/cemp4i`
|
||||
- `standard-cpconfig-path`:标准版 `cpconfig.cfg` 路径
|
||||
- `standard-db-script-base-dir`:默认 `${standard-home-dir}/cpackage`
|
||||
- `standard-db-load-dir`:默认 `${standard-home-dir}/mysql/loadfilepath`
|
||||
- `standard-nginx-template-path`:nginx 模板路径
|
||||
- `standard-web-front-zip-path`:默认 `${standard-home-dir}/cpackage/front/front.zip`
|
||||
- `standard-web-target-dir`:默认 `/usr/local/nginx/html`
|
||||
- `standard-web-organization-json-path`:默认 `/usr/local/nginx/html/organization.json`
|
||||
|
||||
### 7.4 不建议直接写入 application.yml 的凭据
|
||||
|
||||
除非现场策略允许,否则不建议把以下信息直接写进 `/home/tms/config/application.yml`:
|
||||
- `apply_standard_db.sh` 使用的数据库密码
|
||||
- 跨主机共享的数据库用户名
|
||||
- 辅助脚本依赖的其他敏感环境变量
|
||||
|
||||
推荐方式:
|
||||
- 在启动 TMS 前通过环境变量导出
|
||||
- 或通过独立受保护的环境文件,由辅助脚本自行加载
|
||||
|
||||
## 8. 标准初始化辅助脚本
|
||||
|
||||
这些脚本部署到 `/home/tms/bin`,由 TMS 初始化执行器在标准版流程中调用。
|
||||
|
||||
### 8.1 `apply_standard_db.sh`
|
||||
|
||||
来源:
|
||||
- [apply_standard_db.sh](/Users/waner/Work/CISD/文档/tms-framework/scripts/standard-init/apply_standard_db.sh)
|
||||
|
||||
作用:
|
||||
- 不存在时创建 `CMEP` 数据库
|
||||
- 导入 `SCHEMA-DDL.sql`
|
||||
- 导入 `CMEP.sql`
|
||||
- 导入 `UP-ORG-INFO.sql`
|
||||
|
||||
必需环境变量:
|
||||
- `DB_USER`
|
||||
- `DB_PASSWORD`
|
||||
|
||||
可选环境变量:
|
||||
- `DB_HOST`,默认 `127.0.0.1`
|
||||
- `DB_PORT`,默认 `4000`
|
||||
- `DB_NAME`,默认 `CMEP`
|
||||
- `LOAD_DIR`,默认 `/home/cemp4i/mysql/loadfilepath`
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
export DB_USER=app_user
|
||||
export DB_PASSWORD='secret'
|
||||
/home/tms/bin/apply_standard_db.sh
|
||||
```
|
||||
|
||||
### 8.2 `start_standard_apps.sh`
|
||||
|
||||
来源:
|
||||
- [start_standard_apps.sh](/Users/waner/Work/CISD/文档/tms-framework/scripts/standard-init/start_standard_apps.sh)
|
||||
|
||||
作用:
|
||||
- 启动 CMSP
|
||||
- 启动 CMTP
|
||||
|
||||
依赖:
|
||||
- `/home/cemp4i/cmsp/start_cmsp.sh`
|
||||
- `/home/cemp4i/cmtp/start_cmtp.sh`
|
||||
- 用户 `cmep4i`
|
||||
|
||||
### 8.3 `start_standard_nginx.sh`
|
||||
|
||||
来源:
|
||||
- [start_standard_nginx.sh](/Users/waner/Work/CISD/文档/tms-framework/scripts/standard-init/start_standard_nginx.sh)
|
||||
|
||||
作用:
|
||||
- 如果 nginx 已运行则执行 reload
|
||||
- 如果 nginx 未运行则按 `/usr/local/nginx/conf/nginx.conf` 启动
|
||||
|
||||
依赖:
|
||||
- `/usr/local/nginx/sbin/nginx`
|
||||
|
||||
### 8.4 `check_standard_runtime.sh`
|
||||
|
||||
来源:
|
||||
- [check_standard_runtime.sh](/Users/waner/Work/CISD/文档/tms-framework/scripts/standard-init/check_standard_runtime.sh)
|
||||
|
||||
作用:
|
||||
- 检查 CMSP 进程是否存在
|
||||
- 检查 CMTP 进程是否存在
|
||||
- 检查 nginx 进程是否存在
|
||||
- 检查 `/usr/local/nginx/html/organization.json` 是否存在
|
||||
|
||||
该脚本仅用于初始化后的结果校验,不负责修复。
|
||||
|
||||
## 9. 文件上传与 `fileId` 模型
|
||||
|
||||
### 9.1 核心原则
|
||||
|
||||
CISD 初始化接口本身不接收 multipart 文件。
|
||||
|
||||
前端正确流程是:
|
||||
1. 用户选择文件
|
||||
2. 前端先调用文件上传接口
|
||||
3. 后端返回 `fileId`
|
||||
4. 前端在初始化 JSON 中提交 `fileId`
|
||||
|
||||
### 9.2 存储结构
|
||||
|
||||
上传文件统一保存为:
|
||||
|
||||
```text
|
||||
/home/tms/uploads/<fileId>/<originalFilename>
|
||||
```
|
||||
|
||||
### 9.3 初始化常用文件字段
|
||||
|
||||
初始化请求中常见的文件引用字段:
|
||||
- `licenses.receiverLicenseFileId`
|
||||
- `mq.tlqLicenseFileId`
|
||||
- `mq.cfmqConfigFileId`
|
||||
- `licenses.cfgZipFileId`
|
||||
|
||||
### 9.4 初始化执行阶段的解析规则
|
||||
|
||||
执行器会依次尝试解析:
|
||||
- `/home/tms/uploads/<fileId>`
|
||||
- `/home/tms/uploads/<fileId>/<preferredFileName>`
|
||||
|
||||
因此文件上传服务和初始化执行器必须使用同一个上传根目录。
|
||||
|
||||
## 10. 启动、停止与状态检查
|
||||
|
||||
### 10.1 启动
|
||||
|
||||
```bash
|
||||
/home/tms/scripts/tms.sh start
|
||||
```
|
||||
|
||||
### 10.2 停止
|
||||
|
||||
```bash
|
||||
/home/tms/scripts/tms.sh stop
|
||||
```
|
||||
|
||||
### 10.3 重启
|
||||
|
||||
```bash
|
||||
/home/tms/scripts/tms.sh restart
|
||||
```
|
||||
|
||||
### 10.4 状态
|
||||
|
||||
```bash
|
||||
/home/tms/scripts/tms.sh status
|
||||
```
|
||||
|
||||
### 10.5 运行日志
|
||||
|
||||
主要日志:
|
||||
- `/home/tms/logs/tms-framework.out.log`
|
||||
- `/home/tms/logs/tms-framework.err.log`
|
||||
|
||||
常用查看命令:
|
||||
|
||||
```bash
|
||||
tail -n 100 /home/tms/logs/tms-framework.out.log
|
||||
tail -n 100 /home/tms/logs/tms-framework.err.log
|
||||
```
|
||||
|
||||
## 11. 初始化前置检查
|
||||
|
||||
预检查脚本:
|
||||
- [cisd_init_precheck.sh](/Users/waner/Work/CISD/文档/tms-framework/scripts/cisd_init_precheck.sh)
|
||||
|
||||
执行方式:
|
||||
|
||||
```bash
|
||||
chmod +x /Users/waner/Work/CISD/文档/tms-framework/scripts/cisd_init_precheck.sh
|
||||
PRODUCT_TYPE=ENTERPRISE /Users/waner/Work/CISD/文档/tms-framework/scripts/cisd_init_precheck.sh
|
||||
```
|
||||
|
||||
检查内容包括:
|
||||
- 产品类型是否合法
|
||||
- 执行模式是否正确
|
||||
- 日志目录是否可写
|
||||
- 标准版基础文件是否存在
|
||||
- RabbitMQ 工具是否可用
|
||||
- 直参版场景下的脚本目录是否存在
|
||||
|
||||
建议在首次联调前执行一次。
|
||||
|
||||
## 12. 初始化运行期目录与日志
|
||||
|
||||
### 12.1 初始化步骤日志
|
||||
|
||||
由以下配置控制:
|
||||
- `tms.init.executor.log-dir`
|
||||
|
||||
推荐路径:
|
||||
- `/home/tms/tmp/tms-init-logs`
|
||||
|
||||
### 12.2 初始化 staging 目录
|
||||
|
||||
由以下配置控制:
|
||||
- `tms.init.executor.staging-root-dir`
|
||||
|
||||
默认路径:
|
||||
- `/home/tmp/tms-init-staging`
|
||||
|
||||
staging 中通常包含:
|
||||
- 渲染后的 `cpconfig.cfg`
|
||||
- 暂存的 license / 配置文件
|
||||
- 渲染后的 SQL 包
|
||||
- 渲染后的 nginx 配置
|
||||
- 暂存的 web 静态资源
|
||||
|
||||
## 13. 常见故障排查
|
||||
|
||||
### 13.1 TMS 无法启动
|
||||
|
||||
现象:
|
||||
- `tms.sh start` 失败
|
||||
- 未生成 PID 文件
|
||||
|
||||
检查:
|
||||
|
||||
```bash
|
||||
java -version
|
||||
ls -l /home/tms/tms-framework.jar
|
||||
ls -l /home/tms/config/application.yml
|
||||
tail -n 200 /home/tms/logs/tms-framework.err.log
|
||||
```
|
||||
|
||||
### 13.2 `DB_APPLY` 报错 `standardDbApplyCommand is blank`
|
||||
|
||||
含义:
|
||||
- SQL 已经渲染并复制成功
|
||||
- 但未配置真实导库命令
|
||||
|
||||
修复:
|
||||
- 确保 `/home/tms/config/application.yml` 中存在:
|
||||
|
||||
```yaml
|
||||
standard-db-apply-command: /home/tms/bin/apply_standard_db.sh
|
||||
```
|
||||
|
||||
- 确保脚本存在并可执行
|
||||
- 确保 `DB_USER`、`DB_PASSWORD` 已经提供
|
||||
|
||||
### 13.3 `cpconfig not found`
|
||||
|
||||
检查:
|
||||
- `standard-cpconfig-path`
|
||||
- 直参版 `cfgZipFileId`
|
||||
- 标准版介质是否已解压到 `/home/cemp4i/cpconfig.cfg`
|
||||
|
||||
### 13.4 初始化阶段按 `fileId` 找不到文件
|
||||
|
||||
现象:
|
||||
- 步骤日志显示已尝试 `/home/tms/uploads/...`
|
||||
- 但所有候选路径都不存在
|
||||
|
||||
检查命令:
|
||||
|
||||
```bash
|
||||
find /home/tms/uploads -maxdepth 2 -type f | sort
|
||||
```
|
||||
|
||||
重点确认:
|
||||
- 上传接口的落盘根目录是否正确
|
||||
- 初始化请求中的 `fileId` 是否与上传返回值一致
|
||||
- `tms.file.storage.upload-base-dir` 是否与 `tms.init.executor.upload-base-dir` 一致
|
||||
|
||||
### 13.5 RabbitMQ 命令或队列步骤失败
|
||||
|
||||
检查:
|
||||
- `rabbitmqctl`
|
||||
- `rabbitmqadmin`
|
||||
- `standard-rabbit-setup-script-path`
|
||||
- `standard-rabbit-queue-script-path`
|
||||
- 步骤日志内容
|
||||
|
||||
### 13.6 WEB 发布或 nginx 启动失败
|
||||
|
||||
检查:
|
||||
|
||||
```bash
|
||||
ls -l /home/cemp4i/cpackage/front/front.zip
|
||||
ls -l /usr/local/nginx/conf/nginx.conf
|
||||
ls -l /usr/local/nginx/html
|
||||
pgrep -f '/usr/local/nginx/sbin/nginx'
|
||||
```
|
||||
|
||||
同时确认:
|
||||
- `/home/tms/bin/start_standard_nginx.sh` 存在
|
||||
- `standard-web-front-zip-path` 指向正确的标准版前端包
|
||||
- 发布后 `organization.json` 已生成
|
||||
|
||||
## 14. 推荐的本机检查命令
|
||||
|
||||
```bash
|
||||
/home/tms/scripts/tms.sh status
|
||||
ss -lntp | grep 8080
|
||||
curl -s http://127.0.0.1:8080/api/v1/device/status
|
||||
find /home/tms/uploads -maxdepth 2 -type f | sort
|
||||
ls -l /home/tms/bin
|
||||
ls -l /home/tms/config/application.yml
|
||||
```
|
||||
|
||||
对标准初始化辅助脚本做语法检查:
|
||||
|
||||
```bash
|
||||
bash -n /home/tms/bin/apply_standard_db.sh
|
||||
bash -n /home/tms/bin/start_standard_apps.sh
|
||||
bash -n /home/tms/bin/start_standard_nginx.sh
|
||||
bash -n /home/tms/bin/check_standard_runtime.sh
|
||||
```
|
||||
@@ -16,6 +16,10 @@
|
||||
"name": "Device",
|
||||
"description": "设备信息与状态"
|
||||
},
|
||||
{
|
||||
"name": "File",
|
||||
"description": "内部文件上传与查询"
|
||||
},
|
||||
{
|
||||
"name": "Init",
|
||||
"description": "CISD 初始化任务"
|
||||
@@ -103,6 +107,78 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/files/upload": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"File"
|
||||
],
|
||||
"summary": "上传文件并返回 fileId",
|
||||
"operationId": "uploadFile",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"file"
|
||||
],
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseFileUploadResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/files/{fileId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"File"
|
||||
],
|
||||
"summary": "查询文件元数据",
|
||||
"operationId": "getFileDetail",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "fileId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "上传接口返回的 fileId"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseFileDetailResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/init/template": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -406,6 +482,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseFileUploadResponse": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ApiResponseBase"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/FileUploadResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseFileDetailResponse": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ApiResponseBase"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/FileDetailResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseInitPlanTemplateResponse": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -559,6 +665,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FileUploadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileId": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"storagePath": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FileDetailResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileId": {
|
||||
"type": "string"
|
||||
},
|
||||
"originalFilename": {
|
||||
"type": "string"
|
||||
},
|
||||
"storagePath": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ACTIVE"
|
||||
]
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitPlanTemplateResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -640,7 +785,7 @@
|
||||
"$ref": "#/components/schemas/InitPreviewRequestDirectTlqConfig"
|
||||
}
|
||||
},
|
||||
"description": "初始化请求体。部分字段为条件必填:\n- DIRECT + RABBITMQ_TLQ 需提供 directTlq\n- DIRECT 需提供 signServer 与 licenses.cfgZipFileId\n- TLQ 模式需提供 mq.tlqLicenseFileId\n- RABBITMQ_CFMQ 需提供 mq.cfmqConfigFileId\n"
|
||||
"description": "初始化请求体。部分字段为条件必填:\n- 先上传文件到 `/api/v1/files/upload`,再将返回的 `fileId` 填入下述 fileId 字段\n- ENTERPRISE: mq.mqType 仅支持 RABBITMQ 或 TLQ\n- INDIRECT: mq.mqType 仅支持 RABBITMQ 或 RABBITMQ_TLQ\n- DIRECT + RABBITMQ_TLQ 需提供 directTlq\n- DIRECT 需提供 signServer 与 licenses.cfgZipFileId\n- ENTERPRISE + TLQ、INDIRECT + RABBITMQ_TLQ、DIRECT + RABBITMQ_TLQ 需提供 mq.tlqLicenseFileId\n- RABBITMQ_CFMQ 需提供 mq.cfmqConfigFileId\n"
|
||||
},
|
||||
"InitPreviewRequestNodes": {
|
||||
"type": "object",
|
||||
@@ -676,7 +821,7 @@
|
||||
"properties": {
|
||||
"mqType": {
|
||||
"type": "string",
|
||||
"description": "ENTERPRISE/INDIRECT: RABBITMQ|TLQ; DIRECT: RABBITMQ_TLQ|RABBITMQ_CFMQ"
|
||||
"description": "按产品区分的支持方式枚举:\n- ENTERPRISE: RABBITMQ | TLQ\n- INDIRECT: RABBITMQ | RABBITMQ_TLQ\n- DIRECT: RABBITMQ_TLQ | RABBITMQ_CFMQ\n"
|
||||
},
|
||||
"channelUsername": {
|
||||
"type": "string",
|
||||
@@ -904,4 +1049,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ servers:
|
||||
tags:
|
||||
- name: Device
|
||||
description: 设备信息与状态
|
||||
- name: File
|
||||
description: 内部文件上传与查询
|
||||
- name: Init
|
||||
description: CISD 初始化任务
|
||||
security:
|
||||
@@ -60,6 +62,50 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ApiResponseDeviceActionResponse"
|
||||
|
||||
/api/v1/files/upload:
|
||||
post:
|
||||
tags: [File]
|
||||
summary: 上传文件并返回 fileId
|
||||
operationId: uploadFile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file]
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ApiResponseFileUploadResponse"
|
||||
|
||||
/api/v1/files/{fileId}:
|
||||
get:
|
||||
tags: [File]
|
||||
summary: 查询文件元数据
|
||||
operationId: getFileDetail
|
||||
parameters:
|
||||
- name: fileId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: 上传接口返回的 fileId
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ApiResponseFileDetailResponse"
|
||||
|
||||
/api/v1/init/template:
|
||||
get:
|
||||
tags: [Init]
|
||||
@@ -252,6 +298,22 @@ components:
|
||||
data:
|
||||
$ref: "#/components/schemas/DeviceActionResponse"
|
||||
|
||||
ApiResponseFileUploadResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/ApiResponseBase"
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/FileUploadResponse"
|
||||
|
||||
ApiResponseFileDetailResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/ApiResponseBase"
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/FileDetailResponse"
|
||||
|
||||
ApiResponseInitPlanTemplateResponse:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/ApiResponseBase"
|
||||
@@ -339,6 +401,33 @@ components:
|
||||
acceptedAt:
|
||||
type: string
|
||||
|
||||
FileUploadResponse:
|
||||
type: object
|
||||
properties:
|
||||
fileId:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
format: int64
|
||||
storagePath:
|
||||
type: string
|
||||
|
||||
FileDetailResponse:
|
||||
type: object
|
||||
properties:
|
||||
fileId:
|
||||
type: string
|
||||
originalFilename:
|
||||
type: string
|
||||
storagePath:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [ACTIVE]
|
||||
size:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
InitPlanTemplateResponse:
|
||||
type: object
|
||||
properties:
|
||||
@@ -392,9 +481,12 @@ components:
|
||||
$ref: "#/components/schemas/InitPreviewRequestDirectTlqConfig"
|
||||
description: |
|
||||
初始化请求体。部分字段为条件必填:
|
||||
- 先上传文件到 `/api/v1/files/upload`,再将返回的 `fileId` 填入下述 fileId 字段
|
||||
- ENTERPRISE: mq.mqType 仅支持 RABBITMQ 或 TLQ
|
||||
- INDIRECT: mq.mqType 仅支持 RABBITMQ 或 RABBITMQ_TLQ
|
||||
- DIRECT + RABBITMQ_TLQ 需提供 directTlq
|
||||
- DIRECT 需提供 signServer 与 licenses.cfgZipFileId
|
||||
- TLQ 模式需提供 mq.tlqLicenseFileId
|
||||
- ENTERPRISE + TLQ、INDIRECT + RABBITMQ_TLQ、DIRECT + RABBITMQ_TLQ 需提供 mq.tlqLicenseFileId
|
||||
- RABBITMQ_CFMQ 需提供 mq.cfmqConfigFileId
|
||||
|
||||
InitPreviewRequestNodes:
|
||||
@@ -420,7 +512,11 @@ components:
|
||||
properties:
|
||||
mqType:
|
||||
type: string
|
||||
description: "ENTERPRISE/INDIRECT: RABBITMQ|TLQ; DIRECT: RABBITMQ_TLQ|RABBITMQ_CFMQ"
|
||||
description: |
|
||||
按产品区分的支持方式枚举:
|
||||
- ENTERPRISE: RABBITMQ | TLQ
|
||||
- INDIRECT: RABBITMQ | RABBITMQ_TLQ
|
||||
- DIRECT: RABBITMQ_TLQ | RABBITMQ_CFMQ
|
||||
channelUsername:
|
||||
type: string
|
||||
maxLength: 32
|
||||
|
||||
+2320
-1171
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
# CISD 标准版初始化支持方式设计
|
||||
|
||||
## 背景
|
||||
当前 TMS 将标准版初始化统一抽象为 `mqType=RABBITMQ|TLQ`。这与实际部署介质不完全一致:
|
||||
- `ENTERPRISE` 支持 `RABBITMQ` 和 `TLQ` 两种互斥模式。
|
||||
- `INDIRECT` 支持 `仅 RabbitMQ` 和 `RabbitMQ + TLQ` 两种模式。
|
||||
- `INDIRECT` 的 `RabbitMQ + TLQ` 并不是纯 `TLQ`,仍需执行 RabbitMQ 用户、队列和应用配置。
|
||||
|
||||
## 目标
|
||||
在不破坏现有接口字段名的前提下,修正标准版初始化参数校验、步骤编排和执行条件,使其与部署介质和脚本行为一致。
|
||||
|
||||
## 约束
|
||||
- 保留前端请求字段 `mq.mqType`,避免接口字段名变更带来的联调成本。
|
||||
- 仅修改 `modules/init` 范围内代码和 `docs/openapi/cisd-init` 文档。
|
||||
- 不调整出厂前预装边界;本次仍只覆盖初始化阶段。
|
||||
|
||||
## 方案选择
|
||||
### 方案 A:重命名字段为 `supportMode`
|
||||
优点:语义最准确。
|
||||
缺点:破坏现有接口,前后端和历史任务数据都要联动修改。
|
||||
|
||||
### 方案 B:保留 `mqType` 字段名,按产品解释枚举值
|
||||
优点:改动最小,能直接修正行为。
|
||||
缺点:字段名与实际语义存在轻微偏差,需要通过文档解释。
|
||||
|
||||
### 方案 C:继续保留当前模型,仅靠执行器特殊判断
|
||||
优点:表面改动更少。
|
||||
缺点:校验、预检、步骤预览、OpenAPI 全部失真,后续维护成本更高。
|
||||
|
||||
## 采用方案
|
||||
采用方案 B。
|
||||
|
||||
## 调整后的 `mqType` 语义
|
||||
- `ENTERPRISE`: `RABBITMQ` | `TLQ`
|
||||
- `INDIRECT`: `RABBITMQ` | `RABBITMQ_TLQ`
|
||||
- `DIRECT`: `RABBITMQ_TLQ` | `RABBITMQ_CFMQ`
|
||||
|
||||
## 标准版步骤编排
|
||||
### ENTERPRISE + RABBITMQ
|
||||
`RENDER_CONFIG -> FILE_PREPARE -> SET_RABBITMQ -> MQ_USER_APPLY -> MQ_QUEUE_APPLY -> DB_INIT -> DEPLOY_RECEIVER -> APP_MQ_CREDENTIALS_APPLY -> VERIFY`
|
||||
|
||||
### ENTERPRISE + TLQ
|
||||
`RENDER_CONFIG -> FILE_PREPARE -> SET_TLQ -> TLQ_CONF_APPLY -> DB_INIT -> DEPLOY_RECEIVER -> VERIFY`
|
||||
|
||||
### INDIRECT + RABBITMQ
|
||||
`RENDER_CONFIG -> FILE_PREPARE -> SET_RABBITMQ -> MQ_USER_APPLY -> MQ_QUEUE_APPLY -> DB_INIT -> DEPLOY_RECEIVER -> APP_MQ_CREDENTIALS_APPLY -> VERIFY`
|
||||
|
||||
### INDIRECT + RABBITMQ_TLQ
|
||||
`RENDER_CONFIG -> FILE_PREPARE -> SET_TLQ -> TLQ_CONF_APPLY -> MQ_USER_APPLY -> MQ_QUEUE_APPLY -> DB_INIT -> DEPLOY_RECEIVER -> APP_MQ_CREDENTIALS_APPLY -> VERIFY`
|
||||
|
||||
## 脚本映射
|
||||
- `SET_RABBITMQ` -> `cpackage/toolsh/setfraq.sh`
|
||||
- `SET_TLQ` -> `cpackage/toolsh/setftq.sh`
|
||||
- `TLQ_CONF_APPLY` -> `cpackage/toolsh/settlq.sh`
|
||||
- `DB_INIT` -> `cpackage/toolsh/setsql.sh`
|
||||
- `DEPLOY_RECEIVER` -> `cpackage/toolsh/setsptp.sh`
|
||||
- `MQ_USER_APPLY` -> 标准版 `setuprabq.sh` staging 执行
|
||||
- `MQ_QUEUE_APPLY` -> 标准版 `addrabq_R_01.sh` / `addrabq_R_02.sh` staging 执行
|
||||
|
||||
## 执行条件
|
||||
- `ENTERPRISE + TLQ` 跳过 RabbitMQ 用户、队列和应用账号写回。
|
||||
- `INDIRECT + RABBITMQ_TLQ` 保留 RabbitMQ 用户、队列和应用账号写回。
|
||||
- `FILE_PREPARE` 在 `TLQ` 或 `RABBITMQ_TLQ` 模式下均需准备 TLQ license。
|
||||
|
||||
## 测试重点
|
||||
- `ENTERPRISE + TLQ` 预检通过且步骤中包含 `TLQ_CONF_APPLY`,不包含 RabbitMQ 相关步骤。
|
||||
- `INDIRECT + RABBITMQ_TLQ` 预检通过且步骤中同时包含 `TLQ_CONF_APPLY` 与 RabbitMQ 相关步骤。
|
||||
- `INDIRECT + TLQ` 作为非法值被拒绝。
|
||||
- `ENTERPRISE + RABBITMQ_TLQ` 作为非法值被拒绝。
|
||||
- `APP_MQ_CREDENTIALS_APPLY` 在 `ENTERPRISE + TLQ` 下跳过,在 `INDIRECT + RABBITMQ_TLQ` 下执行。
|
||||
@@ -0,0 +1,121 @@
|
||||
# CISD Standard Init Support Mode Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 修正标准版 CISD 初始化中 `ENTERPRISE` 和 `INDIRECT` 的支持方式建模、步骤编排和执行条件。
|
||||
|
||||
**Architecture:** 保留 `mqType` 字段名,但将其解释为按产品区分的支持方式枚举;服务层负责校验和步骤编排,执行器负责按产品与模式判定脚本是否实际执行或跳过。
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot 3, JUnit 5, OpenAPI YAML/JSON
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 固化设计与枚举语义
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/plans/2026-03-06-cisd-standard-init-support-mode-design.md`
|
||||
- Create: `docs/plans/2026-03-06-cisd-standard-init-support-mode-plan.md`
|
||||
|
||||
**Step 1: 保存设计文档**
|
||||
保存版本差异、步骤编排和脚本映射。
|
||||
|
||||
**Step 2: 保存实施计划**
|
||||
记录测试先行的修改顺序。
|
||||
|
||||
### Task 2: 先补服务层失败测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
|
||||
**Step 1: 写 ENTERPRISE TLQ 的预检和步骤测试**
|
||||
断言 `TLQ_CONF_APPLY` 存在,`MQ_USER_APPLY/MQ_QUEUE_APPLY/APP_MQ_CREDENTIALS_APPLY` 不存在。
|
||||
|
||||
**Step 2: 写 INDIRECT RABBITMQ_TLQ 的预检和步骤测试**
|
||||
断言 `TLQ_CONF_APPLY`、`MQ_USER_APPLY`、`MQ_QUEUE_APPLY`、`APP_MQ_CREDENTIALS_APPLY` 同时存在。
|
||||
|
||||
**Step 3: 写非法组合测试**
|
||||
覆盖 `INDIRECT + TLQ`、`ENTERPRISE + RABBITMQ_TLQ` 被拒绝。
|
||||
|
||||
**Step 4: 运行定向测试,确认红灯**
|
||||
Run: `mvn -q -Dtest=InitServiceTest test`
|
||||
Expected: 至少新增用例失败。
|
||||
|
||||
### Task 3: 实现服务层校验和编排
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Modify: `src/main/resources/initplan/step-command-profiles.json`
|
||||
|
||||
**Step 1: 调整标准版允许枚举**
|
||||
按 `productType` 区分 `ENTERPRISE` 与 `INDIRECT`。
|
||||
|
||||
**Step 2: 调整标准版 license 校验**
|
||||
`ENTERPRISE + TLQ` 与 `INDIRECT + RABBITMQ_TLQ` 都要求 `tlqLicenseFileId`。
|
||||
|
||||
**Step 3: 新增 `TLQ_CONF_APPLY` 编排**
|
||||
更新 `defaultSteps` 和命令模板。
|
||||
|
||||
**Step 4: 运行服务层测试,确认绿灯**
|
||||
Run: `mvn -q -Dtest=InitServiceTest test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 4: 先补执行器失败测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: 写 ENTERPRISE TLQ 下 RabbitMQ 应用配置应跳过的测试**
|
||||
|
||||
**Step 2: 写 INDIRECT RABBITMQ_TLQ 下 RabbitMQ 用户步骤应执行的测试**
|
||||
|
||||
**Step 3: 写 `TLQ_CONF_APPLY` 命令解析或任务步骤断言测试**
|
||||
|
||||
**Step 4: 运行定向测试,确认红灯**
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest test`
|
||||
Expected: 至少新增用例失败。
|
||||
|
||||
### Task 5: 实现执行器条件修正
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Modify: `src/main/java/com/cisd/tms/modules/init/config/InitCommandProfileService.java`(如需补充注释/兼容逻辑)
|
||||
|
||||
**Step 1: 抽取标准版 RabbitMQ 是否启用的判断**
|
||||
- `ENTERPRISE`: 仅 `RABBITMQ`
|
||||
- `INDIRECT`: `RABBITMQ` 和 `RABBITMQ_TLQ`
|
||||
|
||||
**Step 2: 修正 `MQ_USER_APPLY` / `MQ_QUEUE_APPLY` 条件**
|
||||
|
||||
**Step 3: 修正 `APP_MQ_CREDENTIALS_APPLY` 条件**
|
||||
|
||||
**Step 4: 运行执行器测试,确认绿灯**
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 6: 同步 OpenAPI 文档
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/openapi/cisd-init.openapi.yaml`
|
||||
- Modify: `docs/openapi/cisd-init.openapi.json`
|
||||
|
||||
**Step 1: 更新 `mqType` 描述**
|
||||
明确不同产品的合法取值。
|
||||
|
||||
**Step 2: 更新请求示例和步骤含义说明**
|
||||
体现 `TLQ_CONF_APPLY` 与 `INDIRECT` 的双通道模式。
|
||||
|
||||
### Task 7: 完整验证
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: 运行模块定向测试**
|
||||
Run: `mvn -q -Dtest=InitServiceTest,ConfigurableInitStepExecutorTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: 运行编译验证**
|
||||
Run: `mvn -q -DskipTests compile`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: 人工检查关键路径**
|
||||
检查 `ENTERPRISE + TLQ`、`INDIRECT + RABBITMQ_TLQ` 的任务步骤和命令模板。
|
||||
@@ -0,0 +1,289 @@
|
||||
# CIPS Auto Deployment Refactor Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 在保留“基础环境预置”的前提下,将 CISD/CIPS 初始化从“cpconfig.cfg 驱动 vendor shell”逐步升级为“TMS 后端接管客户化渲染、输出物管理和执行编排”的自动化部署流程。
|
||||
|
||||
**Architecture:** 运维继续负责操作系统、目录、基础软件、介质上传解压、数据库和中间件平台级账号密码等与客户无关的预置动作;TMS 负责根据前端参数生成部署上下文,输出渲染后的配置和 SQL,按版本/支持方式编排客户化初始化步骤,并产出可审计的日志、配置快照和结果摘要。vendor 原始脚本默认只读,优先采用 staging 临时渲染执行,而不是直接改写介质本体。
|
||||
|
||||
**Tech Stack:** Spring Boot 3, Shell staging execution, 模板渲染, TiDB SQL 初始化, RabbitMQ/TLQ, CMSP/CMTP, Nginx/Web 发布
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 固化“预置层 / 初始化层”边界
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-09-auto-deploy-refactor.md`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/initplan/step-command-profiles.json`
|
||||
|
||||
**Step 1: 明确定义预置层**
|
||||
预置层仅包含与客户参数无关的动作:
|
||||
- OS 用户、用户组、目录、权限创建
|
||||
- JDK、RabbitMQ/TLQ/TiDB/Nginx 安装
|
||||
- 防火墙和服务注册
|
||||
- 安装介质上传、解压、静态资源预放置
|
||||
- 数据库账号密码、中间件管理员账号密码等平台级凭据预置
|
||||
- 许可证预置(如果出厂前已拿到)
|
||||
|
||||
**Step 2: 明确定义初始化层**
|
||||
初始化层仅包含客户化动作:
|
||||
- 机构号 / BIC / CIPS ID / LEI
|
||||
- 节点 IP / 数据库地址
|
||||
- RabbitMQ 通道账号密码
|
||||
- RabbitMQ 用户 / 队列 / VHost 初始化
|
||||
- TLQ 配置替换
|
||||
- SQL 渲染与导库编排
|
||||
- CMSP / CMTP / filetrans 配置渲染
|
||||
- Nginx upstream / organization.json / Web 发布
|
||||
- 服务启动与验收
|
||||
|
||||
**Step 3: 删除“初始化阶段执行预置脚本”的需求**
|
||||
以下脚本只保留为预置脚本,不纳入 TMS 初始化主流程:
|
||||
- `setpd.sh`
|
||||
- `opencport.sh`
|
||||
- `install_newpackage.sh`
|
||||
- `setrabqstart.sh`
|
||||
- `setxsd.sh`
|
||||
- `setngservice.sh`
|
||||
- `pongx.sh`
|
||||
|
||||
### Task 2: 固化部署上下文模型
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-09-auto-deploy-refactor.md`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/dto/InitPreviewRequest.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
|
||||
**Step 1: 区分“前端输入参数”和“后端部署上下文”**
|
||||
前端输入参数只承载业务输入;后端部署上下文承载可直接渲染文件和执行脚本的派生值。
|
||||
|
||||
**Step 2: 部署上下文仅保留初始化阶段所需客户化字段**
|
||||
部署上下文至少应包含:
|
||||
- `productType`
|
||||
- `supportMode`
|
||||
- `myCipsIdOrBic`
|
||||
- `mq01Ip`
|
||||
- `mq02Ip`
|
||||
- `app01Ip`
|
||||
- `app02Ip`
|
||||
- `dbIp`
|
||||
- `bicCode`
|
||||
- `cipsId`
|
||||
- `leiCode`
|
||||
- `organizationNameCn`
|
||||
- `organizationNameEn`
|
||||
- `signVendor`
|
||||
- `signIp`
|
||||
- `signPort`
|
||||
- `signDn`
|
||||
- `signPasswordEnc`
|
||||
|
||||
**Step 3: 平台级凭据不进入初始化上下文**
|
||||
以下内容由预置层提供,不作为初始化接口输入:
|
||||
- 数据库用户名 / 密码
|
||||
- RabbitMQ 管理员账号 / 密码
|
||||
- TiDB 管理员账号 / 密码
|
||||
|
||||
**Step 4: supportMode 显式按产品定义**
|
||||
- `ENTERPRISE`: `RABBITMQ` / `TLQ`
|
||||
- `INDIRECT`: `RABBITMQ` / `RABBITMQ_TLQ`
|
||||
- `DIRECT`: `RABBITMQ_TLQ` / `RABBITMQ_CFMQ`
|
||||
|
||||
### Task 3: 优先接管数据库初始化
|
||||
|
||||
**Status (2026-03-09):** 标准版已完成第一阶段落地,初始化编排已拆为 `DB_RENDER -> DB_APPLY`,并在 task staging 目录生成渲染后 SQL 包;后续仍需补强真实导库命令与结果校验。
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/initplan/step-command-profiles.json`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/toolsh/setsql.sh`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/sql_script/CMEP-DML.sql`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/sql_script/sql_script_tmp/CMTP-01-raq.sql`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/sql_script/sql_script_tmp/CMTP-01-tlq.sql`
|
||||
|
||||
**Step 1: 让后端输出渲染后的 SQL 包**
|
||||
输出至少包含:
|
||||
- `CMTP-01.sql`
|
||||
- `CMEP-DML.sql`
|
||||
- 其他导库所需脚本快照
|
||||
|
||||
**Step 2: 数据库导入凭据继续由预置层提供**
|
||||
初始化阶段不再输入和修改数据库账号密码;TMS 只负责:
|
||||
- 渲染 SQL
|
||||
- 组织导入顺序
|
||||
- 记录执行结果
|
||||
|
||||
**Step 3: DB_INIT 从“纯脚本黑盒”升级为“渲染 + 执行”双阶段**
|
||||
拆成:
|
||||
- `DB_RENDER`
|
||||
- `DB_APPLY`
|
||||
|
||||
**Step 4: 保留 vendor 导库脚本作为执行器,而不是模板引擎**
|
||||
如果短期不能完全替代 `setsql.sh`,则至少做到:
|
||||
- 模板由后端渲染
|
||||
- vendor 脚本只负责导入,不再负责替换占位符
|
||||
|
||||
### Task 4: 接管应用配置输出物
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/toolsh/setsptp.sh`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/cmsp/01/application-prd-ci01.properties`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/cmtp/01/application-prd-ci01.properties`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/cmsp/01/filetrans.properties`
|
||||
|
||||
**Step 1: 后端直接输出以下配置快照**
|
||||
- `application-prd*.properties`
|
||||
- `filetrans.properties`
|
||||
- 标准版 RabbitMQ 通道账号密码回写结果
|
||||
|
||||
**Step 2: 数据库连接凭据保持预置,不由初始化阶段改写**
|
||||
应用配置接管范围仅覆盖客户化字段,不覆盖预置层数据库连接凭据。
|
||||
|
||||
**Step 3: 将 setsptp.sh 从“渲染 + 拷贝”降级为“拷贝器”**
|
||||
短期目标:
|
||||
- 配置替换由后端完成
|
||||
- `setsptp.sh` 仅做 JAR / 目录拷贝
|
||||
|
||||
**Step 4: 禁止敏感信息输出到步骤日志**
|
||||
渲染日志中不得出现密码明文或密文。
|
||||
|
||||
### Task 5: 接管 Nginx / Web 客户化
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/initplan/step-command-profiles.json`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/software/setngx.sh`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/toolsh/setweb.sh`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/software/nginx.conf`
|
||||
|
||||
**Step 1: 新增 Nginx/Web 相关步骤**
|
||||
至少新增:
|
||||
- `NGINX_CONFIG_APPLY`
|
||||
- `WEB_CONFIG_APPLY`
|
||||
|
||||
**Step 2: 后端渲染输出物**
|
||||
- `nginx.conf`
|
||||
- `organization.json`
|
||||
- Web 发布目录快照或发布日志
|
||||
|
||||
**Step 3: 不再把 setweb.sh 作为首批高收益改造对象本体**
|
||||
优先由后端接管组织名称和 nginx upstream 渲染,再决定是否保留 `setweb.sh` 作为拷贝器。
|
||||
|
||||
### Task 6: 建立显式启动顺序
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/initplan/step-command-profiles.json`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/sh/sh/stcmep_cent.sh`
|
||||
|
||||
**Step 1: 将“部署”和“启动”拆开**
|
||||
当前 `DEPLOY_RECEIVER` 不能同时承载“拷贝应用”和“启动服务”两种职责。
|
||||
|
||||
**Step 2: 新增启动步骤**
|
||||
建议新增:
|
||||
- `START_CMSP_CMTP`
|
||||
- `START_NGINX`
|
||||
- `POST_CHECK`
|
||||
|
||||
**Step 3: 定义启动顺序**
|
||||
推荐顺序:
|
||||
1. 参数校验
|
||||
2. 渲染配置和 SQL
|
||||
3. 准备 license
|
||||
4. 初始化 RabbitMQ / TLQ
|
||||
5. 初始化数据库
|
||||
6. 下发 CMSP / CMTP 配置
|
||||
7. 下发 Nginx / Web 配置
|
||||
8. 启动 CMSP / CMTP
|
||||
9. 启动 Nginx
|
||||
10. 验收检查
|
||||
|
||||
### Task 7: 建立失败边界和输出物
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
|
||||
**Step 1: 明确失败即停边界**
|
||||
- MQ 初始化失败后停止后续步骤
|
||||
- 数据库导入失败后不启动应用
|
||||
- 应用配置失败后不覆盖旧版本
|
||||
- Web 发布失败不影响已启动后台服务
|
||||
|
||||
**Step 2: 输出四类审计产物**
|
||||
每次部署任务至少输出:
|
||||
- 渲染后的配置快照
|
||||
- 渲染后的 SQL 包
|
||||
- 步骤执行日志
|
||||
- 最终部署结果摘要
|
||||
|
||||
### Task 8: 第一批高收益实施顺序
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/initplan/step-command-profiles.json`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: 第一优先级**
|
||||
- 标准版 SQL 渲染输出物
|
||||
- DB_INIT 拆分为渲染与执行
|
||||
- 数据库导入结果可审计化
|
||||
|
||||
**Step 2: 第二优先级**
|
||||
- CMSP / CMTP / filetrans 配置输出物
|
||||
- RabbitMQ / TLQ 初始化后的验收
|
||||
|
||||
**Step 3: 第三优先级**
|
||||
- Nginx / Web 渲染与发布
|
||||
- 显式启动顺序和启动后检查
|
||||
|
||||
### Task 9: 验收标准
|
||||
|
||||
**Files:**
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/document/CIPS标准收发器安装部署手册1.0(企业信创版)-V1.3.2.pdf`
|
||||
|
||||
**Step 1: 参数层验收**
|
||||
确认后端输入能完整覆盖初始化阶段真正需要的客户化字段,特别是:
|
||||
- 支持方式
|
||||
- 组织名称
|
||||
- Nginx/Web 所需字段
|
||||
- RabbitMQ 通道账号密码
|
||||
|
||||
**Step 2: 配置层验收**
|
||||
确认输出物中不再残留以下占位符:
|
||||
- `AAAABBBBXXX`
|
||||
- `sptp.01app.ip`
|
||||
- `sptp.02app.ip`
|
||||
- `sptp.01db.ip`
|
||||
- `01tlq.ip`
|
||||
- `02tlq.ip`
|
||||
- `mybiccode`
|
||||
- `mycipsid`
|
||||
- `myleicode`
|
||||
|
||||
**Step 3: 部署层验收**
|
||||
确认:
|
||||
- MQ / TLQ 正常可连
|
||||
- TiDB 库和 schema 正常创建
|
||||
- CMSP / CMTP 正常启动
|
||||
- Nginx 正常代理
|
||||
- Web 首页可访问
|
||||
- `organization.json` 为客户实际机构名称
|
||||
|
||||
### Task 10: 非目标项
|
||||
|
||||
**Step 1: 明确本轮不做**
|
||||
以下内容不作为本轮主线:
|
||||
- 大规模直接修改 vendor 原始脚本
|
||||
- 修改介质 readme/document 作为交付主成果
|
||||
- 将所有 cpconfig 依赖一次性彻底移除
|
||||
- 将平台级凭据纳入初始化接口输入
|
||||
|
||||
**Step 2: 后续演进原则**
|
||||
优先让 TMS 接管“产物和编排”,再逐步削弱 vendor shell 的模板职责。
|
||||
@@ -0,0 +1,175 @@
|
||||
# Standard DB Render Apply Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 将标准版初始化中的数据库步骤从黑盒 `DB_INIT` 拆分为 `DB_RENDER` 和 `DB_APPLY`,生成可审计的 SQL 输出物,并保留数据库凭据在预制层处理。
|
||||
|
||||
**Architecture:** 标准版步骤编排改为先在 task staging 目录生成渲染后的 SQL 包,再由单独的 `DB_APPLY` 步骤将 SQL 包落位到标准收发器数据库加载目录,并按预置命令执行导库。渲染逻辑直接基于 `sql_script_tmp` 模板和 `CMEP-DML.sql/CMEP.sql`,不依赖 vendor 脚本副作用,也不修改 vendor 原始介质。
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot 3.5, Jackson, JUnit 5, Maven
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 固化步骤编排与计划文档
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-09-standard-db-render-apply-plan.md`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/initplan/step-command-profiles.json`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
在 `InitServiceTest` 增加标准版断言:
|
||||
- `ENTERPRISE + RABBITMQ` 不再包含 `DB_INIT`
|
||||
- 改为包含 `DB_RENDER` 和 `DB_APPLY`
|
||||
- `ENTERPRISE + TLQ`、`INDIRECT + RABBITMQ`、`INDIRECT + RABBITMQ_TLQ` 同样断言
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest test`
|
||||
Expected: FAIL,提示仍存在 `DB_INIT` 或缺少 `DB_RENDER/DB_APPLY`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
修改 `InitService.defaultSteps()` 与 `step-command-profiles.json`:
|
||||
- 标准版统一把 `DB_INIT` 替换成 `DB_RENDER`、`DB_APPLY`
|
||||
- 直参版保持 `DB_INIT`
|
||||
- 标准版 `DB_RENDER` 使用 `internal::db_render`
|
||||
- 标准版 `DB_APPLY` 使用 `internal::db_apply`
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 2: 先写标准版 SQL 渲染失败用例
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/application.yml`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/config/application.yml.example`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
新增两个执行器测试:
|
||||
- `shouldRenderStandardDatabaseSqlPackageForEnterpriseRabbitmq`
|
||||
- 在临时目录准备 `cpconfig.cfg`、`sql_script/CMEP-DML.sql`、`sql_script/CMEP.sql`、`sql_script/SCHEMA-DDL.sql`、`sql_script/UP-ORG-INFO.sql`、`sql_script/sql_script_tmp/CMTP-01-raq.sql`
|
||||
- 执行 `internal::db_render`
|
||||
- 断言 staging 下存在 `db/sql/CMTP-01.sql`、`CMEP-DML.sql`、`CMEP.sql` 等文件
|
||||
- 断言 `AAAABBBBXXX`、`01tlq.ip`、`02tlq.ip`、`sptp.01app.ip` 被替换
|
||||
- `shouldApplyStandardDatabaseSqlPackageToLoadDir`
|
||||
- 基于上一步产物执行 `internal::db_apply`
|
||||
- 断言 SQL 被复制到 configured load dir
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest test`
|
||||
Expected: FAIL,提示不支持 `db_render/db_apply` 或目标文件不存在
|
||||
|
||||
**Step 3: Define minimal config surface**
|
||||
|
||||
为标准版数据库处理增加配置项:
|
||||
- `standard-db-script-base-dir`
|
||||
- `standard-db-load-dir`
|
||||
- `standard-db-apply-command`
|
||||
- `standard-db-entry-sql-file-name`
|
||||
|
||||
并在两个 `application.yml` 中补注释和默认值。
|
||||
|
||||
**Step 4: Re-run focused test**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest test`
|
||||
Expected: 仍失败,但失败点只剩未实现逻辑
|
||||
|
||||
### Task 3: 实现标准版 DB_RENDER
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Add internal step dispatch**
|
||||
|
||||
在 `executeInternal()` 新增:
|
||||
- `case "db_render" -> executeStandardDbRender(task, step)`
|
||||
|
||||
**Step 2: Implement render behavior**
|
||||
|
||||
新增最小实现:
|
||||
- 仅对标准版生效;直参版返回 skipped
|
||||
- 从 `standard-db-script-base-dir` 读取:
|
||||
- `sql_script/CMEP-DML.sql`
|
||||
- `sql_script/CMEP.sql`
|
||||
- `sql_script/SCHEMA-DDL.sql`
|
||||
- `sql_script/UP-ORG-INFO.sql`
|
||||
- `sql_script/sql_script_tmp/CMTP-01-raq.sql` 或 `CMTP-01-tlq.sql`
|
||||
- 渲染输出到 `staging/<taskId>/db/sql/`
|
||||
- 输出文件名固定为:
|
||||
- `CMEP-DML.sql`
|
||||
- `CMEP.sql`
|
||||
- `SCHEMA-DDL.sql`
|
||||
- `UP-ORG-INFO.sql`
|
||||
- `CMTP-01.sql`
|
||||
- 渲染规则复用 `buildStandardReplacements()`,并补充 SQL 占位映射:
|
||||
- `AAAABBBBXXX -> orgCode`
|
||||
- `01tlq.ip -> mq_01_ip`
|
||||
- `02tlq.ip -> mq_02_ip`
|
||||
- `sptp.01app.ip -> sptp_01app_ip`
|
||||
- `sptp.02app.ip -> sptp_02app_ip`
|
||||
- `mybiccode/mycipsid/myleicode -> 对应机构号映射`
|
||||
- `CMEP.sql` 里的 `source /home/cmep4i/mysql/loadfilepath/...` 保持不变,不在渲染阶段改路径
|
||||
|
||||
**Step 3: Run render test**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldRenderStandardDatabaseSqlPackageForEnterpriseRabbitmq test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 4: 实现标准版 DB_APPLY
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Add internal step dispatch**
|
||||
|
||||
在 `executeInternal()` 新增:
|
||||
- `case "db_apply" -> executeStandardDbApply(task, step)`
|
||||
|
||||
**Step 2: Implement apply behavior**
|
||||
|
||||
最小实现:
|
||||
- 仅对标准版生效;直参版返回 skipped
|
||||
- 从 `staging/<taskId>/db/sql/` 读取渲染后 SQL 包
|
||||
- 复制到 `standard-db-load-dir`
|
||||
- `SIMULATE` 模式只校验 staging 产物存在并返回 success
|
||||
- `LOCAL` 模式:
|
||||
- 若 `standard-db-apply-command` 为空,只做落位并返回 success
|
||||
- 若不为空,则以该命令执行导库,工作目录指向 `standard-db-load-dir`
|
||||
- 成功日志中明确输出 staging 路径、load dir、是否执行导库命令
|
||||
|
||||
**Step 3: Run apply test**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldApplyStandardDatabaseSqlPackageToLoadDir test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 5: 全量回归与最小文档同步
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-09-auto-deploy-refactor.md`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Sync plan wording**
|
||||
|
||||
在重构计划中把 Task 3 结果更新为已实现方向:标准版数据库改为 `DB_RENDER / DB_APPLY`。
|
||||
|
||||
**Step 2: Run targeted tests**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest,ConfigurableInitStepExecutorTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Run compile verification**
|
||||
|
||||
Run: `mvn -q -DskipTests compile`
|
||||
Expected: PASS
|
||||
@@ -0,0 +1,51 @@
|
||||
# Deployment Doc Chinese Localization Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Convert the full TMS deployment manual and deployment-related README sections to Chinese while preserving commands, paths, config keys, and interface names.
|
||||
|
||||
**Architecture:** Keep the existing document structure and only localize human-readable prose. Do not change command examples, file paths, YAML keys, API paths, or script names. Limit the scope to `docs/deployment/tms-deployment.md` and the deployment-related part of `README.md`.
|
||||
|
||||
**Tech Stack:** Markdown documentation
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Localize the deployment manual
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/deployment/tms-deployment.md`
|
||||
|
||||
**Step 1: Translate headings and narrative text**
|
||||
|
||||
Convert the deployment manual to Chinese, preserving commands, paths, config keys, API paths, and script names.
|
||||
|
||||
**Step 2: Keep structure and examples stable**
|
||||
|
||||
Do not reorder sections or alter shell/YAML examples except for surrounding explanatory text.
|
||||
|
||||
### Task 2: Localize deployment-related README content
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/README.md`
|
||||
|
||||
**Step 1: Translate deployment entry and notes**
|
||||
|
||||
Convert the deployment guide link, Jar deployment section title, and deployment-related notes to Chinese.
|
||||
|
||||
**Step 2: Avoid unrelated README changes**
|
||||
|
||||
Keep non-deployment sections as-is.
|
||||
|
||||
### Task 3: Consistency review
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: Review focused diff**
|
||||
|
||||
Run: `git diff -- README.md docs/deployment/tms-deployment.md`
|
||||
Expected: only prose localization changes.
|
||||
|
||||
**Step 2: Spot-check for path/config drift**
|
||||
|
||||
Verify commands, paths, config keys, and script names remain unchanged.
|
||||
@@ -0,0 +1,214 @@
|
||||
# File Upload FileId Design
|
||||
|
||||
**Date:** 2026-03-10
|
||||
|
||||
## Goal
|
||||
|
||||
为 TMS 提供一个通用文件上传能力,前端在初始化页面选择文件后先上传,后端返回 `fileId`,初始化任务创建和执行阶段继续只引用 `fileId`,不直接传输文件流。
|
||||
|
||||
## Scope
|
||||
|
||||
本设计只覆盖:
|
||||
- 通用文件上传接口
|
||||
- 文件元信息记录
|
||||
- 与 CISD 初始化的 `fileId` 约定
|
||||
- 前端调用时序
|
||||
|
||||
本设计不覆盖:
|
||||
- 文件下载
|
||||
- 文件删除回收
|
||||
- 秒传/去重
|
||||
- 文件内容级业务校验
|
||||
- 初始化主流程改造
|
||||
|
||||
## Current State
|
||||
|
||||
当前初始化接口:
|
||||
- `/api/v1/init/preview`
|
||||
- `/api/v1/init/tasks`
|
||||
|
||||
都只接收 JSON `InitPreviewRequest`,不接收 `multipart/form-data`。
|
||||
|
||||
当前初始化模块已经完整依赖以下 `fileId` 字段:
|
||||
- `licenses.receiverLicenseFileId`
|
||||
- `licenses.cfgZipFileId`
|
||||
- `mq.tlqLicenseFileId`
|
||||
- `mq.cfmqConfigFileId`
|
||||
|
||||
当前执行器通过 `upload-base-dir/<fileId>` 或 `upload-base-dir/<fileId>/<preferredFileName>` 解析文件。
|
||||
|
||||
## Design Choice
|
||||
|
||||
采用:`通用上传接口 + 初始化继续引用 fileId`
|
||||
|
||||
不采用:`初始化任务接口直接接收 MultipartFile`
|
||||
|
||||
原因:
|
||||
- 初始化主流程已经基于 `fileId`,继续沿用改动最小
|
||||
- 上传与初始化创建失败语义不同,拆分更清晰
|
||||
- 通用上传能力后续可被证书、升级、备份等模块复用
|
||||
|
||||
## API Design
|
||||
|
||||
### 1. Upload API
|
||||
|
||||
`POST /api/v1/files/upload`
|
||||
|
||||
Content-Type:`multipart/form-data`
|
||||
|
||||
表单字段:
|
||||
- `file`: 必填,文件本体
|
||||
- `bizType`: 可选,默认 `GENERIC`
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"fileId": "f_20260310_ab12cd34",
|
||||
"bizType": "INIT",
|
||||
"originalFilename": "cmep.license",
|
||||
"contentType": "application/octet-stream",
|
||||
"size": 2048,
|
||||
"storagePath": "/home/tms/uploads/f_20260310_ab12cd34/cmep.license"
|
||||
},
|
||||
"timestamp": 1741600000000,
|
||||
"traceId": "xxx"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. File Detail API
|
||||
|
||||
`GET /api/v1/files/{fileId}`
|
||||
|
||||
用于页面回显和排障,不参与初始化主链路。
|
||||
|
||||
## Storage Design
|
||||
|
||||
### Storage root
|
||||
|
||||
- `/home/tms/uploads`
|
||||
|
||||
### Directory layout
|
||||
|
||||
- `/home/tms/uploads/<fileId>/`
|
||||
- `/home/tms/uploads/<fileId>/<originalFilename>`
|
||||
|
||||
这样做的原因:
|
||||
- 与当前执行器解析逻辑兼容
|
||||
- 保留原始文件名,便于排查
|
||||
- 每个 `fileId` 单独目录,避免同名覆盖
|
||||
|
||||
## Database Design
|
||||
|
||||
新增表:`sys_file_record`
|
||||
|
||||
字段建议:
|
||||
- `id` bigint pk
|
||||
- `file_id` varchar(128) unique not null
|
||||
- `biz_type` varchar(32) not null
|
||||
- `original_filename` varchar(256) not null
|
||||
- `stored_filename` varchar(256) not null
|
||||
- `storage_path` varchar(512) not null
|
||||
- `content_type` varchar(128) null
|
||||
- `file_size` bigint not null
|
||||
- `sha256` varchar(64) null
|
||||
- `status` varchar(16) not null
|
||||
- `create_time` datetime not null
|
||||
- `update_time` datetime not null
|
||||
|
||||
状态值当前只需要:
|
||||
- `ACTIVE`
|
||||
- `DELETED`
|
||||
|
||||
## Backend Module Layout
|
||||
|
||||
新增模块:
|
||||
|
||||
```text
|
||||
src/main/java/com/cisd/tms/modules/file
|
||||
├── controller/internal
|
||||
│ └── FileInternalController.java
|
||||
├── dto
|
||||
│ ├── FileUploadResponse.java
|
||||
│ └── FileDetailResponse.java
|
||||
├── entity
|
||||
│ └── FileRecordEntity.java
|
||||
├── repository
|
||||
│ └── FileRecordRepository.java
|
||||
└── service
|
||||
└── FileService.java
|
||||
```
|
||||
|
||||
资源文件:
|
||||
|
||||
```text
|
||||
src/main/resources/mapper/file/FileRecordMapper.xml
|
||||
src/main/resources/db/migration/V3__create_sys_file_record.sql
|
||||
```
|
||||
|
||||
## Validation Boundary
|
||||
|
||||
### Upload API validates
|
||||
|
||||
- 文件非空
|
||||
- 原始文件名非空
|
||||
- 文件大小不超过配置上限
|
||||
- `bizType` 枚举合法
|
||||
|
||||
### Init API continues to validate
|
||||
|
||||
- `fileId` 格式
|
||||
- 按版本和 `mqType` 的必填规则
|
||||
- 执行阶段按 `fileId` 是否能找到对应文件
|
||||
|
||||
## Frontend Flow
|
||||
|
||||
1. 用户在初始化页面选择文件
|
||||
2. 前端立即调用上传接口
|
||||
3. 上传成功后拿到 `fileId`
|
||||
4. 前端将 `fileId` 保存到表单状态
|
||||
5. 用户点击“预检”或“创建任务”时,仅提交 JSON
|
||||
|
||||
字段映射:
|
||||
- 收发器 license -> `licenses.receiverLicenseFileId`
|
||||
- TLQ 配置/许可证 -> `mq.tlqLicenseFileId`
|
||||
- CFMQ 配置 -> `mq.cfmqConfigFileId`
|
||||
- 直参 cfg 包 -> `licenses.cfgZipFileId`
|
||||
|
||||
## Configuration
|
||||
|
||||
建议新增:
|
||||
|
||||
```yaml
|
||||
tms:
|
||||
file:
|
||||
upload-base-dir: /home/tms/uploads
|
||||
max-file-size-mb: 50
|
||||
```
|
||||
|
||||
后续可考虑让初始化执行器复用同一配置根,而不是再单独维护一份路径配置。
|
||||
|
||||
## Risks
|
||||
|
||||
1. 当前初始化执行器已有 `tms.init.executor.upload-base-dir`,如果新增 `tms.file.upload-base-dir` 而两者不一致,会导致上传成功但执行阶段找不到文件。
|
||||
2. 如果上传接口只保留原始文件名而不做额外清洗,需要限制危险文件名字符。
|
||||
3. 目前不做业务文件类型校验,上传成功不代表该文件一定适合初始化使用。
|
||||
|
||||
## Recommendation
|
||||
|
||||
先实现最小闭环:
|
||||
- 上传接口
|
||||
- 查询接口
|
||||
- 文件元信息表
|
||||
- 与初始化 `fileId` 流程对齐
|
||||
|
||||
不在第一版加入:
|
||||
- 下载
|
||||
- 删除
|
||||
- 清理任务
|
||||
- 哈希去重
|
||||
- 扩展名白名单
|
||||
@@ -0,0 +1,251 @@
|
||||
# File Upload FileId Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add a reusable file upload module that stores uploaded files under `/home/tms/uploads`, returns `fileId`, and keeps CISD initialization flowing through existing `fileId` fields.
|
||||
|
||||
**Architecture:** Introduce a small `modules/file` module with internal upload and query APIs, one metadata table, and a service that owns fileId generation plus filesystem persistence. Keep the init module unchanged at the API boundary except for sharing the same upload storage root and documenting the front-end workflow.
|
||||
|
||||
**Tech Stack:** Spring Boot MVC multipart upload, MyBatis-Plus repository pattern, Flyway SQL migration, JUnit 5
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add file upload persistence contract
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V3__create_sys_file_record.sql`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/file/entity/FileRecordEntity.java`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/file/repository/FileRecordRepository.java`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/mapper/file/FileRecordMapper.xml`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add a test that expects file metadata to be persisted with:
|
||||
- generated `fileId`
|
||||
- original file name
|
||||
- absolute storage path
|
||||
- `ACTIVE` status
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=FileServiceTest#shouldPersistUploadedFileMetadata test`
|
||||
Expected: FAIL because file module classes and table mapping do not exist.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement the entity, mapper/repository, and migration with only the fields needed by upload/query.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=FileServiceTest#shouldPersistUploadedFileMetadata test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/resources/db/migration/V3__create_sys_file_record.sql \
|
||||
src/main/java/com/cisd/tms/modules/file/entity/FileRecordEntity.java \
|
||||
src/main/java/com/cisd/tms/modules/file/repository/FileRecordRepository.java \
|
||||
src/main/resources/mapper/file/FileRecordMapper.xml \
|
||||
src/test/java/com/cisd/tms/modules/file/FileServiceTest.java
|
||||
git commit -m "feat: add file upload persistence contract"
|
||||
```
|
||||
|
||||
### Task 2: Implement upload service
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/file/service/FileService.java`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/file/dto/FileUploadResponse.java`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/file/dto/FileDetailResponse.java`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/common/config/properties/FileStorageProperties.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/application.yml`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/config/application.yml.example`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add tests that assert:
|
||||
- uploaded file is saved under `<upload-base-dir>/<fileId>/<originalFilename>`
|
||||
- blank file upload is rejected
|
||||
- oversize upload is rejected
|
||||
- returned DTO contains `fileId`, size, storagePath
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=FileServiceTest test`
|
||||
Expected: FAIL because service and properties do not exist.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement:
|
||||
- `fileId` generation
|
||||
- file name sanitization
|
||||
- directory creation
|
||||
- file save
|
||||
- metadata insert
|
||||
- detail query by `fileId`
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=FileServiceTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/com/cisd/tms/modules/file/service/FileService.java \
|
||||
src/main/java/com/cisd/tms/modules/file/dto/FileUploadResponse.java \
|
||||
src/main/java/com/cisd/tms/modules/file/dto/FileDetailResponse.java \
|
||||
src/main/java/com/cisd/tms/common/config/properties/FileStorageProperties.java \
|
||||
src/main/resources/application.yml config/application.yml.example \
|
||||
src/test/java/com/cisd/tms/modules/file/FileServiceTest.java
|
||||
git commit -m "feat: implement file upload service"
|
||||
```
|
||||
|
||||
### Task 3: Expose internal file APIs
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/file/controller/internal/FileInternalController.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add controller tests for:
|
||||
- `POST /api/v1/files/upload`
|
||||
- `GET /api/v1/files/{fileId}`
|
||||
|
||||
Verify upload uses multipart, returns `ApiResponse`, and query returns stored metadata.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=FileInternalControllerTest test`
|
||||
Expected: FAIL because controller does not exist.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Expose two endpoints only:
|
||||
- upload
|
||||
- detail query
|
||||
|
||||
Keep them internal-only under `/api/v1/files`.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=FileInternalControllerTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/com/cisd/tms/modules/file/controller/internal/FileInternalController.java \
|
||||
src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java \
|
||||
src/test/java/com/cisd/tms/modules/file/FileServiceTest.java
|
||||
git commit -m "feat: expose internal file upload api"
|
||||
```
|
||||
|
||||
### Task 4: Align init module with shared upload root and docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/application.yml`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/config/application.yml.example`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/openapi/cisd-init.openapi.yaml`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/openapi/cisd-init.openapi.json`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/README.md`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add or update tests to assert:
|
||||
- init executor upload root matches the shared upload root config
|
||||
- docs clearly state init APIs accept `fileId`, not multipart files
|
||||
- README documents front-end flow: upload first, then create init task
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldDocumentInitFileIdFlow test`
|
||||
Expected: FAIL because docs/config are not aligned yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Align configuration and docs without changing init request DTO semantics.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldDocumentInitFileIdFlow test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java \
|
||||
src/main/resources/application.yml config/application.yml.example \
|
||||
docs/openapi/cisd-init.openapi.yaml docs/openapi/cisd-init.openapi.json README.md \
|
||||
src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java
|
||||
git commit -m "docs: align init fileId flow with upload api"
|
||||
```
|
||||
|
||||
### Task 5: Add upload API OpenAPI spec
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/openapi/cisd-init.openapi.yaml`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/openapi/cisd-init.openapi.json`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add a documentation test or assertion that the upload route and response fields are present in the OpenAPI output.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=FileInternalControllerTest#shouldDocumentUploadApi test`
|
||||
Expected: FAIL because the route is not documented yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Document:
|
||||
- upload API
|
||||
- query API
|
||||
- multipart request
|
||||
- `fileId` response
|
||||
- init API references to upload-first workflow
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=FileInternalControllerTest#shouldDocumentUploadApi test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/openapi/cisd-init.openapi.yaml docs/openapi/cisd-init.openapi.json \
|
||||
src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java
|
||||
git commit -m "docs: add file upload api spec"
|
||||
```
|
||||
|
||||
### Task 6: Final verification
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: Run file module tests**
|
||||
|
||||
Run: `mvn -q -Dtest=FileServiceTest,FileInternalControllerTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run init-related tests**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest,ConfigurableInitStepExecutorTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Run compile**
|
||||
|
||||
Run: `mvn -q -DskipTests compile`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Review focused diff**
|
||||
|
||||
Run: `git diff -- src/main/java/com/cisd/tms/modules/file src/main/resources/db/migration/V3__create_sys_file_record.sql src/main/resources/mapper/file src/main/resources/application.yml config/application.yml.example README.md docs/openapi/cisd-init.openapi.yaml docs/openapi/cisd-init.openapi.json`
|
||||
Expected: only file upload module, config, README, and OpenAPI/doc alignment changes.
|
||||
@@ -0,0 +1,211 @@
|
||||
# Init Execute Async Resume Design
|
||||
|
||||
**Date:** 2026-03-10
|
||||
|
||||
## Goal
|
||||
|
||||
解决初始化执行接口同步阻塞导致的请求超时问题,并支持任务在失败后从第一个未成功步骤继续执行,而不是每次都从头开始。
|
||||
|
||||
## Scope
|
||||
|
||||
本设计覆盖:
|
||||
- `/api/v1/init/tasks/{taskId}/execute` 改为异步受理
|
||||
- 初始化任务后台执行模型
|
||||
- 步骤级断点续跑规则
|
||||
- 同一任务的重复触发保护
|
||||
- 现有查询接口与状态语义调整
|
||||
|
||||
本设计不覆盖:
|
||||
- 分布式任务调度
|
||||
- 多实例间执行协调
|
||||
- 服务重启后的自动恢复调度
|
||||
- 新增前端页面
|
||||
- 初始化步骤编排本身的变更
|
||||
|
||||
## Current State
|
||||
|
||||
当前执行入口是同步的:
|
||||
- 控制器直接调用 `InitService.executeTask(taskId)`
|
||||
- 服务层在请求线程内串行执行全部步骤
|
||||
- 任一步骤失败后立刻中断
|
||||
|
||||
当前问题有两个:
|
||||
- 任务执行时间较长时,HTTP 请求容易超时
|
||||
- 再次执行时没有跳过已成功步骤的逻辑,会从第一步重新开始
|
||||
|
||||
当前已有可复用基础:
|
||||
- 任务主表 `tms_init_task`
|
||||
- 任务步骤表 `tms_init_task_step`
|
||||
- 步骤状态字段:`PENDING` / `RUNNING` / `SUCCESS` / `FAILED`
|
||||
- 现有步骤详情和日志查询接口
|
||||
|
||||
## Design Choice
|
||||
|
||||
采用:`应用内异步执行 + 基于步骤状态续跑`
|
||||
|
||||
不采用:`继续同步执行`
|
||||
|
||||
原因:
|
||||
- 无法避免请求超时
|
||||
- 与实际长耗时初始化场景不匹配
|
||||
|
||||
不采用:`数据库任务队列 + 独立 worker`
|
||||
|
||||
原因:
|
||||
- 当前需求重点是先解决超时和重跑问题
|
||||
- 现阶段引入完整调度系统成本过高
|
||||
|
||||
## API Design
|
||||
|
||||
### Execute API
|
||||
|
||||
`POST /api/v1/init/tasks/{taskId}/execute`
|
||||
|
||||
接口语义调整为:
|
||||
- 只负责受理执行请求
|
||||
- 不等待全部步骤完成
|
||||
- 返回当前任务快照
|
||||
|
||||
返回字段继续复用 `InitTaskExecuteResponse`,但语义变为:
|
||||
- `taskId`: 任务号
|
||||
- `status`: 受理后的当前状态,通常为 `RUNNING`
|
||||
- `totalSteps`: 总步骤数
|
||||
- `successSteps`: 当前已成功步骤数
|
||||
|
||||
前端后续继续通过以下接口轮询:
|
||||
- `GET /api/v1/init/tasks/{taskId}`
|
||||
- `GET /api/v1/init/tasks/{taskId}/steps`
|
||||
- `GET /api/v1/init/tasks/{taskId}/steps/{stepNo}/log`
|
||||
|
||||
## Execution Model
|
||||
|
||||
### 1. Request thread
|
||||
|
||||
请求线程只做以下事情:
|
||||
- 校验任务存在且有步骤
|
||||
- 检查是否已经在执行中
|
||||
- 将任务状态置为 `RUNNING`
|
||||
- 提交后台执行任务
|
||||
- 立即返回
|
||||
|
||||
### 2. Background worker
|
||||
|
||||
后台线程负责真正串行执行步骤:
|
||||
- 重新加载任务与步骤
|
||||
- 识别续跑起点
|
||||
- 逐步执行
|
||||
- 每步结束后立刻落库状态、消息、日志路径、退出码
|
||||
- 汇总任务最终状态
|
||||
|
||||
### 3. Single-task guard
|
||||
|
||||
同一 `taskId` 在一个应用实例内只允许一个执行线程:
|
||||
- 若任务已在本实例执行中,再次调用 `execute` 不再重复启动
|
||||
- 接口直接返回当前快照
|
||||
|
||||
实现上可使用进程内 `ConcurrentHashMap<String, Future<?>>` 或等价结构维护活动任务。
|
||||
|
||||
## Resume Rule
|
||||
|
||||
再次执行时采用以下规则:
|
||||
|
||||
1. 所有 `SUCCESS` 步骤直接跳过,不重跑
|
||||
2. 从第一个非 `SUCCESS` 步骤开始继续
|
||||
3. 后续步骤按原顺序执行
|
||||
4. 若本次再次失败,当前失败步骤状态记为 `FAILED`,任务状态记为 `FAILED`
|
||||
|
||||
### RUNNING residue handling
|
||||
|
||||
如果上一次执行过程中服务异常退出,可能残留 `RUNNING` 步骤。
|
||||
|
||||
本设计约定:
|
||||
- 新一轮执行开始前,将残留 `RUNNING` 步骤视为未完成
|
||||
- 统一重置为 `FAILED`
|
||||
- 续跑时从第一个非 `SUCCESS` 步骤开始
|
||||
|
||||
这样可以保留“上次停在这里”的事实,并避免把 `RUNNING` 误判为仍在健康执行。
|
||||
|
||||
## Status Semantics
|
||||
|
||||
### Task status
|
||||
|
||||
- `PENDING`: 任务已创建,尚未开始
|
||||
- `RUNNING`: 已受理且后台执行中
|
||||
- `SUCCESS`: 全部步骤成功
|
||||
- `FAILED`: 某一步失败并已停止
|
||||
|
||||
### Step status
|
||||
|
||||
- `PENDING`: 从未执行
|
||||
- `RUNNING`: 当前正在执行
|
||||
- `SUCCESS`: 已成功,后续重试跳过
|
||||
- `FAILED`: 最近一次执行失败,后续重试从此处继续
|
||||
|
||||
## Concurrency Policy
|
||||
|
||||
### Same task
|
||||
|
||||
同一任务重复点击执行:
|
||||
- 若本实例检测到该任务已在执行,直接返回当前状态
|
||||
- 不再启动第二个后台线程
|
||||
|
||||
### Different tasks
|
||||
|
||||
不同任务是否允许并发执行,第一版保持当前默认:
|
||||
- 允许不同 `taskId` 并行受理
|
||||
- 不额外加全局串行锁
|
||||
|
||||
原因:
|
||||
- 当前需求只明确要求单任务避免重入
|
||||
- 是否需要全局串行,取决于目标主机资源和脚本互斥关系,后续再单独设计
|
||||
|
||||
## Data Changes
|
||||
|
||||
本设计优先复用现有表结构,不新增表字段。
|
||||
|
||||
依赖现有字段即可表达:
|
||||
- 任务当前状态
|
||||
- 步骤当前状态
|
||||
- 失败消息
|
||||
- 日志路径
|
||||
- 退出码
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
需要补的测试重点:
|
||||
|
||||
1. 异步受理
|
||||
- `execute` 调用应快速返回
|
||||
- 后台线程继续推进步骤状态
|
||||
|
||||
2. 失败后续跑
|
||||
- 首次执行在某步失败
|
||||
- 再次执行时跳过已 `SUCCESS` 步骤
|
||||
- 从失败步骤重新执行并继续后续步骤
|
||||
|
||||
3. 残留 `RUNNING`
|
||||
- 构造上一次异常中断留下的 `RUNNING` 步骤
|
||||
- 重试时应从该步骤继续
|
||||
|
||||
4. 单任务重复触发保护
|
||||
- 同一个 `taskId` 在执行中再次调用
|
||||
- 不应重复启动第二个执行线程
|
||||
|
||||
## Risks
|
||||
|
||||
1. 应用内异步执行依赖当前进程存活,若服务重启,运行中的任务不会自动恢复。
|
||||
2. 若底层初始化脚本本身不具备完全幂等性,虽然步骤级跳过已成功步骤可以减少重复执行,但失败步骤重试仍可能触发环境冲突。
|
||||
3. 当前只做单实例内去重;若后续部署多实例,需要补分布式锁或数据库抢占机制。
|
||||
|
||||
## Recommendation
|
||||
|
||||
先实现最小闭环:
|
||||
- `execute` 异步受理
|
||||
- 单任务进程内防重入
|
||||
- 从第一个非 `SUCCESS` 步骤续跑
|
||||
- 残留 `RUNNING` 视为失败点恢复
|
||||
|
||||
不在第一版加入:
|
||||
- 分布式调度
|
||||
- 自动恢复未完成任务
|
||||
- 新状态枚举或额外任务队列表
|
||||
@@ -0,0 +1,188 @@
|
||||
# Init Execute Async Resume Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Make `/api/v1/init/tasks/{taskId}/execute` return immediately after accepting work, then run init steps in the background and resume from the first non-`SUCCESS` step on retry.
|
||||
|
||||
**Architecture:** Keep the existing task table and step table as the source of truth. Split `InitService.executeTask` into an accept-and-dispatch path plus a background execution path, and use existing step statuses to determine where to resume. Add a lightweight in-process guard so one `taskId` cannot be started twice concurrently in the same application instance.
|
||||
|
||||
**Tech Stack:** Spring Boot service/controller layer, Java concurrency utilities, MyBatis-style repositories already present in the module, JUnit 5
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock down current behavior with service tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Add tests that assert:
|
||||
- `executeTask(taskId)` returns before a deliberately blocking executor finishes all steps
|
||||
- retry skips steps already marked `SUCCESS` and resumes from the first `FAILED` step
|
||||
- residual `RUNNING` steps are treated as the resume point on retry
|
||||
- duplicate execute calls for the same running task do not launch a second execution
|
||||
|
||||
**Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest#shouldAcceptTaskExecutionAsynchronously,InitServiceTest#shouldResumeFromFirstFailedStepOnRetry,InitServiceTest#shouldResumeFromResidualRunningStep,InitServiceTest#shouldNotDispatchDuplicateBackgroundExecutionForSameTask test`
|
||||
Expected: FAIL because execution is still synchronous and always starts from step 1.
|
||||
|
||||
**Step 3: Write minimal test scaffolding**
|
||||
|
||||
Extend the in-memory executor/repository helpers in the test file so they can:
|
||||
- block on a latch or sleep
|
||||
- count executed step codes
|
||||
- fail a chosen step once and succeed on retry
|
||||
|
||||
Do not change production code in this step.
|
||||
|
||||
**Step 4: Run tests to verify RED is stable**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest#shouldAcceptTaskExecutionAsynchronously,InitServiceTest#shouldResumeFromFirstFailedStepOnRetry,InitServiceTest#shouldResumeFromResidualRunningStep,InitServiceTest#shouldNotDispatchDuplicateBackgroundExecutionForSameTask test`
|
||||
Expected: FAIL with assertion mismatches that prove the new behavior is not implemented yet.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
|
||||
### Task 2: Introduce async dispatch in `InitService`
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
|
||||
**Step 1: Implement minimal async acceptance path**
|
||||
|
||||
Refactor `InitService` so `executeTask(taskId)`:
|
||||
- loads task and steps
|
||||
- returns a snapshot response quickly
|
||||
- submits real work to a background executor
|
||||
|
||||
Use a small in-process dispatcher owned by `InitService`:
|
||||
- `ExecutorService` or equivalent
|
||||
- active-task map keyed by `taskId`
|
||||
|
||||
**Step 2: Keep the real execution logic in a dedicated method**
|
||||
|
||||
Create a private method such as `executeTaskInBackground(taskId)` that:
|
||||
- reloads task and steps from repository
|
||||
- updates statuses
|
||||
- runs steps serially
|
||||
- persists each step result immediately
|
||||
- updates final task status
|
||||
|
||||
**Step 3: Run focused tests**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest#shouldAcceptTaskExecutionAsynchronously,InitServiceTest#shouldNotDispatchDuplicateBackgroundExecutionForSameTask test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Refactor for readability**
|
||||
|
||||
Extract helpers only if needed:
|
||||
- build response snapshot
|
||||
- mark task running
|
||||
- remove active-task entry in `finally`
|
||||
|
||||
Do not add extra features.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
|
||||
### Task 3: Implement resume-from-first-non-success semantics
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
|
||||
**Step 1: Implement step selection logic**
|
||||
|
||||
Before running steps in the background:
|
||||
- convert residual `RUNNING` steps to `FAILED`
|
||||
- skip every step already marked `SUCCESS`
|
||||
- start execution at the first non-`SUCCESS` step
|
||||
|
||||
Preserve current semantics after the resume point:
|
||||
- mark current step `RUNNING`
|
||||
- on success mark `SUCCESS`
|
||||
- on failure mark `FAILED` and stop
|
||||
|
||||
**Step 2: Preserve response counters**
|
||||
|
||||
Ensure `successSteps` in `InitTaskExecuteResponse` reflects the current number of successful steps, including successes from earlier runs.
|
||||
|
||||
**Step 3: Run focused tests**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest#shouldResumeFromFirstFailedStepOnRetry,InitServiceTest#shouldResumeFromResidualRunningStep test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Run the earlier async tests again**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest#shouldAcceptTaskExecutionAsynchronously,InitServiceTest#shouldNotDispatchDuplicateBackgroundExecutionForSameTask,InitServiceTest#shouldResumeFromFirstFailedStepOnRetry,InitServiceTest#shouldResumeFromResidualRunningStep test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
|
||||
### Task 4: Align controller and API-facing semantics
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/controller/InitInternalController.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/dto/InitTaskExecuteResponse.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
|
||||
**Step 1: Make the API comments and DTO semantics match async behavior**
|
||||
|
||||
Update JavaDoc/comments so they describe:
|
||||
- execute endpoint only accepts and starts background work
|
||||
- polling should use existing detail/step/log APIs
|
||||
|
||||
If needed, add a field only if tests prove it is necessary. Prefer reusing the current DTO unchanged.
|
||||
|
||||
**Step 2: Run focused verification**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest test`
|
||||
Expected: PASS for all service tests, including the pre-existing create/query/execute cases.
|
||||
|
||||
**Step 3: Review the controller surface**
|
||||
|
||||
Check that:
|
||||
- route path stays unchanged
|
||||
- response wrapper stays unchanged
|
||||
- no new endpoint is introduced unnecessarily
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
|
||||
### Task 5: Final verification
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: Run init service tests**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run init executor regression tests touched by earlier behavior changes**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldInjectDatabaseCredentialsWhenExecutingStandardDbApplyCommand,ConfigurableInitStepExecutorTest#shouldExposeStandardInitScriptTemplates,ConfigurableInitStepExecutorTest#shouldStartStandardCmspCmtpWithConfiguredCommand test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Run compile**
|
||||
|
||||
Run: `mvn -q -DskipTests compile`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Review focused diff**
|
||||
|
||||
Run: `git diff -- src/main/java/com/cisd/tms/modules/init/service/InitService.java src/main/java/com/cisd/tms/modules/init/controller/InitInternalController.java src/main/java/com/cisd/tms/modules/init/dto/InitTaskExecuteResponse.java src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java docs/plans/2026-03-10-init-execute-async-resume-design.md docs/plans/2026-03-10-init-execute-async-resume-plan.md`
|
||||
Expected: only async-dispatch, resume logic, and related documentation changes.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Standard Init Script Templates Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add standard CISD post-init script templates to the repository and document how TMS should reference them after deployment to `/home/tms/bin`.
|
||||
|
||||
**Architecture:** Keep the runtime command model unchanged in TMS. Add script templates under the repository so deployment has a clear source of truth, then point `application.yml.example` and `README.md` to the deployed absolute paths. Tests only verify the repository artifacts and expected command content, not real database or nginx execution.
|
||||
|
||||
**Tech Stack:** Shell scripts, Spring Boot config examples, JUnit 5 filesystem assertions
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add repository script templates
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/scripts/standard-init/apply_standard_db.sh`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/scripts/standard-init/start_standard_apps.sh`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/scripts/standard-init/start_standard_nginx.sh`
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/scripts/standard-init/check_standard_runtime.sh`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add tests that assert the four script files exist in `scripts/standard-init/` and contain the expected key commands:
|
||||
- DB script references `SCHEMA-DDL.sql`, `CMEP.sql`, `UP-ORG-INFO.sql`
|
||||
- App start script references `start_cmsp.sh` and `start_cmtp.sh`
|
||||
- Nginx start script references `/usr/local/nginx/sbin/nginx`
|
||||
- Post-check script references CMSP/CMTP/nginx process checks and `organization.json`
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldExposeStandardInitScriptTemplates test`
|
||||
Expected: FAIL because the scripts do not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Create the four scripts with:
|
||||
- `#!/bin/bash`
|
||||
- `set -euo pipefail`
|
||||
- environment variable placeholders for preset credentials where needed
|
||||
- comments limited to critical operational assumptions
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldExposeStandardInitScriptTemplates test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/standard-init src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java
|
||||
git commit -m "feat: add standard init script templates"
|
||||
```
|
||||
|
||||
### Task 2: Wire deployment-facing config examples
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/config/application.yml.example`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/README.md`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add a test that asserts:
|
||||
- `application.yml.example` points the four command properties to `/home/tms/bin/*.sh`
|
||||
- `README.md` documents the repository source path `scripts/standard-init/` and the target deploy path `/home/tms/bin`
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldDocumentStandardInitScriptTemplateDeployment test`
|
||||
Expected: FAIL because the docs/config are not updated yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Update example config and README with:
|
||||
- exact deployed command values
|
||||
- copy/deploy note from `scripts/standard-init/` to `/home/tms/bin`
|
||||
- required environment variables for DB script
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldDocumentStandardInitScriptTemplateDeployment test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add config/application.yml.example README.md src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java
|
||||
git commit -m "docs: wire standard init script template deployment"
|
||||
```
|
||||
|
||||
### Task 3: Final verification
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: Run focused tests**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run init test suite**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest,ConfigurableInitStepExecutorTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Run compile**
|
||||
|
||||
Run: `mvn -q -DskipTests compile`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Review git diff**
|
||||
|
||||
Run: `git diff -- scripts/standard-init config/application.yml.example README.md src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
Expected: only script templates, config example, README, and tests changed.
|
||||
@@ -0,0 +1,249 @@
|
||||
# Standard Post Deploy Completion Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 补齐标准版初始化在部署文档后半段缺失的动作,包括真实导库、CMSP/CMTP 启动与检查、nginx 配置与启动、WEB 发布。
|
||||
|
||||
**Architecture:** 保持“预置层负责基础安装与平台凭据,初始化层负责客户化和运行配置”边界不变。TMS 继续负责生成配置和 SQL 输出物,并在标准版流程中新增后半段步骤:真实导库、应用启动、nginx 配置与启动、WEB 发布、启动后检查。vendor 原始脚本继续只读,优先通过 staging 配置和显式命令编排补齐流程,而不扩散成新的黑盒主导链路。
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot 3.5, Jackson, JUnit 5, Maven, Shell command orchestration
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 固化标准版后半段步骤编排
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/service/InitService.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/initplan/step-command-profiles.json`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/document/CIPS标准收发器安装部署手册1.0(企业信创版)-V1.3.2.doc`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(间参版)-V1.3.2/document/CIPS标准收发器安装部署手册1.0(金融机构信创版)-V1.3.2.doc`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
在 `InitServiceTest` 新增标准版步骤断言:
|
||||
- `ENTERPRISE + RABBITMQ` 包含:`DB_RENDER`、`DB_APPLY`、`DEPLOY_RECEIVER`、`NGINX_CONFIG_APPLY`、`WEB_CONFIG_APPLY`、`START_CMSP_CMTP`、`START_NGINX`、`POST_CHECK`
|
||||
- `ENTERPRISE + TLQ` 同样包含上述后半段步骤
|
||||
- `INDIRECT + RABBITMQ`、`INDIRECT + RABBITMQ_TLQ` 同样断言
|
||||
- 直参版仍不包含这些标准版步骤
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest test`
|
||||
Expected: FAIL,提示步骤未生成
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
修改 `InitService.defaultSteps()` 和 `step-command-profiles.json`:
|
||||
- 在标准版 `DEPLOY_RECEIVER` 后追加:
|
||||
- `NGINX_CONFIG_APPLY`
|
||||
- `WEB_CONFIG_APPLY`
|
||||
- `START_CMSP_CMTP`
|
||||
- `START_NGINX`
|
||||
- `POST_CHECK`
|
||||
- 命令模板统一先挂到 internal step:
|
||||
- `internal::nginx_config_apply`
|
||||
- `internal::web_config_apply`
|
||||
- `internal::start_cmsp_cmtp`
|
||||
- `internal::start_nginx`
|
||||
- `internal::post_check`
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 2: 增加配置面与失败测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/application.yml`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/config/application.yml.example`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
新增执行器测试:
|
||||
- `shouldApplyStandardNginxConfigFromTemplate`
|
||||
- `shouldPublishStandardWebAssetsAndOrganizationJson`
|
||||
- `shouldStartStandardCmspCmtpWithConfiguredCommand`
|
||||
- `shouldStartStandardNginxWithConfiguredCommand`
|
||||
- `shouldRunStandardPostCheck`
|
||||
|
||||
测试目标:
|
||||
- `NGINX_CONFIG_APPLY` 会输出替换后的 `nginx.conf`
|
||||
- `WEB_CONFIG_APPLY` 会生成 `organization.json` 并发布 `front.zip` 内容
|
||||
- `START_CMSP_CMTP` 会调用预置启动命令或脚本
|
||||
- `START_NGINX` 会调用 nginx 启动命令
|
||||
- `POST_CHECK` 会执行配置的检查命令并记录日志
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest test`
|
||||
Expected: FAIL,提示 internal step 未实现或配置缺失
|
||||
|
||||
**Step 3: Define minimal config surface**
|
||||
|
||||
为标准版后半段补齐配置项:
|
||||
- `standard-nginx-template-path`
|
||||
- `standard-nginx-target-path`
|
||||
- `standard-web-front-zip-path`
|
||||
- `standard-web-work-dir`
|
||||
- `standard-web-target-dir`
|
||||
- `standard-web-organization-json-path`
|
||||
- `standard-app-start-command`
|
||||
- `standard-nginx-start-command`
|
||||
- `standard-post-check-command`
|
||||
|
||||
说明:
|
||||
- 真实命令由环境配置注入,TMS 只编排,不硬编码具体 systemd/脚本形态
|
||||
- 默认值可沿用文档建议路径
|
||||
|
||||
**Step 4: Re-run focused test**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest test`
|
||||
Expected: 仍失败,但仅剩实现缺失
|
||||
|
||||
### Task 3: 实现标准版 Nginx 配置落地
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/software/setngx.sh`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/software/nginx.conf`
|
||||
|
||||
**Step 1: Add internal step dispatch**
|
||||
|
||||
在 `executeInternal()` 新增:
|
||||
- `case "nginx_config_apply" -> executeStandardNginxConfigApply(task, step)`
|
||||
|
||||
**Step 2: Implement minimal behavior**
|
||||
|
||||
实现逻辑:
|
||||
- 仅对标准版生效
|
||||
- 读取 `standard-nginx-template-path`
|
||||
- 复用 `buildStandardReplacements()` 替换 `sptp.01app.ip`、`sptp.02app.ip`
|
||||
- 将渲染结果先输出到 `staging/<taskId>/nginx/nginx.conf`
|
||||
- LOCAL 模式复制到 `standard-nginx-target-path`
|
||||
- SIMULATE 模式仅产出 staging 文件
|
||||
|
||||
**Step 3: Run focused test**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldApplyStandardNginxConfigFromTemplate test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 4: 实现标准版 WEB 发布
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/toolsh/setweb.sh`
|
||||
|
||||
**Step 1: Add internal step dispatch**
|
||||
|
||||
在 `executeInternal()` 新增:
|
||||
- `case "web_config_apply" -> executeStandardWebConfigApply(task, step)`
|
||||
|
||||
**Step 2: Implement minimal behavior**
|
||||
|
||||
实现逻辑:
|
||||
- 仅对标准版生效
|
||||
- 从 `standard-web-front-zip-path` 读取 `front.zip`
|
||||
- 解压到 `staging/<taskId>/web/`
|
||||
- 生成 `organization.json`,内容来自 `request.orgNameCn`
|
||||
- LOCAL 模式将解压产物发布到 `standard-web-target-dir`
|
||||
- 同步 `organization.json` 到 `standard-web-organization-json-path`
|
||||
- SIMULATE 模式仅生成 staging 产物
|
||||
|
||||
**Step 3: Run focused test**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldPublishStandardWebAssetsAndOrganizationJson test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 5: 实现标准版启动与检查
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/sh/sh/stcmep_cent.sh`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/cmsp/01/start_cmsp.sh`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/CIPS部署文档/标准收发器安装介质及部署手册(企业信创版)-V1.3.2/cpackage/cmtp/01/start_cmtp.sh`
|
||||
|
||||
**Step 1: Add internal step dispatch**
|
||||
|
||||
在 `executeInternal()` 新增:
|
||||
- `case "start_cmsp_cmtp" -> executeStandardStartApp(task, step)`
|
||||
- `case "start_nginx" -> executeStandardStartNginx(task, step)`
|
||||
- `case "post_check" -> executeStandardPostCheck(task, step)`
|
||||
|
||||
**Step 2: Implement minimal behavior**
|
||||
|
||||
实现逻辑:
|
||||
- `START_CMSP_CMTP`
|
||||
- 仅对标准版生效
|
||||
- 执行 `standard-app-start-command`
|
||||
- `START_NGINX`
|
||||
- 执行 `standard-nginx-start-command`
|
||||
- `POST_CHECK`
|
||||
- 执行 `standard-post-check-command`
|
||||
- 三者都要求:
|
||||
- SIMULATE 模式返回模拟成功
|
||||
- LOCAL 模式走统一 `runCommand` 逻辑
|
||||
- 日志记录命令、cwd、exit code
|
||||
|
||||
**Step 3: Run focused tests**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldStartStandardCmspCmtpWithConfiguredCommand,ConfigurableInitStepExecutorTest#shouldStartStandardNginxWithConfiguredCommand,ConfigurableInitStepExecutorTest#shouldRunStandardPostCheck test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 6: 将 DB_APPLY 收敛为“真实导库”入口
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/application.yml`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/config/application.yml.example`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Tighten DB_APPLY contract**
|
||||
|
||||
修改 `DB_APPLY` 行为:
|
||||
- 标准版 LOCAL 模式下,如果 `standard-db-apply-command` 为空,直接失败而不是跳过
|
||||
- 错误信息明确提示“导库命令未配置”
|
||||
|
||||
**Step 2: Add test**
|
||||
|
||||
新增测试:
|
||||
- `shouldFailDbApplyWhenStandardDbApplyCommandMissingInLocalMode`
|
||||
- `shouldExecuteStandardDbApplyCommandWhenConfigured`
|
||||
|
||||
**Step 3: Run focused test**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldFailDbApplyWhenStandardDbApplyCommandMissingInLocalMode,ConfigurableInitStepExecutorTest#shouldExecuteStandardDbApplyCommandWhenConfigured test`
|
||||
Expected: PASS
|
||||
|
||||
### Task 7: 文档同步与回归验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-09-auto-deploy-refactor.md`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/openapi/cisd-init.openapi.yaml`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Sync plan wording**
|
||||
|
||||
将标准版初始化主链路更新为:
|
||||
- `... -> DB_APPLY -> DEPLOY_RECEIVER -> NGINX_CONFIG_APPLY -> WEB_CONFIG_APPLY -> START_CMSP_CMTP -> START_NGINX -> POST_CHECK -> VERIFY`
|
||||
|
||||
**Step 2: Refresh API/Doc examples if needed**
|
||||
|
||||
如果接口示例中已有步骤说明,补充新增标准版后半段步骤说明。
|
||||
|
||||
**Step 3: Run targeted tests**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest,ConfigurableInitStepExecutorTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Run compile verification**
|
||||
|
||||
Run: `mvn -q -DskipTests compile`
|
||||
Expected: PASS
|
||||
@@ -0,0 +1,114 @@
|
||||
# TMS Deployment Document Design
|
||||
|
||||
**Date:** 2026-03-10
|
||||
|
||||
## Goal
|
||||
|
||||
维护一份面向现场部署和运维的 TMS 部署手册,覆盖 `/home/tms` 目录结构、jar 与脚本部署、配置文件、初始化依赖、文件上传目录、启动检查与常见故障定位。
|
||||
|
||||
## Audience
|
||||
|
||||
- 现场运维
|
||||
- 集成交付人员
|
||||
- 后端开发/联调人员
|
||||
|
||||
## Document Style
|
||||
|
||||
文档应以“可执行手册”为目标,而不是概念说明。要求:
|
||||
- 直接给目录和命令
|
||||
- 标明哪些内容必须预置,哪些由 TMS 初始化处理
|
||||
- 区分标准版和直参版相关依赖,但不重复解释业务背景
|
||||
- 每节都有输入、操作、结果或检查点
|
||||
|
||||
## Scope
|
||||
|
||||
纳入部署文档:
|
||||
- TMS 运行环境要求
|
||||
- `/home/tms` 目录结构
|
||||
- jar、配置、脚本、上传目录部署
|
||||
- `application.yml` 关键配置说明
|
||||
- 标准版初始化脚本 `/home/tms/bin/*.sh` 的作用和依赖
|
||||
- 文件上传与 `fileId` 模型说明
|
||||
- TMS 启动、停止、状态检查
|
||||
- 初始化联调前检查
|
||||
- 常见故障与排查
|
||||
|
||||
不纳入:
|
||||
- CIPS 标准收发器基础安装手册全文复述
|
||||
- 密码卡详细使用文档
|
||||
- OpenAPI 业务接口全集说明
|
||||
|
||||
## Proposed Document Location
|
||||
|
||||
建议新增正式手册:
|
||||
- `/Users/waner/Work/CISD/文档/tms-framework/docs/deployment/tms-deployment.md`
|
||||
|
||||
README 保留简版入口,完整运维说明进入该文档。
|
||||
|
||||
## Proposed Structure
|
||||
|
||||
1. 文档目的与适用范围
|
||||
2. 环境要求
|
||||
3. 部署目录结构
|
||||
4. 部署物清单
|
||||
5. `/home/tms/config/application.yml` 关键配置
|
||||
6. `/home/tms/bin` 初始化脚本说明
|
||||
7. 文件上传与 `fileId` 约定
|
||||
8. 启动/停止/状态命令
|
||||
9. 初始化前置检查
|
||||
10. 常见故障排查
|
||||
11. 附录:推荐命令与检查项
|
||||
|
||||
## Key Content Decisions
|
||||
|
||||
### 1. 明确边界
|
||||
|
||||
部署文档必须明确:
|
||||
- TMS 只负责自身部署和 CISD 初始化编排
|
||||
- 标准收发器、RabbitMQ、TLQ、TiDB、nginx 的基础安装属于预置层
|
||||
- 初始化阶段负责客户化配置、落位、导库、发布和启动命令调用
|
||||
|
||||
### 2. 目录结构固定化
|
||||
|
||||
文档采用 `/home/tms` 作为标准路径:
|
||||
- `/home/tms/tms-framework.jar`
|
||||
- `/home/tms/config/application.yml`
|
||||
- `/home/tms/scripts/tms.sh`
|
||||
- `/home/tms/bin/*.sh`
|
||||
- `/home/tms/uploads/`
|
||||
- `/home/tms/logs/`
|
||||
- `/home/tms/run/`
|
||||
- `/home/tms/tmp/`
|
||||
|
||||
### 3. 配置说明强调初始化相关参数
|
||||
|
||||
重点说明:
|
||||
- `tms.file.storage.upload-base-dir`
|
||||
- `tms.init.executor.*`
|
||||
- 标准版脚本命令路径
|
||||
- RabbitMQ 预置管理员账号
|
||||
- 日志目录、staging 目录
|
||||
|
||||
### 4. 文件上传模型单独成节
|
||||
|
||||
必须明确:
|
||||
- 初始化接口不接收 multipart 文件
|
||||
- 前端先上传文件,再使用 `fileId`
|
||||
- 上传目录与初始化执行器解析规则保持一致
|
||||
|
||||
### 5. 故障排查按常见现场问题组织
|
||||
|
||||
优先纳入:
|
||||
- jar 无法启动
|
||||
- `DB_APPLY` 命令为空
|
||||
- `cpconfig` 找不到
|
||||
- `fileId` 文件找不到
|
||||
- RabbitMQ 队列创建失败
|
||||
- nginx 未启动
|
||||
- WEB 未发布
|
||||
|
||||
## Output Expectation
|
||||
|
||||
产出两类文档:
|
||||
- `README.md` 中保留精简入口
|
||||
- `docs/deployment/tms-deployment.md` 作为完整部署手册
|
||||
@@ -0,0 +1,156 @@
|
||||
# TMS Deployment Document Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Create a production-oriented TMS deployment manual that documents runtime files, directories, scripts, config, upload/fileId flow, and on-host verification for `/home/tms` deployments.
|
||||
|
||||
**Architecture:** Keep `README.md` as the short entry point and move the full operational content into a dedicated deployment manual under `docs/deployment/`. Reuse existing runtime scripts and config examples instead of duplicating logic, and align every section with the current init/fileId implementation.
|
||||
|
||||
**Tech Stack:** Markdown documentation, existing shell scripts, Spring Boot config examples
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create deployment manual skeleton
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/waner/Work/CISD/文档/tms-framework/docs/deployment/tms-deployment.md`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/README.md`
|
||||
|
||||
**Step 1: Draft the document outline**
|
||||
|
||||
Add section headers for:
|
||||
- environment
|
||||
- directory layout
|
||||
- deployment artifacts
|
||||
- config
|
||||
- scripts
|
||||
- file upload/fileId flow
|
||||
- startup operations
|
||||
- precheck
|
||||
- troubleshooting
|
||||
|
||||
**Step 2: Review against current repo assets**
|
||||
|
||||
Verify each section maps to existing files:
|
||||
- `scripts/tms.sh`
|
||||
- `scripts/standard-init/*.sh`
|
||||
- `config/application.yml.example`
|
||||
- init/file upload config
|
||||
|
||||
**Step 3: Write the initial manual content**
|
||||
|
||||
Populate the outline with precise paths and commands using `/home/tms`.
|
||||
|
||||
**Step 4: Link README to the new manual**
|
||||
|
||||
Keep README concise and add a “see full deployment guide” link.
|
||||
|
||||
### Task 2: Document deployment artifacts and directories
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/deployment/tms-deployment.md`
|
||||
|
||||
**Step 1: Add directory tree**
|
||||
|
||||
Document:
|
||||
- `/home/tms/tms-framework.jar`
|
||||
- `/home/tms/config/application.yml`
|
||||
- `/home/tms/scripts/tms.sh`
|
||||
- `/home/tms/bin/*.sh`
|
||||
- `/home/tms/uploads`
|
||||
- `/home/tms/logs`
|
||||
- `/home/tms/run`
|
||||
- `/home/tms/tmp`
|
||||
|
||||
**Step 2: Add artifact source mapping**
|
||||
|
||||
State where each deployed file comes from in the repo.
|
||||
|
||||
**Step 3: Add deployment commands**
|
||||
|
||||
Include copy/chmod commands required on CentOS.
|
||||
|
||||
### Task 3: Document config and init dependencies
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/deployment/tms-deployment.md`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/tms-framework/config/application.yml.example`
|
||||
- Reference: `/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/application.yml`
|
||||
|
||||
**Step 1: Document mandatory config items**
|
||||
|
||||
Explain required runtime settings:
|
||||
- security token
|
||||
- upload directory
|
||||
- init executor mode
|
||||
- standard script commands
|
||||
- staging/log directories
|
||||
|
||||
**Step 2: Document standard-init scripts**
|
||||
|
||||
Describe purpose and prerequisites of:
|
||||
- `apply_standard_db.sh`
|
||||
- `start_standard_apps.sh`
|
||||
- `start_standard_nginx.sh`
|
||||
- `check_standard_runtime.sh`
|
||||
|
||||
**Step 3: Document credential handling**
|
||||
|
||||
Clarify which credentials stay in pre-provisioned env vars instead of `application.yml`.
|
||||
|
||||
### Task 4: Document file upload and fileId flow
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/deployment/tms-deployment.md`
|
||||
|
||||
**Step 1: Describe upload-first workflow**
|
||||
|
||||
State that init APIs do not accept multipart files and only accept `fileId`.
|
||||
|
||||
**Step 2: Map init fields to uploaded files**
|
||||
|
||||
Document:
|
||||
- `receiverLicenseFileId`
|
||||
- `tlqLicenseFileId`
|
||||
- `cfmqConfigFileId`
|
||||
- `cfgZipFileId`
|
||||
|
||||
**Step 3: Explain storage layout**
|
||||
|
||||
Describe `/home/tms/uploads/<fileId>/<originalFilename>` and how the init executor resolves files.
|
||||
|
||||
### Task 5: Add operations and troubleshooting sections
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/docs/deployment/tms-deployment.md`
|
||||
|
||||
**Step 1: Add start/stop/status commands**
|
||||
|
||||
Document `tms.sh` usage and log locations.
|
||||
|
||||
**Step 2: Add precheck guidance**
|
||||
|
||||
Reference `scripts/cisd_init_precheck.sh` and describe expected pass/fail output.
|
||||
|
||||
**Step 3: Add troubleshooting cases**
|
||||
|
||||
Document diagnosis steps for:
|
||||
- `standardDbApplyCommand is blank`
|
||||
- `cpconfig not found`
|
||||
- uploaded file not found by `fileId`
|
||||
- RabbitMQ command/path errors
|
||||
- WEB/NGINX publish/start issues
|
||||
|
||||
### Task 6: Final review
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: Read the final manual top to bottom**
|
||||
|
||||
Check for mismatches against current code/scripts/config.
|
||||
|
||||
**Step 2: Review focused diff**
|
||||
|
||||
Run: `git diff -- README.md docs/deployment/tms-deployment.md`
|
||||
Expected: only deployment documentation changes.
|
||||
@@ -0,0 +1,135 @@
|
||||
# Controller Internal Convention Removal Design
|
||||
|
||||
**Date:** 2026-03-11
|
||||
|
||||
## Goal
|
||||
|
||||
去掉“非 openapi 接口必须使用 `controller/internal` 包和 `*InternalController` 命名”的项目约定,将内部业务接口控制器统一收敛到中性 `controller` 包和 `*Controller` 类名,同时保持现有 URL、鉴权和 Swagger 暴露行为完全不变。
|
||||
|
||||
## Scope
|
||||
|
||||
本设计覆盖:
|
||||
- `controller/internal` -> `controller` 的包路径调整
|
||||
- `*InternalController` -> `*Controller` 的类名调整
|
||||
- 测试引用、README、结构说明、命名约定同步更新
|
||||
|
||||
本设计不覆盖:
|
||||
- `/api/**` 与 `/openapi/**` 路径变更
|
||||
- DTO 包结构调整
|
||||
- 鉴权规则变更
|
||||
- `openapi` 控制器命名风格调整
|
||||
- service/repository 结构重构
|
||||
|
||||
## Current State
|
||||
|
||||
当前仓库对内部接口有一套强命名约定:
|
||||
- 内部业务接口通常命名为 `*InternalController`
|
||||
- 其中部分模块控制器位于 `controller/internal`
|
||||
- README 明确规定:`Internal API controllers only in */controller/internal`
|
||||
|
||||
实际代码状态并不统一:
|
||||
- `auth`、`device`、`init`、`system` 使用 `controller` 包,但类名仍为 `*InternalController`
|
||||
- `file` 使用 `controller/internal` 包,类名为 `FileInternalController`
|
||||
- `sign` 模块同时存在 `controller/internal` 和 `controller/openapi`
|
||||
|
||||
这意味着“internal controller”已经被项目文档和部分代码固化,但在包结构上并未完全一致。
|
||||
|
||||
## Design Choice
|
||||
|
||||
采用:`统一改为 controller + *Controller`
|
||||
|
||||
不采用:`只改文档,不改代码`
|
||||
|
||||
原因:
|
||||
- 用户目标是去掉该设定本身,而不是仅仅弱化描述
|
||||
- 如果只改文档,现有类名和包名仍然持续强化旧约定
|
||||
|
||||
不采用:`只改类名,不改包路径`
|
||||
|
||||
原因:
|
||||
- `controller/internal` 目录仍会继续表达旧分层含义
|
||||
- 无法真正完成约定清理
|
||||
|
||||
不采用:`新增一套中性 Controller,保留旧类做代理`
|
||||
|
||||
原因:
|
||||
- 没有兼容性收益
|
||||
- 会引入重复 Bean 或重复请求映射风险
|
||||
|
||||
## Naming Rule After Change
|
||||
|
||||
调整后的规则:
|
||||
- `/api/**` 控制器放在 `modules/*/controller`
|
||||
- `/openapi/**` 控制器继续放在 `modules/*/controller/openapi`
|
||||
- 内部控制器类名使用中性 `*Controller`
|
||||
- 对外接口控制器命名不强制带 `Open` 前缀,但当前已有 `OpenSignController` 可暂时保留
|
||||
|
||||
## Class Rename Plan
|
||||
|
||||
建议重命名如下:
|
||||
- `AuthInternalController` -> `AuthController`
|
||||
- `DeviceInternalController` -> `DeviceController`
|
||||
- `CryptoCardInternalController` -> `CryptoCardController`
|
||||
- `InitInternalController` -> `InitController`
|
||||
- `HealthInternalController` -> `HealthController`
|
||||
- `InternalSignController` -> `SignController`
|
||||
- `FileInternalController` -> `FileController`
|
||||
|
||||
对应包路径调整:
|
||||
- `modules/file/controller/internal` -> `modules/file/controller`
|
||||
- `modules/sign/controller/internal` -> `modules/sign/controller`
|
||||
|
||||
其余当前已在 `controller` 包下的类,仅改类名即可。
|
||||
|
||||
## Compatibility Boundary
|
||||
|
||||
以下内容必须保持不变:
|
||||
- `@RequestMapping` 路径
|
||||
- 所有 `/api/v1/**` 路径
|
||||
- 所有 `/openapi/**` 路径
|
||||
- `WebMvcConfig` 的拦截规则
|
||||
- Swagger/OpenAPI 文档访问路径
|
||||
- 前端、脚本、curl 示例调用路径
|
||||
|
||||
也就是说,这次是纯 Java 命名与项目约定调整,不是 API 行为调整。
|
||||
|
||||
## Security Impact
|
||||
|
||||
鉴权策略不变:
|
||||
- `/api/**` 继续走内部 token 拦截器
|
||||
- `/openapi/**` 继续走 openapi 签名拦截器
|
||||
|
||||
因为拦截器是基于 URL 前缀,而不是控制器类名或包名,所以控制器重命名不会影响运行时鉴权行为。
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
需要验证三类内容:
|
||||
|
||||
1. 编译与导入
|
||||
- 测试类中的 import、`standaloneSetup(...)`、构造器引用都更新完成
|
||||
|
||||
2. 运行时边界不变
|
||||
- 控制器路径不变
|
||||
- 鉴权拦截配置不变
|
||||
|
||||
3. 文档约定更新
|
||||
- README 中模块结构和 API Boundary Rules 不再要求 `controller/internal`
|
||||
- 示例结构改为 `controller + controller/openapi`
|
||||
|
||||
## Risks
|
||||
|
||||
1. 类名和文件路径同时重命名,容易遗漏测试 import 或文档引用。
|
||||
2. 如果存在基于类名扫描或人工 grep 的脚本,需要同步更新引用。
|
||||
3. `sign` 与 `file` 模块同时涉及包移动,若未同步修改 package 声明会导致编译失败。
|
||||
|
||||
## Recommendation
|
||||
|
||||
先做最小一致性改造:
|
||||
- 统一内部控制器类名为 `*Controller`
|
||||
- 去掉 `controller/internal` 包
|
||||
- 更新 README、测试和约定文档
|
||||
|
||||
不在这一轮处理:
|
||||
- `openapi` 控制器风格统一
|
||||
- DTO 包命名统一
|
||||
- 额外的模块结构重构
|
||||
@@ -0,0 +1,193 @@
|
||||
# Controller Internal Convention Removal Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Remove the project-wide `controller/internal` and `*InternalController` convention for non-openapi APIs while keeping all request paths, authentication behavior, and Swagger exposure unchanged.
|
||||
|
||||
**Architecture:** Rename internal API controllers to neutral `*Controller` names and place them under `modules/*/controller`, but leave all `@RequestMapping` values and `WebMvcConfig` interceptor rules untouched. Update tests and README so the repository no longer encodes the old naming convention as a rule.
|
||||
|
||||
**Tech Stack:** Spring Boot MVC, springdoc-openapi, JUnit 5, MockMvc
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock down URL and documentation invariants with tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java`
|
||||
- Test: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Adjust or add tests that assert:
|
||||
- file upload/query endpoints remain `/api/v1/files/**`
|
||||
- Swagger/OpenAPI docs still mention the same URLs
|
||||
- README no longer contains the hard rule `Internal API controllers only in */controller/internal`
|
||||
|
||||
Do not rename production classes yet.
|
||||
|
||||
**Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `mvn -q -Dtest=FileInternalControllerTest,ConfigurableInitStepExecutorTest#shouldDocumentStandardInitScriptTemplateDeployment test`
|
||||
Expected: FAIL because tests will still reference old controller names and README still encodes the old convention.
|
||||
|
||||
**Step 3: Keep the tests minimal**
|
||||
|
||||
Limit assertions to:
|
||||
- unchanged routes
|
||||
- removed convention wording
|
||||
- continued `/home/tms/bin` deployment doc assertions already covered in existing tests
|
||||
|
||||
**Step 4: Run tests again to confirm stable RED**
|
||||
|
||||
Run: `mvn -q -Dtest=FileInternalControllerTest,ConfigurableInitStepExecutorTest#shouldDocumentStandardInitScriptTemplateDeployment test`
|
||||
Expected: FAIL for the intended naming/documentation reasons only.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
|
||||
### Task 2: Rename file and sign internal controllers first
|
||||
|
||||
**Files:**
|
||||
- Move/Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/file/controller/internal/FileInternalController.java`
|
||||
- Move/Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/sign/controller/internal/InternalSignController.java`
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java`
|
||||
|
||||
**Step 1: Write the minimal rename**
|
||||
|
||||
Rename and relocate:
|
||||
- `FileInternalController` -> `FileController`
|
||||
- `InternalSignController` -> `SignController`
|
||||
|
||||
Move them into `modules/*/controller`.
|
||||
|
||||
Keep:
|
||||
- package-level Spring scanning working
|
||||
- all request mappings unchanged
|
||||
|
||||
**Step 2: Update tests/imports**
|
||||
|
||||
Update MockMvc standalone setup and imports to use new class names.
|
||||
|
||||
**Step 3: Run focused tests**
|
||||
|
||||
Run: `mvn -q -Dtest=FileInternalControllerTest test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Review for accidental URL changes**
|
||||
|
||||
Check that no `@RequestMapping` strings changed.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
|
||||
### Task 3: Rename the remaining internal controllers
|
||||
|
||||
**Files:**
|
||||
- Move/Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/auth/controller/AuthInternalController.java`
|
||||
- Move/Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/device/controller/DeviceInternalController.java`
|
||||
- Move/Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardInternalController.java`
|
||||
- Move/Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/init/controller/InitInternalController.java`
|
||||
- Move/Modify: `/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/system/controller/HealthInternalController.java`
|
||||
- Modify: imports/usages that reference those class names
|
||||
|
||||
**Step 1: Apply the rename set**
|
||||
|
||||
Rename:
|
||||
- `AuthInternalController` -> `AuthController`
|
||||
- `DeviceInternalController` -> `DeviceController`
|
||||
- `CryptoCardInternalController` -> `CryptoCardController`
|
||||
- `InitInternalController` -> `InitController`
|
||||
- `HealthInternalController` -> `HealthController`
|
||||
|
||||
Keep each class in `modules/*/controller`.
|
||||
|
||||
**Step 2: Update all direct references**
|
||||
|
||||
Use `rg` to find and fix:
|
||||
- imports
|
||||
- JavaDoc mentions
|
||||
- comments that name concrete classes
|
||||
|
||||
**Step 3: Run focused compile/test checks**
|
||||
|
||||
Run: `mvn -q -Dtest=InitServiceTest,TmsApplicationTests test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Run compile if needed**
|
||||
|
||||
Run: `mvn -q -DskipTests compile`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
|
||||
### Task 4: Remove the repository-wide convention from README and docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/waner/Work/CISD/文档/tms-framework/README.md`
|
||||
- Modify: any design/plan/doc files that are actively used and explicitly enforce `controller/internal`
|
||||
|
||||
**Step 1: Rewrite the structure guidance**
|
||||
|
||||
Update README so it says:
|
||||
- default module layout is `controller + service + repository + entity + dto`
|
||||
- external APIs may additionally use `controller/openapi`
|
||||
|
||||
Remove the hard rule:
|
||||
- `Internal API controllers only in */controller/internal`
|
||||
|
||||
Replace it with a neutral rule:
|
||||
- `/api/**` controllers live under `controller`
|
||||
- `/openapi/**` controllers live under `controller/openapi`
|
||||
|
||||
**Step 2: Update structure tree examples**
|
||||
|
||||
Replace `controller/internal` entries in the README tree with `controller`.
|
||||
|
||||
**Step 3: Run focused documentation regression**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldDocumentStandardInitScriptTemplateDeployment test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Manual diff review**
|
||||
|
||||
Run: `git diff -- README.md`
|
||||
Expected: naming and structure guidance changes only, no API path changes.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
|
||||
### Task 5: Final verification
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: Run controller-related tests**
|
||||
|
||||
Run: `mvn -q -Dtest=FileInternalControllerTest,InitServiceTest,TmsApplicationTests test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run documentation regression tests**
|
||||
|
||||
Run: `mvn -q -Dtest=ConfigurableInitStepExecutorTest#shouldDocumentStandardInitScriptTemplateDeployment test`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Run compile**
|
||||
|
||||
Run: `mvn -q -DskipTests compile`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Review focused diff**
|
||||
|
||||
Run: `git diff -- src/main/java/com/cisd/tms/modules README.md src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java docs/plans/2026-03-11-controller-internal-convention-removal-design.md docs/plans/2026-03-11-controller-internal-convention-removal-plan.md`
|
||||
Expected: only controller renames, import updates, and documentation rule changes.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
Skip commit in the current dirty workspace per user instruction.
|
||||
+2427
File diff suppressed because it is too large
Load Diff
@@ -82,6 +82,7 @@
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>tms-framework</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
DB_HOST="${DB_HOST:-127.0.0.1}"
|
||||
DB_PORT="${DB_PORT:-4000}"
|
||||
DB_NAME="${DB_NAME:-CMEP}"
|
||||
DB_USER="${DB_USER:?DB_USER is required}"
|
||||
DB_PASSWORD="${DB_PASSWORD:?DB_PASSWORD is required}"
|
||||
LOAD_DIR="${LOAD_DIR:?LOAD_DIR is required}"
|
||||
|
||||
mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" \
|
||||
-e "CREATE DATABASE IF NOT EXISTS ${DB_NAME} DEFAULT CHARACTER SET utf8mb4;"
|
||||
|
||||
mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
|
||||
< "${LOAD_DIR}/SCHEMA-DDL.sql"
|
||||
|
||||
mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
|
||||
< "${LOAD_DIR}/CMEP.sql"
|
||||
|
||||
mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
|
||||
< "${LOAD_DIR}/UP-ORG-INFO.sql"
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
pgrep -f "CMEP-CMSP" >/dev/null
|
||||
pgrep -f "CMEP-CMTP" >/dev/null
|
||||
pgrep -f "/usr/local/nginx/sbin/nginx" >/dev/null
|
||||
test -f /usr/local/nginx/html/organization.json
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
STANDARD_HOME_DIR="${STANDARD_HOME_DIR:?STANDARD_HOME_DIR is required}"
|
||||
STANDARD_RUN_USER="${STANDARD_RUN_USER:?STANDARD_RUN_USER is required}"
|
||||
|
||||
su - "${STANDARD_RUN_USER}" -s /bin/bash "${STANDARD_HOME_DIR}/cmsp/start_cmsp.sh"
|
||||
su - "${STANDARD_RUN_USER}" -s /bin/bash "${STANDARD_HOME_DIR}/cmtp/start_cmtp.sh"
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if pgrep -f "/usr/local/nginx/sbin/nginx" >/dev/null; then
|
||||
/usr/local/nginx/sbin/nginx -s reload
|
||||
else
|
||||
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
|
||||
fi
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.cisd.tms.common.config.properties;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "tms.file.storage")
|
||||
public class FileStorageProperties {
|
||||
|
||||
private String uploadBaseDir = "/home/tms/uploads";
|
||||
private long maxFileSizeBytes = 104857600L;
|
||||
|
||||
public String getUploadBaseDir() {
|
||||
return uploadBaseDir;
|
||||
}
|
||||
|
||||
public void setUploadBaseDir(String uploadBaseDir) {
|
||||
this.uploadBaseDir = uploadBaseDir;
|
||||
}
|
||||
|
||||
public long getMaxFileSizeBytes() {
|
||||
return maxFileSizeBytes;
|
||||
}
|
||||
|
||||
public void setMaxFileSizeBytes(long maxFileSizeBytes) {
|
||||
this.maxFileSizeBytes = maxFileSizeBytes;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public class PcieErrorMapper {
|
||||
lowByteErrorMap.put(0x25, "init status");
|
||||
lowByteErrorMap.put(0x26, "already logined");
|
||||
lowByteErrorMap.put(0x27, "timeout");
|
||||
lowByteErrorMap.put(0x1D, "invalid argument");
|
||||
}
|
||||
|
||||
public String toMessage(int retCode) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cisd.tms.integration.crypto.pcie;
|
||||
import com.cisd.tms.integration.crypto.config.CryptoCardProperties;
|
||||
import com.cisd.tms.integration.crypto.pcie.jna.PcieNativeLibrary;
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.ptr.PointerByReference;
|
||||
@@ -61,6 +62,8 @@ public class PcieSessionTemplate {
|
||||
public <T> T withSession(String apiName, SessionCallback<T> callback) {
|
||||
return withDevice(apiName, (lib, deviceHandle) -> {
|
||||
PointerByReference sessionRef = new PointerByReference();
|
||||
Pointer ph = new Memory(8L);
|
||||
sessionRef.setValue(ph);
|
||||
int openSessionRc = lib.SDF_OpenSession(deviceHandle, sessionRef);
|
||||
ensureSuccess(apiName + ":SDF_OpenSession", openSessionRc);
|
||||
|
||||
|
||||
@@ -90,6 +90,10 @@ public class JnaPcieCryptoService implements PcieCryptoService {
|
||||
private static final int ECC_KEY_BITS = 256;
|
||||
private static final int LMK_SEED_MAC_LENGTH = 8;
|
||||
private static final int SDFE_INIT_STATUS_LOW_BYTE = 0x25;
|
||||
private static final int IK_KEY_TYPE_DEVICE = 1;
|
||||
private static final int IK_KEY_TYPE_AUTH = 2;
|
||||
private static final int ADMIN_INDEX_MIN = 1;
|
||||
private static final int ADMIN_INDEX_MAX = 3;
|
||||
private static final int HMAC_MAX_LENGTH = 64;
|
||||
private static final int ECC_CIPHER_PREFIX_LENGTH = (EccRefPublicKey.MAX_LEN * 2) + 32 + Integer.BYTES;
|
||||
|
||||
|
||||
+2
-2
@@ -15,11 +15,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
public class AuthInternalController {
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
public AuthInternalController(AuthService authService) {
|
||||
public AuthController(AuthService authService) {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
+2
-2
@@ -42,11 +42,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/device/crypto")
|
||||
public class CryptoCardInternalController {
|
||||
public class CryptoCardController {
|
||||
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
|
||||
public CryptoCardInternalController(PcieCryptoService pcieCryptoService) {
|
||||
public CryptoCardController(PcieCryptoService pcieCryptoService) {
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,11 +12,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/device")
|
||||
public class DeviceInternalController {
|
||||
public class DeviceController {
|
||||
|
||||
private final DeviceService deviceService;
|
||||
|
||||
public DeviceInternalController(DeviceService deviceService) {
|
||||
public DeviceController(DeviceService deviceService) {
|
||||
this.deviceService = deviceService;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.cisd.tms.modules.file.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.file.dto.FileDetailResponse;
|
||||
import com.cisd.tms.modules.file.dto.FileUploadResponse;
|
||||
import com.cisd.tms.modules.file.service.FileService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/files")
|
||||
public class FileController {
|
||||
|
||||
private final FileService fileService;
|
||||
|
||||
public FileController(FileService fileService) {
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
// 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。
|
||||
@PostMapping("/upload")
|
||||
public ApiResponse<FileUploadResponse> upload(@RequestParam("file") MultipartFile file) {
|
||||
return ApiResponse.success(fileService.upload(file));
|
||||
}
|
||||
|
||||
// 返回已存储的文件元数据,便于调用方确认 fileId 到实际文件的映射关系。
|
||||
@GetMapping("/{fileId}")
|
||||
public ApiResponse<FileDetailResponse> detail(@PathVariable String fileId) {
|
||||
return ApiResponse.success(fileService.getDetail(fileId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.cisd.tms.modules.file.dto;
|
||||
|
||||
public class FileDetailResponse {
|
||||
|
||||
private String fileId;
|
||||
private String originalFilename;
|
||||
private String storagePath;
|
||||
private String status;
|
||||
private long size;
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
public String getOriginalFilename() {
|
||||
return originalFilename;
|
||||
}
|
||||
|
||||
public void setOriginalFilename(String originalFilename) {
|
||||
this.originalFilename = originalFilename;
|
||||
}
|
||||
|
||||
public String getStoragePath() {
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
public void setStoragePath(String storagePath) {
|
||||
this.storagePath = storagePath;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(long size) {
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.cisd.tms.modules.file.dto;
|
||||
|
||||
public class FileUploadResponse {
|
||||
|
||||
private String fileId;
|
||||
private long size;
|
||||
private String storagePath;
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getStoragePath() {
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
public void setStoragePath(String storagePath) {
|
||||
this.storagePath = storagePath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.cisd.tms.modules.file.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
|
||||
|
||||
@TableName("tms_file_record")
|
||||
public class FileRecordEntity extends BaseEntity {
|
||||
|
||||
private String fileId;
|
||||
private String originalFilename;
|
||||
private String storagePath;
|
||||
private String status;
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
public String getOriginalFilename() {
|
||||
return originalFilename;
|
||||
}
|
||||
|
||||
public void setOriginalFilename(String originalFilename) {
|
||||
this.originalFilename = originalFilename;
|
||||
}
|
||||
|
||||
public String getStoragePath() {
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
public void setStoragePath(String storagePath) {
|
||||
this.storagePath = storagePath;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cisd.tms.modules.file.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface FileRecordMapper extends BaseMapperX<FileRecordEntity> {
|
||||
|
||||
FileRecordEntity selectByFileId(@Param("fileId") String fileId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cisd.tms.modules.file.repository;
|
||||
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FileRecordRepository {
|
||||
|
||||
Optional<FileRecordEntity> findByFileId(String fileId);
|
||||
|
||||
void save(FileRecordEntity entity);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.cisd.tms.modules.file.repository.impl;
|
||||
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.mapper.FileRecordMapper;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class FileRecordRepositoryImpl implements FileRecordRepository {
|
||||
|
||||
private final FileRecordMapper fileRecordMapper;
|
||||
|
||||
public FileRecordRepositoryImpl(FileRecordMapper fileRecordMapper) {
|
||||
this.fileRecordMapper = fileRecordMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<FileRecordEntity> findByFileId(String fileId) {
|
||||
return Optional.ofNullable(fileRecordMapper.selectByFileId(fileId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(FileRecordEntity entity) {
|
||||
fileRecordMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.cisd.tms.modules.file.service;
|
||||
|
||||
import com.cisd.tms.common.config.properties.FileStorageProperties;
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.file.dto.FileDetailResponse;
|
||||
import com.cisd.tms.modules.file.dto.FileUploadResponse;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Service
|
||||
public class FileService {
|
||||
|
||||
private static final DateTimeFormatter FILE_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
private final FileRecordRepository fileRecordRepository;
|
||||
private final FileStorageProperties fileStorageProperties;
|
||||
|
||||
public FileService(FileRecordRepository fileRecordRepository, FileStorageProperties fileStorageProperties) {
|
||||
this.fileRecordRepository = fileRecordRepository;
|
||||
this.fileStorageProperties = fileStorageProperties;
|
||||
}
|
||||
|
||||
public FileUploadResponse upload(MultipartFile file) {
|
||||
if (file == null || file.isEmpty() || file.getSize() <= 0) {
|
||||
throw new IllegalArgumentException("upload file is empty");
|
||||
}
|
||||
if (file.getSize() > fileStorageProperties.getMaxFileSizeBytes()) {
|
||||
throw new IllegalArgumentException("upload file size exceeds limit");
|
||||
}
|
||||
|
||||
String originalFilename = sanitizeFilename(file.getOriginalFilename());
|
||||
String fileId = generateFileId();
|
||||
Path uploadBaseDir = Paths.get(normalize(fileStorageProperties.getUploadBaseDir())).normalize().toAbsolutePath();
|
||||
// 固定为 <upload-base-dir>/<fileId>/<originalFilename>,这样初始化流程只靠 fileId 就能稳定反查文件。
|
||||
Path target = uploadBaseDir.resolve(fileId).resolve(originalFilename).normalize().toAbsolutePath();
|
||||
|
||||
try {
|
||||
Files.createDirectories(target.getParent());
|
||||
file.transferTo(target);
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to save upload file");
|
||||
}
|
||||
|
||||
// 直接落库存储绝对路径,避免后续再依赖可能变化的配置重新推导文件位置。
|
||||
FileRecordEntity entity = new FileRecordEntity();
|
||||
entity.setFileId(fileId);
|
||||
entity.setOriginalFilename(originalFilename);
|
||||
entity.setStoragePath(target.toString());
|
||||
entity.setStatus("ACTIVE");
|
||||
fileRecordRepository.save(entity);
|
||||
|
||||
FileUploadResponse response = new FileUploadResponse();
|
||||
response.setFileId(fileId);
|
||||
response.setSize(file.getSize());
|
||||
response.setStoragePath(target.toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
public FileDetailResponse getDetail(String fileId) {
|
||||
FileRecordEntity entity = fileRecordRepository.findByFileId(normalize(fileId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "file not found: " + normalize(fileId)));
|
||||
Path path = Paths.get(entity.getStoragePath()).normalize().toAbsolutePath();
|
||||
|
||||
FileDetailResponse response = new FileDetailResponse();
|
||||
response.setFileId(entity.getFileId());
|
||||
response.setOriginalFilename(entity.getOriginalFilename());
|
||||
response.setStoragePath(entity.getStoragePath());
|
||||
response.setStatus(entity.getStatus());
|
||||
try {
|
||||
// 文件大小以磁盘上的真实文件为准,保证查询结果反映当前实际落盘状态。
|
||||
response.setSize(Files.exists(path) ? Files.size(path) : 0L);
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read file detail");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private String generateFileId() {
|
||||
String ts = LocalDateTime.now().format(FILE_ID_TS);
|
||||
String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT);
|
||||
return "FILE-" + ts + "-" + suffix;
|
||||
}
|
||||
|
||||
private String sanitizeFilename(String originalFilename) {
|
||||
String normalized = normalize(originalFilename);
|
||||
if (normalized.isEmpty()) {
|
||||
throw new IllegalArgumentException("original filename is blank");
|
||||
}
|
||||
try {
|
||||
// 先剥离客户端传入文件名里的路径段,避免把目录信息带入服务端存储路径。
|
||||
String fileName = Paths.get(normalized).getFileName().toString();
|
||||
String sanitized = fileName.replaceAll("[^A-Za-z0-9._-]", "_");
|
||||
if (sanitized.isBlank()) {
|
||||
throw new IllegalArgumentException("original filename is invalid");
|
||||
}
|
||||
return sanitized;
|
||||
} catch (InvalidPathException ex) {
|
||||
throw new IllegalArgumentException("original filename is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -65,9 +65,6 @@ public class InitCommandProfileService {
|
||||
|
||||
private String interpolate(String rawCommand) {
|
||||
String standardHomeDir = normalize(initExecutorProperties.getStandardHomeDir());
|
||||
if (standardHomeDir.isEmpty()) {
|
||||
standardHomeDir = "/home/cemp4i";
|
||||
}
|
||||
return rawCommand.replace(STANDARD_HOME_DIR_PLACEHOLDER, standardHomeDir);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,49 @@ public class InitExecutorProperties {
|
||||
LOCAL
|
||||
}
|
||||
|
||||
private static final String DEFAULT_STANDARD_HOME_DIR = "/home/cemp4i";
|
||||
private static final String DEFAULT_STANDARD_RUN_USER = "cmep4i";
|
||||
|
||||
private Mode mode = Mode.SIMULATE;
|
||||
private int timeoutSec = 600;
|
||||
// 所有步骤日志会写入该目录,读取日志接口会做同目录约束校验
|
||||
private String logDir = "/tmp/tms-init-logs";
|
||||
// 标准版(企业/间参)安装根目录(统一入口)
|
||||
private String standardHomeDir = "/home/cemp4i";
|
||||
private String standardHomeDir = DEFAULT_STANDARD_HOME_DIR;
|
||||
// 标准版(企业/间参)默认 cpconfig.cfg 路径
|
||||
private String standardCpconfigPath = "/home/cemp4i/cpconfig.cfg";
|
||||
private String standardCpconfigPath = "";
|
||||
// 标准版数据库脚本基目录(应包含 cpackage/sql_script)
|
||||
private String standardDbScriptBaseDir = "";
|
||||
// 标准版数据库加载目录(渲染后的 SQL 会复制到此目录)
|
||||
private String standardDbLoadDir = "";
|
||||
// 标准版数据库导库用户名;执行 DB_APPLY 时会注入 DB_USER
|
||||
private String standardDbUsername = "";
|
||||
// 标准版数据库导库密码;执行 DB_APPLY 时会注入 DB_PASSWORD
|
||||
private String standardDbPassword = "";
|
||||
// 标准版数据库导入命令;LOCAL 模式下不能为空,否则 DB_APPLY 直接失败
|
||||
private String standardDbApplyCommand = "";
|
||||
// 标准版数据库入口 SQL 文件名
|
||||
private String standardDbEntrySqlFileName = "CMEP.sql";
|
||||
// 标准版 nginx 模板路径
|
||||
private String standardNginxTemplatePath = "";
|
||||
// 标准版 nginx 目标配置路径
|
||||
private String standardNginxTargetPath = "/usr/local/nginx/conf/nginx.conf";
|
||||
// 标准版 WEB 前端压缩包路径
|
||||
private String standardWebFrontZipPath = "";
|
||||
// 标准版 WEB 工作目录
|
||||
private String standardWebWorkDir = "";
|
||||
// 标准版 WEB 发布目录
|
||||
private String standardWebTargetDir = "/usr/local/nginx/html";
|
||||
// 标准版 organization.json 输出路径
|
||||
private String standardWebOrganizationJsonPath = "/usr/local/nginx/html/organization.json";
|
||||
// 标准版 CMSP/CMTP 启动用户
|
||||
private String standardRunUser = DEFAULT_STANDARD_RUN_USER;
|
||||
// 标准版 CMSP/CMTP 启动命令
|
||||
private String standardAppStartCommand = "";
|
||||
// 标准版 nginx 启动命令
|
||||
private String standardNginxStartCommand = "";
|
||||
// 标准版启动后检查命令
|
||||
private String standardPostCheckCommand = "";
|
||||
// 直参版 cfgZipFileId 对应目录下的 cpconfig 文件名
|
||||
private String directCpconfigFileName = "cpconfig.cfg";
|
||||
// 上传文件基础目录(fileId->文件路径解析时使用)
|
||||
@@ -26,7 +61,7 @@ public class InitExecutorProperties {
|
||||
// 初始化阶段文件暂存目录(除固定落位文件外,均先暂存再使用)
|
||||
private String stagingRootDir = "/tmp/tms-init-staging";
|
||||
// 标准版收发器 license 固定落位
|
||||
private String standardReceiverLicenseTargetPath = "/home/cemp4i/cmep/license/cmep.license";
|
||||
private String standardReceiverLicenseTargetPath = "";
|
||||
// 直参版收发器 license 固定落位
|
||||
private String directReceiverLicenseTargetPath = "/home/cmep4d/cmep/license/cmep.license";
|
||||
// 标准版 TLQ license(tlqconf.zip)固定落位
|
||||
@@ -48,25 +83,21 @@ public class InitExecutorProperties {
|
||||
private String rabbitmqCtlCommand = "rabbitmqctl";
|
||||
private String rabbitmqAdminCommand = "/usr/sbin/rabbitmqadmin";
|
||||
// 标准版 RabbitMQ 初始化脚本(创建admin/通道账号、替换队列脚本机构号)
|
||||
private String standardRabbitSetupScriptPath = "/home/cemp4i/cpackage/rabq/setuprabq.sh";
|
||||
private String standardRabbitSetupScriptPath = "";
|
||||
// 标准版 RabbitMQ 队列初始化脚本(01节点)
|
||||
private String standardRabbitQueueScriptPath = "/home/cemp4i/cpackage/rabq/addrabq_R_01.sh";
|
||||
private String standardRabbitQueueScriptPath = "";
|
||||
// 标准版 RabbitMQ 队列初始化脚本(02节点)
|
||||
private String standardRabbitQueueScriptPath02 = "/home/cemp4i/cpackage/rabq/addrabq_R_02.sh";
|
||||
private String standardRabbitQueueScriptPath02 = "";
|
||||
// 队列和通道账号默认配置 vhost
|
||||
private String rabbitVhost = "/RQ";
|
||||
// 标准版应用 RabbitMQ 连接配置文件(支持文件名通配符)
|
||||
private List<String> standardRabbitAppConfigPatterns = List.of(
|
||||
"/home/cemp4i/cmsp/application-prd*.properties",
|
||||
"/home/cemp4i/cmtp/application-prd*.properties"
|
||||
);
|
||||
private List<String> standardRabbitAppConfigPatterns = List.of();
|
||||
// 标准版应用 RabbitMQ 连接账号/密码写回时是否加密
|
||||
private boolean standardRabbitCredentialEncryptEnabled = true;
|
||||
// 加密密钥,默认与官方 encrypt-decrypt.jar 保持一致
|
||||
private String standardRabbitCredentialSecretKey = "ABCDEFGHIJKLMN1234567890";
|
||||
// LOCAL 模式命令白名单前缀;仅命中前缀的命令允许执行
|
||||
private List<String> whitelistPrefixes = List.of(
|
||||
"bash /home/cemp4i/cpackage/",
|
||||
"bash /opt/CAE_Install_",
|
||||
"systemctl ",
|
||||
"echo "
|
||||
@@ -97,7 +128,7 @@ public class InitExecutorProperties {
|
||||
}
|
||||
|
||||
public String getStandardHomeDir() {
|
||||
return standardHomeDir;
|
||||
return isBlank(standardHomeDir) ? DEFAULT_STANDARD_HOME_DIR : standardHomeDir.trim();
|
||||
}
|
||||
|
||||
public void setStandardHomeDir(String standardHomeDir) {
|
||||
@@ -105,7 +136,24 @@ public class InitExecutorProperties {
|
||||
}
|
||||
|
||||
public List<String> getWhitelistPrefixes() {
|
||||
return whitelistPrefixes;
|
||||
if (whitelistPrefixes == null || whitelistPrefixes.isEmpty()) {
|
||||
return List.of(
|
||||
"bash " + getStandardHomeDir() + "/cpackage/",
|
||||
"bash /opt/CAE_Install_",
|
||||
"systemctl ",
|
||||
"echo "
|
||||
);
|
||||
}
|
||||
boolean hasStandardPrefix = whitelistPrefixes.stream()
|
||||
.filter(item -> item != null && !item.isBlank())
|
||||
.anyMatch(item -> item.startsWith("bash ") && item.contains("/cpackage/"));
|
||||
if (hasStandardPrefix) {
|
||||
return whitelistPrefixes;
|
||||
}
|
||||
java.util.ArrayList<String> merged = new java.util.ArrayList<>();
|
||||
merged.add("bash " + getStandardHomeDir() + "/cpackage/");
|
||||
merged.addAll(whitelistPrefixes);
|
||||
return merged;
|
||||
}
|
||||
|
||||
public void setWhitelistPrefixes(List<String> whitelistPrefixes) {
|
||||
@@ -113,7 +161,7 @@ public class InitExecutorProperties {
|
||||
}
|
||||
|
||||
public String getStandardCpconfigPath() {
|
||||
return standardCpconfigPath;
|
||||
return firstNonBlank(standardCpconfigPath, buildStandardHomePath("/cpconfig.cfg"));
|
||||
}
|
||||
|
||||
public void setStandardCpconfigPath(String standardCpconfigPath) {
|
||||
@@ -128,6 +176,134 @@ public class InitExecutorProperties {
|
||||
this.directCpconfigFileName = directCpconfigFileName;
|
||||
}
|
||||
|
||||
public String getStandardDbScriptBaseDir() {
|
||||
return firstNonBlank(standardDbScriptBaseDir, buildStandardHomePath("/cpackage"));
|
||||
}
|
||||
|
||||
public void setStandardDbScriptBaseDir(String standardDbScriptBaseDir) {
|
||||
this.standardDbScriptBaseDir = standardDbScriptBaseDir;
|
||||
}
|
||||
|
||||
public String getStandardDbLoadDir() {
|
||||
return firstNonBlank(standardDbLoadDir, buildStandardHomePath("/mysql/loadfilepath"));
|
||||
}
|
||||
|
||||
public void setStandardDbLoadDir(String standardDbLoadDir) {
|
||||
this.standardDbLoadDir = standardDbLoadDir;
|
||||
}
|
||||
|
||||
public String getStandardDbUsername() {
|
||||
return standardDbUsername;
|
||||
}
|
||||
|
||||
public void setStandardDbUsername(String standardDbUsername) {
|
||||
this.standardDbUsername = standardDbUsername;
|
||||
}
|
||||
|
||||
public String getStandardDbPassword() {
|
||||
return standardDbPassword;
|
||||
}
|
||||
|
||||
public void setStandardDbPassword(String standardDbPassword) {
|
||||
this.standardDbPassword = standardDbPassword;
|
||||
}
|
||||
|
||||
public String getStandardDbApplyCommand() {
|
||||
return standardDbApplyCommand;
|
||||
}
|
||||
|
||||
public void setStandardDbApplyCommand(String standardDbApplyCommand) {
|
||||
this.standardDbApplyCommand = standardDbApplyCommand;
|
||||
}
|
||||
|
||||
public String getStandardDbEntrySqlFileName() {
|
||||
return standardDbEntrySqlFileName;
|
||||
}
|
||||
|
||||
public void setStandardDbEntrySqlFileName(String standardDbEntrySqlFileName) {
|
||||
this.standardDbEntrySqlFileName = standardDbEntrySqlFileName;
|
||||
}
|
||||
|
||||
public String getStandardNginxTemplatePath() {
|
||||
return firstNonBlank(standardNginxTemplatePath, buildStandardHomePath("/cpackage/software/nginx.conf"));
|
||||
}
|
||||
|
||||
public void setStandardNginxTemplatePath(String standardNginxTemplatePath) {
|
||||
this.standardNginxTemplatePath = standardNginxTemplatePath;
|
||||
}
|
||||
|
||||
public String getStandardNginxTargetPath() {
|
||||
return standardNginxTargetPath;
|
||||
}
|
||||
|
||||
public void setStandardNginxTargetPath(String standardNginxTargetPath) {
|
||||
this.standardNginxTargetPath = standardNginxTargetPath;
|
||||
}
|
||||
|
||||
public String getStandardWebFrontZipPath() {
|
||||
return firstNonBlank(standardWebFrontZipPath, buildStandardHomePath("/cpackage/front/front.zip"));
|
||||
}
|
||||
|
||||
public void setStandardWebFrontZipPath(String standardWebFrontZipPath) {
|
||||
this.standardWebFrontZipPath = standardWebFrontZipPath;
|
||||
}
|
||||
|
||||
public String getStandardWebWorkDir() {
|
||||
return firstNonBlank(standardWebWorkDir, buildStandardHomePath("/web"));
|
||||
}
|
||||
|
||||
public void setStandardWebWorkDir(String standardWebWorkDir) {
|
||||
this.standardWebWorkDir = standardWebWorkDir;
|
||||
}
|
||||
|
||||
public String getStandardWebTargetDir() {
|
||||
return standardWebTargetDir;
|
||||
}
|
||||
|
||||
public void setStandardWebTargetDir(String standardWebTargetDir) {
|
||||
this.standardWebTargetDir = standardWebTargetDir;
|
||||
}
|
||||
|
||||
public String getStandardWebOrganizationJsonPath() {
|
||||
return standardWebOrganizationJsonPath;
|
||||
}
|
||||
|
||||
public void setStandardWebOrganizationJsonPath(String standardWebOrganizationJsonPath) {
|
||||
this.standardWebOrganizationJsonPath = standardWebOrganizationJsonPath;
|
||||
}
|
||||
|
||||
public String getStandardRunUser() {
|
||||
return isBlank(standardRunUser) ? DEFAULT_STANDARD_RUN_USER : standardRunUser.trim();
|
||||
}
|
||||
|
||||
public void setStandardRunUser(String standardRunUser) {
|
||||
this.standardRunUser = standardRunUser;
|
||||
}
|
||||
|
||||
public String getStandardAppStartCommand() {
|
||||
return standardAppStartCommand;
|
||||
}
|
||||
|
||||
public void setStandardAppStartCommand(String standardAppStartCommand) {
|
||||
this.standardAppStartCommand = standardAppStartCommand;
|
||||
}
|
||||
|
||||
public String getStandardNginxStartCommand() {
|
||||
return standardNginxStartCommand;
|
||||
}
|
||||
|
||||
public void setStandardNginxStartCommand(String standardNginxStartCommand) {
|
||||
this.standardNginxStartCommand = standardNginxStartCommand;
|
||||
}
|
||||
|
||||
public String getStandardPostCheckCommand() {
|
||||
return standardPostCheckCommand;
|
||||
}
|
||||
|
||||
public void setStandardPostCheckCommand(String standardPostCheckCommand) {
|
||||
this.standardPostCheckCommand = standardPostCheckCommand;
|
||||
}
|
||||
|
||||
public String getUploadBaseDir() {
|
||||
return uploadBaseDir;
|
||||
}
|
||||
@@ -145,7 +321,7 @@ public class InitExecutorProperties {
|
||||
}
|
||||
|
||||
public String getStandardReceiverLicenseTargetPath() {
|
||||
return standardReceiverLicenseTargetPath;
|
||||
return firstNonBlank(standardReceiverLicenseTargetPath, buildStandardHomePath("/cmep/license/cmep.license"));
|
||||
}
|
||||
|
||||
public void setStandardReceiverLicenseTargetPath(String standardReceiverLicenseTargetPath) {
|
||||
@@ -241,7 +417,7 @@ public class InitExecutorProperties {
|
||||
}
|
||||
|
||||
public String getStandardRabbitSetupScriptPath() {
|
||||
return standardRabbitSetupScriptPath;
|
||||
return firstNonBlank(standardRabbitSetupScriptPath, buildStandardHomePath("/cpackage/rabq/setuprabq.sh"));
|
||||
}
|
||||
|
||||
public void setStandardRabbitSetupScriptPath(String standardRabbitSetupScriptPath) {
|
||||
@@ -249,7 +425,7 @@ public class InitExecutorProperties {
|
||||
}
|
||||
|
||||
public String getStandardRabbitQueueScriptPath() {
|
||||
return standardRabbitQueueScriptPath;
|
||||
return firstNonBlank(standardRabbitQueueScriptPath, buildStandardHomePath("/cpackage/rabq/addrabq_R_01.sh"));
|
||||
}
|
||||
|
||||
public void setStandardRabbitQueueScriptPath(String standardRabbitQueueScriptPath) {
|
||||
@@ -257,7 +433,7 @@ public class InitExecutorProperties {
|
||||
}
|
||||
|
||||
public String getStandardRabbitQueueScriptPath02() {
|
||||
return standardRabbitQueueScriptPath02;
|
||||
return firstNonBlank(standardRabbitQueueScriptPath02, buildStandardHomePath("/cpackage/rabq/addrabq_R_02.sh"));
|
||||
}
|
||||
|
||||
public void setStandardRabbitQueueScriptPath02(String standardRabbitQueueScriptPath02) {
|
||||
@@ -273,6 +449,13 @@ public class InitExecutorProperties {
|
||||
}
|
||||
|
||||
public List<String> getStandardRabbitAppConfigPatterns() {
|
||||
if (standardRabbitAppConfigPatterns == null || standardRabbitAppConfigPatterns.isEmpty()) {
|
||||
String home = getStandardHomeDir();
|
||||
return List.of(
|
||||
home + "/cmsp/application-prd*.properties",
|
||||
home + "/cmtp/application-prd*.properties"
|
||||
);
|
||||
}
|
||||
return standardRabbitAppConfigPatterns;
|
||||
}
|
||||
|
||||
@@ -295,4 +478,16 @@ public class InitExecutorProperties {
|
||||
public void setStandardRabbitCredentialSecretKey(String standardRabbitCredentialSecretKey) {
|
||||
this.standardRabbitCredentialSecretKey = standardRabbitCredentialSecretKey;
|
||||
}
|
||||
|
||||
private String buildStandardHomePath(String suffix) {
|
||||
return getStandardHomeDir() + suffix;
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String preferred, String fallback) {
|
||||
return isBlank(preferred) ? fallback : preferred.trim();
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -22,11 +22,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/init")
|
||||
public class InitInternalController {
|
||||
public class InitController {
|
||||
|
||||
private final InitService initService;
|
||||
|
||||
public InitInternalController(InitService initService) {
|
||||
public InitController(InitService initService) {
|
||||
this.initService = initService;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ public class InitInternalController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行初始化任务:按步骤串行执行,失败即中断。
|
||||
* 受理初始化任务执行请求。
|
||||
* 接口只负责启动后台执行并立即返回,前端需继续轮询任务详情、步骤状态和步骤日志。
|
||||
*/
|
||||
@PostMapping("/tasks/{taskId}/execute")
|
||||
public ApiResponse<InitTaskExecuteResponse> executeTask(@PathVariable String taskId) {
|
||||
+429
-9
@@ -26,6 +26,9 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -111,6 +114,13 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
case "mq_user_apply" -> executeMqUserApply(task, step);
|
||||
case "mq_queue_apply" -> executeMqQueueApply(task, step);
|
||||
case "app_mq_credentials_apply" -> executeAppMqCredentialsApply(task, step);
|
||||
case "db_render" -> executeStandardDbRender(task, step);
|
||||
case "db_apply" -> executeStandardDbApply(task, step);
|
||||
case "nginx_config_apply" -> executeStandardNginxConfigApply(task, step);
|
||||
case "web_config_apply" -> executeStandardWebConfigApply(task, step);
|
||||
case "start_cmsp_cmtp" -> executeStandardStartCommand(task, step, "start_cmsp_cmtp", properties.getStandardAppStartCommand());
|
||||
case "start_nginx" -> executeStandardStartCommand(task, step, "start_nginx", properties.getStandardNginxStartCommand());
|
||||
case "post_check" -> executeStandardStartCommand(task, step, "post_check", properties.getStandardPostCheckCommand());
|
||||
case "direct_db_init" -> executeDirectDbInit(task, step);
|
||||
case "tlq_init_cfg" -> executeTlqInitCfg(task, step);
|
||||
case "cfmq_apply" -> executeCfmqApply(task, step);
|
||||
@@ -195,8 +205,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
String logPath = writeInternalLog(task, step, "mq_user_apply skipped for non-standard product type");
|
||||
return InitStepExecutionResult.success("mq_user_apply skipped", 0, logPath);
|
||||
}
|
||||
if (!"RABBITMQ".equals(context.mqType())) {
|
||||
String logPath = writeInternalLog(task, step, "mq_user_apply skipped for non-rabbitmq mode");
|
||||
if (!shouldApplyStandardRabbitmq(context)) {
|
||||
String logPath = writeInternalLog(task, step, "mq_user_apply skipped for current standard mode");
|
||||
return InitStepExecutionResult.success("mq_user_apply skipped", 0, logPath);
|
||||
}
|
||||
|
||||
@@ -237,8 +247,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
String logPath = writeInternalLog(task, step, "mq_queue_apply skipped for non-standard product type");
|
||||
return InitStepExecutionResult.success("mq_queue_apply skipped", 0, logPath);
|
||||
}
|
||||
if (!"RABBITMQ".equals(context.mqType())) {
|
||||
String logPath = writeInternalLog(task, step, "mq_queue_apply skipped for non-rabbitmq mode");
|
||||
if (!shouldApplyStandardRabbitmq(context)) {
|
||||
String logPath = writeInternalLog(task, step, "mq_queue_apply skipped for current standard mode");
|
||||
return InitStepExecutionResult.success("mq_queue_apply skipped", 0, logPath);
|
||||
}
|
||||
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
|
||||
@@ -343,7 +353,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
InitTaskEntity task,
|
||||
PlanContext context
|
||||
) throws IOException {
|
||||
String standardHome = firstNonBlank(properties.getStandardHomeDir(), "/home/cemp4i");
|
||||
String standardHome = properties.getStandardHomeDir();
|
||||
Path sourceSetupScript = resolveRequiredScriptPath(
|
||||
properties.getStandardRabbitSetupScriptPath(),
|
||||
standardHome + "/cpackage/rabq/setuprabq.sh",
|
||||
@@ -381,7 +391,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
InitTaskEntity task,
|
||||
PlanContext context
|
||||
) throws IOException {
|
||||
String standardHome = firstNonBlank(properties.getStandardHomeDir(), "/home/cemp4i");
|
||||
String standardHome = properties.getStandardHomeDir();
|
||||
Path sourceQueueScript = resolveStandardRabbitQueueSourceScript(context, standardHome);
|
||||
|
||||
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
|
||||
@@ -416,7 +426,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
String permissionsLine = "rabbitmqctl set_permissions -p /RQ " + userQuoted + " '.*' '.*' '.*'";
|
||||
String profileLine = "source /etc/profile >/dev/null 2>&1 || true";
|
||||
String pathLine = "export PATH=$PATH:/usr/local/rabbitmq/sbin:/usr/local/rabbitmq-server/sbin:/usr/lib/rabbitmq/bin";
|
||||
String sourceLine = "source " + firstNonBlank(properties.getStandardCpconfigPath(), "/home/cemp4i/cpconfig.cfg");
|
||||
String sourceLine = "source " + properties.getStandardCpconfigPath();
|
||||
String cdLine = "cd " + stagingScriptDir;
|
||||
|
||||
for (String line : lines) {
|
||||
@@ -527,6 +537,16 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
private boolean shouldApplyStandardRabbitmq(PlanContext context) {
|
||||
if ("ENTERPRISE".equals(context.productType())) {
|
||||
return "RABBITMQ".equals(context.mqType());
|
||||
}
|
||||
if ("INDIRECT".equals(context.productType())) {
|
||||
return "RABBITMQ".equals(context.mqType()) || "RABBITMQ_TLQ".equals(context.mqType());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean waitRabbitmqReady(Path logFile, String ctl) throws IOException, InterruptedException {
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
int code = runCommand(logFile, Arrays.asList(ctl, "status"), true);
|
||||
@@ -701,6 +721,10 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply skipped for non-standard product type");
|
||||
return InitStepExecutionResult.success("app_mq_credentials_apply skipped", 0, logPath);
|
||||
}
|
||||
if (!shouldApplyStandardRabbitmq(context)) {
|
||||
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply skipped for current standard mode");
|
||||
return InitStepExecutionResult.success("app_mq_credentials_apply skipped", 0, logPath);
|
||||
}
|
||||
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
|
||||
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply simulated");
|
||||
return InitStepExecutionResult.success("app_mq_credentials_apply simulated", 0, logPath);
|
||||
@@ -797,6 +821,237 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private InitStepExecutionResult executeStandardDbRender(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
|
||||
PlanContext context = loadPlanContext(task);
|
||||
if ("DIRECT".equals(context.productType())) {
|
||||
String logPath = writeInternalLog(task, step, "db_render skipped for direct product type");
|
||||
return InitStepExecutionResult.success("db_render skipped", 0, logPath);
|
||||
}
|
||||
|
||||
Path scriptBaseDir = Paths.get(firstNonBlank(
|
||||
properties.getStandardDbScriptBaseDir(),
|
||||
properties.getStandardHomeDir() + "/cpackage"
|
||||
)).normalize().toAbsolutePath();
|
||||
Path sqlScriptDir = scriptBaseDir.resolve("sql_script");
|
||||
Path sqlTmpDir = sqlScriptDir.resolve("sql_script_tmp");
|
||||
Path cmtpTemplate = sqlTmpDir.resolve(resolveStandardDbCmtpTemplateName(context));
|
||||
Path cmepDmlTemplate = sqlScriptDir.resolve("CMEP-DML.sql");
|
||||
Path cmepEntryTemplate = sqlScriptDir.resolve(firstNonBlank(properties.getStandardDbEntrySqlFileName(), "CMEP.sql"));
|
||||
Path schemaTemplate = sqlScriptDir.resolve("SCHEMA-DDL.sql");
|
||||
Path upOrgTemplate = sqlScriptDir.resolve("UP-ORG-INFO.sql");
|
||||
|
||||
List<Path> requiredFiles = List.of(cmtpTemplate, cmepDmlTemplate, cmepEntryTemplate, schemaTemplate, upOrgTemplate);
|
||||
for (Path requiredFile : requiredFiles) {
|
||||
if (!Files.exists(requiredFile) || !Files.isRegularFile(requiredFile)) {
|
||||
String logPath = writeInternalLog(task, step, "db_render failed: required sql template not found: " + requiredFile);
|
||||
return InitStepExecutionResult.failure("db_render failed: sql template not found", -1, logPath);
|
||||
}
|
||||
}
|
||||
|
||||
Path stageSqlDir = resolveStandardDbStageSqlDir(task);
|
||||
Files.createDirectories(stageSqlDir);
|
||||
writeRenderedSql(stageSqlDir.resolve("CMTP-01.sql"), Files.readString(cmtpTemplate), buildStandardSqlReplacements(context));
|
||||
writeRenderedSql(stageSqlDir.resolve("CMEP-DML.sql"), Files.readString(cmepDmlTemplate), buildStandardSqlReplacements(context));
|
||||
Files.copy(cmepEntryTemplate, stageSqlDir.resolve("CMEP.sql"), StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(schemaTemplate, stageSqlDir.resolve("SCHEMA-DDL.sql"), StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(upOrgTemplate, stageSqlDir.resolve("UP-ORG-INFO.sql"), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
String message = "db sql rendered, dir=" + stageSqlDir;
|
||||
String logPath = writeInternalLog(task, step, message);
|
||||
return InitStepExecutionResult.success(message, 0, logPath);
|
||||
}
|
||||
|
||||
private InitStepExecutionResult executeStandardDbApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
|
||||
PlanContext context = loadPlanContext(task);
|
||||
if ("DIRECT".equals(context.productType())) {
|
||||
String logPath = writeInternalLog(task, step, "db_apply skipped for direct product type");
|
||||
return InitStepExecutionResult.success("db_apply skipped", 0, logPath);
|
||||
}
|
||||
|
||||
Path stageSqlDir = resolveStandardDbStageSqlDir(task);
|
||||
if (!Files.isDirectory(stageSqlDir)) {
|
||||
String logPath = writeInternalLog(task, step, "db_apply failed: staged sql dir not found: " + stageSqlDir);
|
||||
return InitStepExecutionResult.failure("db_apply failed: staged sql dir not found", -1, logPath);
|
||||
}
|
||||
|
||||
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
|
||||
String logPath = writeInternalLog(task, step, "db_apply simulated, staged dir=" + stageSqlDir);
|
||||
return InitStepExecutionResult.success("db_apply simulated", 0, logPath);
|
||||
}
|
||||
|
||||
Path loadDir = Paths.get(firstNonBlank(
|
||||
properties.getStandardDbLoadDir(),
|
||||
properties.getStandardHomeDir() + "/mysql/loadfilepath"
|
||||
)).normalize().toAbsolutePath();
|
||||
Files.createDirectories(loadDir);
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(stageSqlDir)) {
|
||||
for (Path item : stream) {
|
||||
if (Files.isRegularFile(item)) {
|
||||
Files.copy(item, loadDir.resolve(item.getFileName()), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Path logFile = prepareLogPath(task, step);
|
||||
appendLog(logFile, "staged sql copied: " + stageSqlDir + " -> " + loadDir);
|
||||
String applyCommand = normalize(properties.getStandardDbApplyCommand());
|
||||
if (isBlank(applyCommand)) {
|
||||
appendLog(logFile, "db_apply failed: standardDbApplyCommand is blank");
|
||||
return InitStepExecutionResult.failure("db_apply failed: apply command is blank", -1, logFile.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
int code = runCommand(
|
||||
logFile,
|
||||
Arrays.asList("/bin/zsh", "-lc", applyCommand),
|
||||
false,
|
||||
loadDir,
|
||||
buildStandardDbApplyEnvironment(loadDir)
|
||||
);
|
||||
if (code != 0) {
|
||||
return InitStepExecutionResult.failure("db_apply failed", code, logFile.toString());
|
||||
}
|
||||
return InitStepExecutionResult.success("db sql applied to load dir=" + loadDir + ", command executed", 0, logFile.toString());
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return InitStepExecutionResult.failure("db_apply interrupted", -1, logFile.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private InitStepExecutionResult executeStandardNginxConfigApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
|
||||
PlanContext context = loadPlanContext(task);
|
||||
if ("DIRECT".equals(context.productType())) {
|
||||
String logPath = writeInternalLog(task, step, "nginx_config_apply skipped for direct product type");
|
||||
return InitStepExecutionResult.success("nginx_config_apply skipped", 0, logPath);
|
||||
}
|
||||
|
||||
Path templatePath = Paths.get(firstNonBlank(
|
||||
properties.getStandardNginxTemplatePath(),
|
||||
properties.getStandardHomeDir() + "/cpackage/software/nginx.conf"
|
||||
)).normalize().toAbsolutePath();
|
||||
if (!Files.exists(templatePath) || !Files.isRegularFile(templatePath)) {
|
||||
String logPath = writeInternalLog(task, step, "nginx_config_apply failed: template not found: " + templatePath);
|
||||
return InitStepExecutionResult.failure("nginx_config_apply failed: template not found", -1, logPath);
|
||||
}
|
||||
|
||||
Path stageFile = resolveStandardNginxStageFile(task);
|
||||
Files.createDirectories(stageFile.getParent());
|
||||
Map<String, String> replacements = buildStandardReplacements(context);
|
||||
String rendered = Files.readString(templatePath)
|
||||
.replace("sptp.01app.ip", normalize(replacements.get("sptp_01app_ip")))
|
||||
.replace("sptp.02app.ip", normalize(replacements.get("sptp_02app_ip")));
|
||||
Files.writeString(stageFile, rendered, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
|
||||
Path logFile = prepareLogPath(task, step);
|
||||
appendLog(logFile, "nginx config staged: " + templatePath + " -> " + stageFile);
|
||||
|
||||
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
|
||||
return InitStepExecutionResult.success("nginx config staged", 0, logFile.toString());
|
||||
}
|
||||
|
||||
Path targetPath = Paths.get(firstNonBlank(properties.getStandardNginxTargetPath(), "/usr/local/nginx/conf/nginx.conf"))
|
||||
.normalize()
|
||||
.toAbsolutePath();
|
||||
copyToTarget(stageFile, targetPath);
|
||||
appendLog(logFile, "nginx config applied: " + stageFile + " -> " + targetPath);
|
||||
return InitStepExecutionResult.success("nginx config applied", 0, logFile.toString());
|
||||
}
|
||||
|
||||
private InitStepExecutionResult executeStandardWebConfigApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
|
||||
PlanContext context = loadPlanContext(task);
|
||||
if ("DIRECT".equals(context.productType())) {
|
||||
String logPath = writeInternalLog(task, step, "web_config_apply skipped for direct product type");
|
||||
return InitStepExecutionResult.success("web_config_apply skipped", 0, logPath);
|
||||
}
|
||||
|
||||
Path frontZip = Paths.get(firstNonBlank(
|
||||
properties.getStandardWebFrontZipPath(),
|
||||
properties.getStandardHomeDir() + "/cpackage/front/front.zip"
|
||||
)).normalize().toAbsolutePath();
|
||||
if (!Files.exists(frontZip) || !Files.isRegularFile(frontZip)) {
|
||||
String logPath = writeInternalLog(task, step, "web_config_apply failed: front zip not found: " + frontZip);
|
||||
return InitStepExecutionResult.failure("web_config_apply failed: front zip not found", -1, logPath);
|
||||
}
|
||||
|
||||
Path stagedWebDir = resolveStandardWebStageDir(task);
|
||||
recreateDirectory(stagedWebDir);
|
||||
unzipToDirectory(frontZip, stagedWebDir);
|
||||
|
||||
Path stagedOrganizationJson = stagedWebDir.resolve("organization.json");
|
||||
String organizationName = normalize(context.orgNameCn());
|
||||
Files.writeString(
|
||||
stagedOrganizationJson,
|
||||
"{\"organizationName\":\"" + escapeJson(organizationName) + "\"}",
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING
|
||||
);
|
||||
|
||||
Path logFile = prepareLogPath(task, step);
|
||||
appendLog(logFile, "web assets staged: " + frontZip + " -> " + stagedWebDir);
|
||||
|
||||
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
|
||||
return InitStepExecutionResult.success("web assets staged", 0, logFile.toString());
|
||||
}
|
||||
|
||||
Path targetDir = Paths.get(firstNonBlank(properties.getStandardWebTargetDir(), "/usr/local/nginx/html"))
|
||||
.normalize()
|
||||
.toAbsolutePath();
|
||||
recreateDirectory(targetDir);
|
||||
copyDirectoryContents(stagedWebDir, targetDir, "organization.json");
|
||||
|
||||
Path organizationJsonTarget = Paths.get(firstNonBlank(
|
||||
properties.getStandardWebOrganizationJsonPath(),
|
||||
"/usr/local/nginx/html/organization.json"
|
||||
)).normalize().toAbsolutePath();
|
||||
copyToTarget(stagedOrganizationJson, organizationJsonTarget);
|
||||
appendLog(logFile, "web assets applied: " + stagedWebDir + " -> " + targetDir);
|
||||
appendLog(logFile, "organization.json applied: " + stagedOrganizationJson + " -> " + organizationJsonTarget);
|
||||
return InitStepExecutionResult.success("web assets applied", 0, logFile.toString());
|
||||
}
|
||||
|
||||
private InitStepExecutionResult executeStandardStartCommand(
|
||||
InitTaskEntity task,
|
||||
InitTaskStepEntity step,
|
||||
String stepName,
|
||||
String command
|
||||
) throws IOException {
|
||||
PlanContext context = loadPlanContext(task);
|
||||
if ("DIRECT".equals(context.productType())) {
|
||||
String logPath = writeInternalLog(task, step, stepName + " skipped for direct product type");
|
||||
return InitStepExecutionResult.success(stepName + " skipped", 0, logPath);
|
||||
}
|
||||
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
|
||||
String logPath = writeInternalLog(task, step, stepName + " simulated");
|
||||
return InitStepExecutionResult.success(stepName + " simulated", 0, logPath);
|
||||
}
|
||||
if (isBlank(command)) {
|
||||
String logPath = writeInternalLog(task, step, stepName + " failed: command is blank");
|
||||
return InitStepExecutionResult.failure(stepName + " failed: command is blank", -1, logPath);
|
||||
}
|
||||
|
||||
Path logFile = prepareLogPath(task, step);
|
||||
Path workingDirectory = resolveExistingStandardWorkingDirectory();
|
||||
if (workingDirectory == null) {
|
||||
appendLog(logFile, "cwd skipped: standardHomeDir not found");
|
||||
}
|
||||
try {
|
||||
int code = runCommand(
|
||||
logFile,
|
||||
Arrays.asList("/bin/zsh", "-lc", command),
|
||||
false,
|
||||
workingDirectory,
|
||||
buildStandardStartEnvironment(stepName)
|
||||
);
|
||||
if (code != 0) {
|
||||
return InitStepExecutionResult.failure(stepName + " failed", code, logFile.toString());
|
||||
}
|
||||
return InitStepExecutionResult.success(stepName + " success", 0, logFile.toString());
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return InitStepExecutionResult.failure(stepName + " interrupted", -1, logFile.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private InitStepExecutionResult executeTlqInitCfg(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
|
||||
PlanContext context = loadPlanContext(task);
|
||||
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) {
|
||||
@@ -976,6 +1231,20 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
return replacements;
|
||||
}
|
||||
|
||||
private Map<String, String> buildStandardSqlReplacements(PlanContext context) {
|
||||
Map<String, String> replacements = new LinkedHashMap<>();
|
||||
Map<String, String> configReplacements = buildStandardReplacements(context);
|
||||
replacements.put("AAAABBBBXXX", normalize(context.orgCode()));
|
||||
replacements.put("01tlq.ip", normalize(configReplacements.get("mq_01_ip")));
|
||||
replacements.put("02tlq.ip", normalize(configReplacements.get("mq_02_ip")));
|
||||
replacements.put("sptp.01app.ip", normalize(configReplacements.get("sptp_01app_ip")));
|
||||
replacements.put("sptp.02app.ip", normalize(configReplacements.get("sptp_02app_ip")));
|
||||
replacements.put("mybiccode", normalize(configReplacements.get("MY_BIC_CODE")));
|
||||
replacements.put("mycipsid", normalize(configReplacements.get("MY_CIPS_ID")));
|
||||
replacements.put("myleicode", normalize(configReplacements.get("MY_LEI_CODE")));
|
||||
return replacements;
|
||||
}
|
||||
|
||||
private Map<String, String> buildDirectReplacements(PlanContext context) {
|
||||
Map<String, String> replacements = new LinkedHashMap<>();
|
||||
String node01 = normalize(context.node01Ip());
|
||||
@@ -1117,7 +1386,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
}
|
||||
|
||||
List<Path> candidates = new ArrayList<>();
|
||||
addAbsoluteCandidate(candidates, firstNonBlank(properties.getStandardCpconfigPath(), "/home/cemp4i/cpconfig.cfg"));
|
||||
addAbsoluteCandidate(candidates, properties.getStandardCpconfigPath());
|
||||
return candidates;
|
||||
}
|
||||
|
||||
@@ -1229,6 +1498,53 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
return target;
|
||||
}
|
||||
|
||||
private Path resolveStandardDbStageSqlDir(InitTaskEntity task) {
|
||||
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
|
||||
.normalize()
|
||||
.toAbsolutePath();
|
||||
return stageRoot.resolve(task.getTaskId()).resolve("db").resolve("sql");
|
||||
}
|
||||
|
||||
private Path resolveStandardNginxStageFile(InitTaskEntity task) {
|
||||
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
|
||||
.normalize()
|
||||
.toAbsolutePath();
|
||||
return stageRoot.resolve(task.getTaskId()).resolve("nginx").resolve("nginx.conf");
|
||||
}
|
||||
|
||||
private Path resolveStandardWebStageDir(InitTaskEntity task) {
|
||||
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
|
||||
.normalize()
|
||||
.toAbsolutePath();
|
||||
return stageRoot.resolve(task.getTaskId()).resolve("web");
|
||||
}
|
||||
|
||||
private Path resolveExistingStandardWorkingDirectory() {
|
||||
String configured = properties.getStandardHomeDir();
|
||||
if (isBlank(configured)) {
|
||||
return null;
|
||||
}
|
||||
Path path = Paths.get(configured).normalize().toAbsolutePath();
|
||||
if (!Files.isDirectory(path)) {
|
||||
return null;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private String resolveStandardDbCmtpTemplateName(PlanContext context) {
|
||||
return ("TLQ".equals(context.mqType()) || "RABBITMQ_TLQ".equals(context.mqType()))
|
||||
? "CMTP-01-tlq.sql"
|
||||
: "CMTP-01-raq.sql";
|
||||
}
|
||||
|
||||
private void writeRenderedSql(Path target, String templateContent, Map<String, String> replacements) throws IOException {
|
||||
String rendered = templateContent;
|
||||
for (Map.Entry<String, String> entry : replacements.entrySet()) {
|
||||
rendered = rendered.replace(entry.getKey(), normalize(entry.getValue()));
|
||||
}
|
||||
Files.writeString(target, rendered, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
|
||||
private void copyToTarget(Path source, Path target) throws IOException {
|
||||
if (target.getParent() != null) {
|
||||
Files.createDirectories(target.getParent());
|
||||
@@ -1236,11 +1552,78 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private void unzipToDirectory(Path zipFile, Path targetDir) throws IOException {
|
||||
Files.createDirectories(targetDir);
|
||||
try (ZipInputStream inputStream = new ZipInputStream(Files.newInputStream(zipFile))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = inputStream.getNextEntry()) != null) {
|
||||
Path output = targetDir.resolve(entry.getName()).normalize();
|
||||
if (!output.startsWith(targetDir)) {
|
||||
throw new IOException("zip entry escapes target dir: " + entry.getName());
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(output);
|
||||
} else {
|
||||
if (output.getParent() != null) {
|
||||
Files.createDirectories(output.getParent());
|
||||
}
|
||||
Files.copy(inputStream, output, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
inputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void recreateDirectory(Path directory) throws IOException {
|
||||
if (Files.exists(directory)) {
|
||||
deleteRecursively(directory);
|
||||
}
|
||||
Files.createDirectories(directory);
|
||||
}
|
||||
|
||||
private void copyDirectoryContents(Path sourceDir, Path targetDir, String... excludes) throws IOException {
|
||||
Set<String> excludeSet = Set.of(excludes);
|
||||
try (Stream<Path> stream = Files.walk(sourceDir)) {
|
||||
for (Path source : stream.toList()) {
|
||||
if (source.equals(sourceDir)) {
|
||||
continue;
|
||||
}
|
||||
Path relative = sourceDir.relativize(source);
|
||||
if (!relative.toString().isEmpty() && excludeSet.contains(relative.toString())) {
|
||||
continue;
|
||||
}
|
||||
Path target = targetDir.resolve(relative.toString()).normalize();
|
||||
if (Files.isDirectory(source)) {
|
||||
Files.createDirectories(target);
|
||||
} else {
|
||||
copyToTarget(source, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteRecursively(Path path) throws IOException {
|
||||
if (!Files.exists(path)) {
|
||||
return;
|
||||
}
|
||||
try (Stream<Path> stream = Files.walk(path)) {
|
||||
for (Path item : stream.sorted((left, right) -> right.getNameCount() - left.getNameCount()).toList()) {
|
||||
Files.deleteIfExists(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String escapeJson(String raw) {
|
||||
return normalize(raw)
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private String resolveReceiverLicenseTargetPath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return firstNonBlank(properties.getDirectReceiverLicenseTargetPath(), "/home/cmep4d/cmep/license/cmep.license");
|
||||
}
|
||||
return firstNonBlank(properties.getStandardReceiverLicenseTargetPath(), "/home/cemp4i/cmep/license/cmep.license");
|
||||
return properties.getStandardReceiverLicenseTargetPath();
|
||||
}
|
||||
|
||||
private String resolveTlqLicenseTargetPath(String productType) {
|
||||
@@ -1292,11 +1675,25 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
}
|
||||
|
||||
private int runCommand(Path logFile, List<String> command, boolean ignoreFailure, Path workingDirectory) throws IOException, InterruptedException {
|
||||
return runCommand(logFile, command, ignoreFailure, workingDirectory, null);
|
||||
}
|
||||
|
||||
private int runCommand(
|
||||
Path logFile,
|
||||
List<String> command,
|
||||
boolean ignoreFailure,
|
||||
Path workingDirectory,
|
||||
Map<String, String> extraEnvironment
|
||||
) throws IOException, InterruptedException {
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
if (workingDirectory != null) {
|
||||
pb.directory(workingDirectory.toFile());
|
||||
appendLog(logFile, "cwd=" + workingDirectory);
|
||||
}
|
||||
if (extraEnvironment != null && !extraEnvironment.isEmpty()) {
|
||||
pb.environment().putAll(extraEnvironment);
|
||||
appendLog(logFile, "envKeys=" + extraEnvironment.keySet());
|
||||
}
|
||||
pb.redirectErrorStream(true);
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
|
||||
Process process = pb.start();
|
||||
@@ -1313,6 +1710,29 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
private Map<String, String> buildStandardDbApplyEnvironment(Path loadDir) {
|
||||
Map<String, String> environment = new LinkedHashMap<>();
|
||||
if (!isBlank(properties.getStandardDbUsername())) {
|
||||
environment.put("DB_USER", normalize(properties.getStandardDbUsername()));
|
||||
}
|
||||
if (!isBlank(properties.getStandardDbPassword())) {
|
||||
environment.put("DB_PASSWORD", normalize(properties.getStandardDbPassword()));
|
||||
}
|
||||
if (loadDir != null) {
|
||||
environment.put("LOAD_DIR", loadDir.toAbsolutePath().normalize().toString());
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
private Map<String, String> buildStandardStartEnvironment(String stepName) {
|
||||
Map<String, String> environment = new LinkedHashMap<>();
|
||||
if ("start_cmsp_cmtp".equals(stepName)) {
|
||||
environment.put("STANDARD_HOME_DIR", normalize(properties.getStandardHomeDir()));
|
||||
environment.put("STANDARD_RUN_USER", normalize(properties.getStandardRunUser()));
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
private List<Path> resolveGlobFiles(List<String> patterns) throws IOException {
|
||||
List<Path> result = new ArrayList<>();
|
||||
if (patterns == null || patterns.isEmpty()) {
|
||||
|
||||
@@ -21,20 +21,27 @@ import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
||||
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -43,7 +50,8 @@ public class InitService {
|
||||
private static final Set<String> PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT");
|
||||
private static final Set<String> DEPLOY_STANDARD = Set.of("SINGLE", "DUAL");
|
||||
private static final Set<String> DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD");
|
||||
private static final Set<String> MQ_STANDARD = Set.of("RABBITMQ", "TLQ");
|
||||
private static final Set<String> MQ_ENTERPRISE = Set.of("RABBITMQ", "TLQ");
|
||||
private static final Set<String> MQ_INDIRECT = Set.of("RABBITMQ", "RABBITMQ_TLQ");
|
||||
private static final Set<String> MQ_DIRECT = Set.of("RABBITMQ_TLQ", "RABBITMQ_CFMQ");
|
||||
private static final Pattern FILE_ID_PATTERN = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$");
|
||||
|
||||
@@ -54,7 +62,10 @@ public class InitService {
|
||||
private final InitStepExecutor initStepExecutor;
|
||||
private final InitCommandProfileService initCommandProfileService;
|
||||
private final InitExecutorProperties initExecutorProperties;
|
||||
private final Executor backgroundExecutor;
|
||||
private final ConcurrentMap<String, CompletableFuture<Void>> activeExecutions;
|
||||
|
||||
@Autowired
|
||||
public InitService(
|
||||
InitTaskRepository initTaskRepository,
|
||||
InitTaskStepRepository initTaskStepRepository,
|
||||
@@ -63,6 +74,30 @@ public class InitService {
|
||||
InitStepExecutor initStepExecutor,
|
||||
InitCommandProfileService initCommandProfileService,
|
||||
InitExecutorProperties initExecutorProperties
|
||||
) {
|
||||
this(
|
||||
initTaskRepository,
|
||||
initTaskStepRepository,
|
||||
cisdPresetProperties,
|
||||
objectMapper,
|
||||
initStepExecutor,
|
||||
initCommandProfileService,
|
||||
initExecutorProperties,
|
||||
Executors.newCachedThreadPool(),
|
||||
new ConcurrentHashMap<>()
|
||||
);
|
||||
}
|
||||
|
||||
InitService(
|
||||
InitTaskRepository initTaskRepository,
|
||||
InitTaskStepRepository initTaskStepRepository,
|
||||
CisdPresetProperties cisdPresetProperties,
|
||||
ObjectMapper objectMapper,
|
||||
InitStepExecutor initStepExecutor,
|
||||
InitCommandProfileService initCommandProfileService,
|
||||
InitExecutorProperties initExecutorProperties,
|
||||
Executor backgroundExecutor,
|
||||
ConcurrentMap<String, CompletableFuture<Void>> activeExecutions
|
||||
) {
|
||||
this.initTaskRepository = initTaskRepository;
|
||||
this.initTaskStepRepository = initTaskStepRepository;
|
||||
@@ -71,6 +106,8 @@ public class InitService {
|
||||
this.initStepExecutor = initStepExecutor;
|
||||
this.initCommandProfileService = initCommandProfileService;
|
||||
this.initExecutorProperties = initExecutorProperties;
|
||||
this.backgroundExecutor = backgroundExecutor;
|
||||
this.activeExecutions = activeExecutions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,48 +275,97 @@ public class InitService {
|
||||
if (steps.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "init task has no steps: " + taskId);
|
||||
}
|
||||
if (steps.stream().allMatch(step -> "SUCCESS".equals(step.getStatus()))) {
|
||||
task.setStatus("SUCCESS");
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
initTaskRepository.update(task);
|
||||
return buildExecuteResponse(task, steps);
|
||||
}
|
||||
|
||||
// 任务进入运行态,后续每个步骤状态独立推进
|
||||
task.setStatus("RUNNING");
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
initTaskRepository.update(task);
|
||||
synchronized (activeExecutions) {
|
||||
CompletableFuture<Void> active = activeExecutions.get(taskId);
|
||||
if (active != null && !active.isDone()) {
|
||||
task.setStatus("RUNNING");
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
initTaskRepository.update(task);
|
||||
return buildExecuteResponse(task, steps);
|
||||
}
|
||||
|
||||
task.setStatus("RUNNING");
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
initTaskRepository.update(task);
|
||||
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> executeTaskInBackground(taskId), backgroundExecutor)
|
||||
.whenComplete((ignored, ex) -> activeExecutions.remove(taskId));
|
||||
activeExecutions.put(taskId, future);
|
||||
}
|
||||
return buildExecuteResponse(task, steps);
|
||||
}
|
||||
|
||||
private void executeTaskInBackground(String taskId) {
|
||||
InitTaskEntity task = requireTask(taskId);
|
||||
List<InitTaskStepEntity> steps = new ArrayList<>(initTaskStepRepository.findByTaskIdOrderByStepNo(taskId));
|
||||
if (steps.isEmpty()) {
|
||||
task.setStatus("FAILED");
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
initTaskRepository.update(task);
|
||||
return;
|
||||
}
|
||||
steps.sort(Comparator.comparing(item -> item.getStepNo() == null ? Integer.MAX_VALUE : item.getStepNo()));
|
||||
resetResidualRunningSteps(steps);
|
||||
initTaskStepRepository.updateBatch(steps);
|
||||
|
||||
int successSteps = 0;
|
||||
boolean failed = false;
|
||||
for (InitTaskStepEntity step : steps) {
|
||||
if ("SUCCESS".equals(step.getStatus())) {
|
||||
continue;
|
||||
}
|
||||
step.setStatus("RUNNING");
|
||||
step.setMessage("running");
|
||||
step.setUpdateTime(LocalDateTime.now());
|
||||
initTaskStepRepository.updateBatch(steps);
|
||||
|
||||
InitStepExecutionResult result;
|
||||
try {
|
||||
result = initStepExecutor.execute(task, step);
|
||||
} catch (RuntimeException ex) {
|
||||
result = InitStepExecutionResult.failure(ex.getMessage(), -1, step.getLogPath());
|
||||
}
|
||||
|
||||
// 串行执行:每步结束后立即持有 exitCode/logPath/message,便于失败诊断
|
||||
InitStepExecutionResult result = initStepExecutor.execute(task, step);
|
||||
step.setExitCode(result.getExitCode());
|
||||
step.setLogPath(result.getLogPath());
|
||||
step.setMessage(result.getMessage());
|
||||
step.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
if (result.isSuccess()) {
|
||||
step.setStatus("SUCCESS");
|
||||
successSteps++;
|
||||
} else {
|
||||
// 当前步骤失败后立刻中断,保持“失败即停”语义
|
||||
step.setStatus("FAILED");
|
||||
step.setStatus(result.isSuccess() ? "SUCCESS" : "FAILED");
|
||||
initTaskStepRepository.updateBatch(steps);
|
||||
if (!result.isSuccess()) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
initTaskStepRepository.updateBatch(steps);
|
||||
|
||||
// 任务最终态由步骤执行结果汇总得出
|
||||
task.setStatus(failed ? "FAILED" : "SUCCESS");
|
||||
task.setUpdateTime(LocalDateTime.now());
|
||||
initTaskRepository.update(task);
|
||||
}
|
||||
|
||||
private void resetResidualRunningSteps(List<InitTaskStepEntity> steps) {
|
||||
for (InitTaskStepEntity step : steps) {
|
||||
if (!"RUNNING".equals(step.getStatus())) {
|
||||
continue;
|
||||
}
|
||||
step.setStatus("FAILED");
|
||||
step.setMessage(isBlank(step.getMessage()) ? "interrupted previously" : step.getMessage());
|
||||
step.setUpdateTime(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
private InitTaskExecuteResponse buildExecuteResponse(InitTaskEntity task, List<InitTaskStepEntity> steps) {
|
||||
InitTaskExecuteResponse response = new InitTaskExecuteResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setTaskId(task.getTaskId());
|
||||
response.setStatus(task.getStatus());
|
||||
response.setTotalSteps(steps.size());
|
||||
response.setSuccessSteps(successSteps);
|
||||
response.setSuccessSteps((int) steps.stream().filter(step -> "SUCCESS".equals(step.getStatus())).count());
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -325,10 +411,60 @@ public class InitService {
|
||||
}
|
||||
return List.of("RENDER_CONFIG", "FILE_PREPARE", "MQ_USER_APPLY", "DB_INIT", "TLQ_INIT_CFG", "VERIFY");
|
||||
}
|
||||
if ("TLQ".equals(mqType)) {
|
||||
return List.of("RENDER_CONFIG", "FILE_PREPARE", "SET_TLQ", "MQ_USER_APPLY", "MQ_QUEUE_APPLY", "DB_INIT", "DEPLOY_RECEIVER", "APP_MQ_CREDENTIALS_APPLY", "VERIFY");
|
||||
if ("ENTERPRISE".equals(productType) && "TLQ".equals(mqType)) {
|
||||
return List.of(
|
||||
"RENDER_CONFIG",
|
||||
"FILE_PREPARE",
|
||||
"SET_TLQ",
|
||||
"TLQ_CONF_APPLY",
|
||||
"DB_RENDER",
|
||||
"DB_APPLY",
|
||||
"DEPLOY_RECEIVER",
|
||||
"NGINX_CONFIG_APPLY",
|
||||
"WEB_CONFIG_APPLY",
|
||||
"START_CMSP_CMTP",
|
||||
"START_NGINX",
|
||||
"POST_CHECK",
|
||||
"VERIFY"
|
||||
);
|
||||
}
|
||||
return List.of("RENDER_CONFIG", "FILE_PREPARE", "SET_RABBITMQ", "MQ_USER_APPLY", "MQ_QUEUE_APPLY", "DB_INIT", "DEPLOY_RECEIVER", "APP_MQ_CREDENTIALS_APPLY", "VERIFY");
|
||||
if ("INDIRECT".equals(productType) && "RABBITMQ_TLQ".equals(mqType)) {
|
||||
return List.of(
|
||||
"RENDER_CONFIG",
|
||||
"FILE_PREPARE",
|
||||
"SET_TLQ",
|
||||
"TLQ_CONF_APPLY",
|
||||
"MQ_USER_APPLY",
|
||||
"MQ_QUEUE_APPLY",
|
||||
"DB_RENDER",
|
||||
"DB_APPLY",
|
||||
"DEPLOY_RECEIVER",
|
||||
"APP_MQ_CREDENTIALS_APPLY",
|
||||
"NGINX_CONFIG_APPLY",
|
||||
"WEB_CONFIG_APPLY",
|
||||
"START_CMSP_CMTP",
|
||||
"START_NGINX",
|
||||
"POST_CHECK",
|
||||
"VERIFY"
|
||||
);
|
||||
}
|
||||
return List.of(
|
||||
"RENDER_CONFIG",
|
||||
"FILE_PREPARE",
|
||||
"SET_RABBITMQ",
|
||||
"MQ_USER_APPLY",
|
||||
"MQ_QUEUE_APPLY",
|
||||
"DB_RENDER",
|
||||
"DB_APPLY",
|
||||
"DEPLOY_RECEIVER",
|
||||
"APP_MQ_CREDENTIALS_APPLY",
|
||||
"NGINX_CONFIG_APPLY",
|
||||
"WEB_CONFIG_APPLY",
|
||||
"START_CMSP_CMTP",
|
||||
"START_NGINX",
|
||||
"POST_CHECK",
|
||||
"VERIFY"
|
||||
);
|
||||
}
|
||||
|
||||
private String buildTaskId() {
|
||||
@@ -387,23 +523,33 @@ public class InitService {
|
||||
return;
|
||||
}
|
||||
|
||||
validateStandard(request, deployMode, mqType);
|
||||
validateStandard(productType, request, deployMode, mqType);
|
||||
}
|
||||
|
||||
private void validateStandard(InitPreviewRequest request, String deployMode, String mqType) {
|
||||
private void validateStandard(String productType, InitPreviewRequest request, String deployMode, String mqType) {
|
||||
requireAllowed("deployMode", deployMode, DEPLOY_STANDARD);
|
||||
requireAllowed("mq.mqType", mqType, MQ_STANDARD);
|
||||
requireAllowed("mq.mqType", mqType, resolveStandardMqAllowed(productType));
|
||||
|
||||
if ("DUAL".equals(deployMode)) {
|
||||
requireNotBlank("nodes.node02Ip", request.getNodes().getNode02Ip());
|
||||
}
|
||||
|
||||
if ("TLQ".equals(mqType)) {
|
||||
if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) {
|
||||
requireNotBlank("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId());
|
||||
requireFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId());
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> resolveStandardMqAllowed(String productType) {
|
||||
if ("ENTERPRISE".equals(productType)) {
|
||||
return MQ_ENTERPRISE;
|
||||
}
|
||||
if ("INDIRECT".equals(productType)) {
|
||||
return MQ_INDIRECT;
|
||||
}
|
||||
throw new IllegalArgumentException("unsupported standard productType: " + productType);
|
||||
}
|
||||
|
||||
private void validateDirect(InitPreviewRequest request, String deployMode, String mqType) {
|
||||
requireAllowed("deployMode", deployMode, DEPLOY_DIRECT);
|
||||
requireAllowed("mq.mqType", mqType, MQ_DIRECT);
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package com.cisd.tms.modules.sign.controller.internal;
|
||||
package com.cisd.tms.modules.sign.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.sign.dto.internal.InternalSignPreviewRequest;
|
||||
@@ -14,11 +14,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sign")
|
||||
public class InternalSignController {
|
||||
public class SignController {
|
||||
|
||||
private final SignService signService;
|
||||
|
||||
public InternalSignController(SignService signService) {
|
||||
public SignController(SignService signService) {
|
||||
this.signService = signService;
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/system")
|
||||
public class HealthInternalController {
|
||||
public class HealthController {
|
||||
|
||||
@GetMapping("/health")
|
||||
public ApiResponse<Map<String, Object>> status() {
|
||||
@@ -38,6 +38,12 @@ springdoc:
|
||||
operations-sorter: alpha
|
||||
|
||||
tms:
|
||||
file:
|
||||
storage:
|
||||
# 上传文件根目录;上传接口会将文件保存到 <upload-base-dir>/<fileId>/<originalFilename>。
|
||||
upload-base-dir: ${TMS_FILE_STORAGE_UPLOAD_BASE_DIR:/home/tms/uploads}
|
||||
# 上传文件大小上限(字节)。
|
||||
max-file-size-bytes: ${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:104857600}
|
||||
init:
|
||||
executor:
|
||||
# 初始化执行模式:SIMULATE(仅模拟,不执行外部命令) / LOCAL(本机执行脚本命令)。
|
||||
@@ -50,6 +56,38 @@ tms:
|
||||
standard-home-dir: ${TMS_INIT_STANDARD_HOME_DIR:/home/cmep4i}
|
||||
# 标准版 cpconfig.cfg 路径;默认取 standard-home-dir 下文件。
|
||||
standard-cpconfig-path: ${TMS_INIT_EXECUTOR_STANDARD_CPCONFIG_PATH:${tms.init.executor.standard-home-dir}/cpconfig.cfg}
|
||||
# 标准版数据库脚本基目录;默认取 standard-home-dir/cpackage,用于读取 sql_script 模板。
|
||||
standard-db-script-base-dir: ${TMS_INIT_EXECUTOR_STANDARD_DB_SCRIPT_BASE_DIR:${tms.init.executor.standard-home-dir}/cpackage}
|
||||
# 标准版数据库 SQL 加载目录;DB_APPLY 会将渲染后 SQL 复制到该目录。
|
||||
standard-db-load-dir: ${TMS_INIT_EXECUTOR_STANDARD_DB_LOAD_DIR:${tms.init.executor.standard-home-dir}/mysql/loadfilepath}
|
||||
# 标准版数据库导库用户名;默认复用应用数据源账号。
|
||||
standard-db-username: ${TMS_INIT_EXECUTOR_STANDARD_DB_USERNAME:root}
|
||||
# 标准版数据库导库密码;默认复用应用数据源密码。
|
||||
standard-db-password: ${TMS_INIT_EXECUTOR_STANDARD_DB_PASSWORD:Sunyard123}
|
||||
# 标准版数据库导入命令;LOCAL 模式下必须配置,否则 DB_APPLY 会直接失败。
|
||||
standard-db-apply-command: ${TMS_INIT_EXECUTOR_STANDARD_DB_APPLY_COMMAND:/home/tms/bin/apply_standard_db.sh}
|
||||
# 标准版数据库入口 SQL 文件名;与导库命令配合使用。
|
||||
standard-db-entry-sql-file-name: ${TMS_INIT_EXECUTOR_STANDARD_DB_ENTRY_SQL_FILE_NAME:CMEP.sql}
|
||||
# 标准版 nginx 模板路径;用于生成最终 nginx.conf。
|
||||
standard-nginx-template-path: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_TEMPLATE_PATH:${tms.init.executor.standard-home-dir}/cpackage/software/nginx.conf}
|
||||
# 标准版 nginx 目标配置路径。
|
||||
standard-nginx-target-path: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_TARGET_PATH:/usr/local/nginx/conf/nginx.conf}
|
||||
# 标准版 WEB 前端压缩包路径;默认对齐厂商 cpackage/front/front.zip。
|
||||
standard-web-front-zip-path: ${TMS_INIT_EXECUTOR_STANDARD_WEB_FRONT_ZIP_PATH:${tms.init.executor.standard-home-dir}/cpackage/front/front.zip}
|
||||
# 标准版 WEB 工作目录;用于解压前端产物。
|
||||
standard-web-work-dir: ${TMS_INIT_EXECUTOR_STANDARD_WEB_WORK_DIR:${tms.init.executor.standard-home-dir}/web}
|
||||
# 标准版 WEB 发布目录。
|
||||
standard-web-target-dir: ${TMS_INIT_EXECUTOR_STANDARD_WEB_TARGET_DIR:/usr/local/nginx/html}
|
||||
# 标准版 organization.json 输出路径;默认对齐厂商脚本写入位置。
|
||||
standard-web-organization-json-path: ${TMS_INIT_EXECUTOR_STANDARD_WEB_ORGANIZATION_JSON_PATH:${tms.init.executor.standard-web-target-dir}/organization.json}
|
||||
# 标准版 CMSP/CMTP 启动用户;start_standard_apps.sh 会读取该值。
|
||||
standard-run-user: ${TMS_INIT_EXECUTOR_STANDARD_RUN_USER:cmep4i}
|
||||
# 标准版 CMSP/CMTP 启动命令。
|
||||
standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh}
|
||||
# 标准版 nginx 启动命令。
|
||||
standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:/home/tms/bin/start_standard_nginx.sh}
|
||||
# 标准版启动后检查命令。
|
||||
standard-post-check-command: ${TMS_INIT_EXECUTOR_STANDARD_POST_CHECK_COMMAND:/home/tms/bin/check_standard_runtime.sh}
|
||||
# 直参版 cpconfig 文件名(按 upload-base-dir/<cfgZipFileId>/<fileName> 解析)。
|
||||
direct-cpconfig-file-name: ${TMS_INIT_EXECUTOR_DIRECT_CPCONFIG_FILE_NAME:cpconfig.cfg}
|
||||
# 上传文件根目录;所有 fileId 均从该目录解析。
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS tms_file_record (
|
||||
id BIGINT PRIMARY KEY,
|
||||
file_id VARCHAR(64) NOT NULL,
|
||||
original_filename VARCHAR(255) NOT NULL,
|
||||
storage_path VARCHAR(1024) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
UNIQUE KEY uk_tms_file_record_file_id (file_id)
|
||||
);
|
||||
@@ -6,20 +6,30 @@
|
||||
"SET_RABBITMQ": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setfraq.sh",
|
||||
"MQ_USER_APPLY": "internal::mq_user_apply",
|
||||
"MQ_QUEUE_APPLY": "internal::mq_queue_apply",
|
||||
"DB_INIT": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsql.sh",
|
||||
"DB_RENDER": "internal::db_render",
|
||||
"DB_APPLY": "internal::db_apply",
|
||||
"DEPLOY_RECEIVER": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsptp.sh",
|
||||
"APP_MQ_CREDENTIALS_APPLY": "internal::app_mq_credentials_apply",
|
||||
"NGINX_CONFIG_APPLY": "internal::nginx_config_apply",
|
||||
"WEB_CONFIG_APPLY": "internal::web_config_apply",
|
||||
"START_CMSP_CMTP": "internal::start_cmsp_cmtp",
|
||||
"START_NGINX": "internal::start_nginx",
|
||||
"POST_CHECK": "internal::post_check",
|
||||
"VERIFY": "internal::verify"
|
||||
},
|
||||
"TLQ": {
|
||||
"RENDER_CONFIG": "internal::render_config",
|
||||
"FILE_PREPARE": "internal::file_prepare",
|
||||
"SET_TLQ": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setftq.sh",
|
||||
"MQ_USER_APPLY": "internal::mq_user_apply",
|
||||
"MQ_QUEUE_APPLY": "internal::mq_queue_apply",
|
||||
"DB_INIT": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsql.sh",
|
||||
"TLQ_CONF_APPLY": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/settlq.sh",
|
||||
"DB_RENDER": "internal::db_render",
|
||||
"DB_APPLY": "internal::db_apply",
|
||||
"DEPLOY_RECEIVER": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsptp.sh",
|
||||
"APP_MQ_CREDENTIALS_APPLY": "internal::app_mq_credentials_apply",
|
||||
"NGINX_CONFIG_APPLY": "internal::nginx_config_apply",
|
||||
"WEB_CONFIG_APPLY": "internal::web_config_apply",
|
||||
"START_CMSP_CMTP": "internal::start_cmsp_cmtp",
|
||||
"START_NGINX": "internal::start_nginx",
|
||||
"POST_CHECK": "internal::post_check",
|
||||
"VERIFY": "internal::verify"
|
||||
}
|
||||
},
|
||||
@@ -30,20 +40,33 @@
|
||||
"SET_RABBITMQ": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setfraq.sh",
|
||||
"MQ_USER_APPLY": "internal::mq_user_apply",
|
||||
"MQ_QUEUE_APPLY": "internal::mq_queue_apply",
|
||||
"DB_INIT": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsql.sh",
|
||||
"DB_RENDER": "internal::db_render",
|
||||
"DB_APPLY": "internal::db_apply",
|
||||
"DEPLOY_RECEIVER": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsptp.sh",
|
||||
"APP_MQ_CREDENTIALS_APPLY": "internal::app_mq_credentials_apply",
|
||||
"NGINX_CONFIG_APPLY": "internal::nginx_config_apply",
|
||||
"WEB_CONFIG_APPLY": "internal::web_config_apply",
|
||||
"START_CMSP_CMTP": "internal::start_cmsp_cmtp",
|
||||
"START_NGINX": "internal::start_nginx",
|
||||
"POST_CHECK": "internal::post_check",
|
||||
"VERIFY": "internal::verify"
|
||||
},
|
||||
"TLQ": {
|
||||
"RABBITMQ_TLQ": {
|
||||
"RENDER_CONFIG": "internal::render_config",
|
||||
"FILE_PREPARE": "internal::file_prepare",
|
||||
"SET_TLQ": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setftq.sh",
|
||||
"TLQ_CONF_APPLY": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/settlq.sh",
|
||||
"MQ_USER_APPLY": "internal::mq_user_apply",
|
||||
"MQ_QUEUE_APPLY": "internal::mq_queue_apply",
|
||||
"DB_INIT": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsql.sh",
|
||||
"DB_RENDER": "internal::db_render",
|
||||
"DB_APPLY": "internal::db_apply",
|
||||
"DEPLOY_RECEIVER": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsptp.sh",
|
||||
"APP_MQ_CREDENTIALS_APPLY": "internal::app_mq_credentials_apply",
|
||||
"NGINX_CONFIG_APPLY": "internal::nginx_config_apply",
|
||||
"WEB_CONFIG_APPLY": "internal::web_config_apply",
|
||||
"START_CMSP_CMTP": "internal::start_cmsp_cmtp",
|
||||
"START_NGINX": "internal::start_nginx",
|
||||
"POST_CHECK": "internal::post_check",
|
||||
"VERIFY": "internal::verify"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.cisd.tms.modules.file.mapper.FileRecordMapper">
|
||||
|
||||
<resultMap id="FileRecordResultMap" type="com.cisd.tms.modules.file.entity.FileRecordEntity">
|
||||
<id property="id" column="id"/>
|
||||
<result property="fileId" column="file_id"/>
|
||||
<result property="originalFilename" column="original_filename"/>
|
||||
<result property="storagePath" column="storage_path"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByFileId" parameterType="string" resultMap="FileRecordResultMap">
|
||||
SELECT id, file_id, original_filename, storage_path, status, create_time, update_time
|
||||
FROM tms_file_record
|
||||
WHERE file_id = #{fileId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
+1
@@ -17,6 +17,7 @@ import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyWithIpkEccRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyWithIpkRsaRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.ImportKeyWithIskEccRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.ImportKeyWithIskRsaRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.RecoverKekRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.RsaExternalPrivateKeyOpRequest;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.cisd.tms.modules.file;
|
||||
|
||||
import com.cisd.tms.common.exception.GlobalExceptionHandler;
|
||||
import com.cisd.tms.modules.file.controller.FileController;
|
||||
import com.cisd.tms.modules.file.dto.FileDetailResponse;
|
||||
import com.cisd.tms.modules.file.dto.FileUploadResponse;
|
||||
import com.cisd.tms.modules.file.service.FileService;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class FileInternalControllerTest {
|
||||
|
||||
private static final Path OPENAPI_YAML = Path.of("docs/openapi/cisd-init.openapi.yaml");
|
||||
private static final Path OPENAPI_JSON = Path.of("docs/openapi/cisd-init.openapi.json");
|
||||
|
||||
@Test
|
||||
void shouldUploadFileViaInternalApi() throws Exception {
|
||||
FileService fileService = Mockito.mock(FileService.class);
|
||||
FileUploadResponse response = new FileUploadResponse();
|
||||
response.setFileId("FILE-20260310-000001");
|
||||
response.setSize(12L);
|
||||
response.setStoragePath("/home/tms/uploads/FILE-20260310-000001/license.dat");
|
||||
Mockito.when(fileService.upload(Mockito.any())).thenReturn(response);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new FileController(fileService))
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
MockMultipartFile file = new MockMultipartFile("file", "license.dat", MediaType.APPLICATION_OCTET_STREAM_VALUE, "hello-file".getBytes());
|
||||
|
||||
mockMvc.perform(multipart("/api/v1/files/upload").file(file))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.fileId").value("FILE-20260310-000001"))
|
||||
.andExpect(jsonPath("$.data.size").value(12))
|
||||
.andExpect(jsonPath("$.data.storagePath").value("/home/tms/uploads/FILE-20260310-000001/license.dat"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldQueryFileDetailViaInternalApi() throws Exception {
|
||||
FileService fileService = Mockito.mock(FileService.class);
|
||||
FileDetailResponse response = new FileDetailResponse();
|
||||
response.setFileId("FILE-20260310-000001");
|
||||
response.setOriginalFilename("license.dat");
|
||||
response.setStoragePath("/home/tms/uploads/FILE-20260310-000001/license.dat");
|
||||
response.setStatus("ACTIVE");
|
||||
response.setSize(12L);
|
||||
Mockito.when(fileService.getDetail("FILE-20260310-000001")).thenReturn(response);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new FileController(fileService))
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
|
||||
mockMvc.perform(get("/api/v1/files/FILE-20260310-000001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.fileId").value("FILE-20260310-000001"))
|
||||
.andExpect(jsonPath("$.data.originalFilename").value("license.dat"))
|
||||
.andExpect(jsonPath("$.data.storagePath").value("/home/tms/uploads/FILE-20260310-000001/license.dat"))
|
||||
.andExpect(jsonPath("$.data.status").value("ACTIVE"))
|
||||
.andExpect(jsonPath("$.data.size").value(12));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDocumentUploadApi() throws Exception {
|
||||
String yaml = Files.readString(OPENAPI_YAML);
|
||||
String json = Files.readString(OPENAPI_JSON);
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("/api/v1/files/upload:"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("/api/v1/files/{fileId}:"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("multipart/form-data:"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("ApiResponseFileUploadResponse"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("ApiResponseFileDetailResponse"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("fileId"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("先上传文件"));
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertTrue(json.contains("/api/v1/files/upload"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(json.contains("/api/v1/files/{fileId}"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(json.contains("multipart/form-data"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(json.contains("ApiResponseFileUploadResponse"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(json.contains("ApiResponseFileDetailResponse"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(json.contains("先上传文件"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.cisd.tms.modules.file;
|
||||
|
||||
import com.cisd.tms.common.config.properties.FileStorageProperties;
|
||||
import com.cisd.tms.modules.file.dto.FileDetailResponse;
|
||||
import com.cisd.tms.modules.file.dto.FileUploadResponse;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.file.service.FileService;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
class FileServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldPersistUploadedFileMetadata() {
|
||||
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
|
||||
String fileId = "FILE-20260310-000001";
|
||||
String originalFilename = "receiver-license.dat";
|
||||
String storagePath = "/home/tms/uploads/" + fileId + "/" + originalFilename;
|
||||
|
||||
FileRecordEntity entity = new FileRecordEntity();
|
||||
entity.setFileId(fileId);
|
||||
entity.setOriginalFilename(originalFilename);
|
||||
entity.setStoragePath(storagePath);
|
||||
entity.setStatus("ACTIVE");
|
||||
|
||||
repository.save(entity);
|
||||
FileRecordEntity persisted = repository.findByFileId(fileId).orElseThrow();
|
||||
|
||||
Assertions.assertEquals(fileId, persisted.getFileId());
|
||||
Assertions.assertEquals(originalFilename, persisted.getOriginalFilename());
|
||||
Assertions.assertEquals(storagePath, persisted.getStoragePath());
|
||||
Assertions.assertEquals("ACTIVE", persisted.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveUploadedFileUnderFileIdDirectory() throws IOException {
|
||||
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-test");
|
||||
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
|
||||
FileService service = new FileService(repository, properties(uploadBaseDir, 1024));
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"receiver-license.dat",
|
||||
"application/octet-stream",
|
||||
"license-content".getBytes()
|
||||
);
|
||||
|
||||
FileUploadResponse response = service.upload(file);
|
||||
FileRecordEntity persisted = repository.findByFileId(response.getFileId()).orElseThrow();
|
||||
|
||||
Assertions.assertNotNull(response.getFileId());
|
||||
Assertions.assertEquals(file.getSize(), response.getSize());
|
||||
Assertions.assertTrue(response.getStoragePath().endsWith("/" + response.getFileId() + "/receiver-license.dat"));
|
||||
Assertions.assertTrue(Files.exists(Path.of(response.getStoragePath())));
|
||||
Assertions.assertEquals("ACTIVE", persisted.getStatus());
|
||||
Assertions.assertEquals(response.getStoragePath(), persisted.getStoragePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBlankFileUpload() throws IOException {
|
||||
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-empty-test");
|
||||
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024));
|
||||
MockMultipartFile file = new MockMultipartFile("file", "empty.txt", "text/plain", new byte[0]);
|
||||
|
||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
|
||||
|
||||
Assertions.assertTrue(ex.getMessage().contains("empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectOversizeUpload() throws IOException {
|
||||
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-oversize-test");
|
||||
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 4));
|
||||
MockMultipartFile file = new MockMultipartFile("file", "big.txt", "text/plain", "12345".getBytes());
|
||||
|
||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
|
||||
|
||||
Assertions.assertTrue(ex.getMessage().contains("size"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnStoredFileDetailByFileId() throws IOException {
|
||||
Path uploadBaseDir = Files.createTempDirectory("file-service-detail-test");
|
||||
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024));
|
||||
MockMultipartFile file = new MockMultipartFile("file", "front.zip", "application/zip", "zip-data".getBytes());
|
||||
|
||||
FileUploadResponse uploaded = service.upload(file);
|
||||
FileDetailResponse detail = service.getDetail(uploaded.getFileId());
|
||||
|
||||
Assertions.assertEquals(uploaded.getFileId(), detail.getFileId());
|
||||
Assertions.assertEquals(uploaded.getSize(), detail.getSize());
|
||||
Assertions.assertEquals(uploaded.getStoragePath(), detail.getStoragePath());
|
||||
Assertions.assertEquals("front.zip", detail.getOriginalFilename());
|
||||
Assertions.assertEquals("ACTIVE", detail.getStatus());
|
||||
}
|
||||
|
||||
private static FileStorageProperties properties(Path uploadBaseDir, long maxFileSizeBytes) {
|
||||
FileStorageProperties properties = new FileStorageProperties();
|
||||
properties.setUploadBaseDir(uploadBaseDir.toString());
|
||||
properties.setMaxFileSizeBytes(maxFileSizeBytes);
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static class InMemoryFileRecordRepository implements FileRecordRepository {
|
||||
|
||||
private final Map<String, FileRecordEntity> data = new LinkedHashMap<>();
|
||||
|
||||
@Override
|
||||
public Optional<FileRecordEntity> findByFileId(String fileId) {
|
||||
return Optional.ofNullable(data.get(fileId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(FileRecordEntity entity) {
|
||||
data.put(entity.getFileId(), entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
+661
@@ -7,10 +7,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@@ -375,6 +378,557 @@ class ConfigurableInitStepExecutorTest {
|
||||
Assertions.assertTrue(Files.readString(cmtpCfg).contains("spring.rabbitmq.password=" + encryptedPass));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderStandardDatabaseSqlPackageForEnterpriseRabbitmq() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-db-render-test");
|
||||
Path standardHome = prepareStandardSqlScriptBase(tempRoot);
|
||||
Path cpconfig = standardHome.resolve("cpconfig.cfg");
|
||||
Files.writeString(cpconfig, """
|
||||
MY_CIPSID_OR_BIC=OLD
|
||||
mq_01_ip=127.0.0.1
|
||||
mq_02_ip=127.0.0.2
|
||||
sptp_01app_ip=127.0.0.3
|
||||
sptp_02app_ip=127.0.0.4
|
||||
sptp_01db_ip=127.0.0.5
|
||||
MY_BIC_CODE=OLD
|
||||
MY_CIPS_ID=
|
||||
MY_LEI_CODE=
|
||||
""");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.SIMULATE);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
|
||||
properties.setStandardHomeDir(standardHome.toString());
|
||||
properties.setStandardCpconfigPath(cpconfig.toString());
|
||||
properties.setStandardDbScriptBaseDir(standardHome.resolve("cpackage").toString());
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = new InitTaskEntity();
|
||||
task.setTaskId("TASK-STANDARD-DB-RENDER-1");
|
||||
task.setProductType("ENTERPRISE");
|
||||
task.setInitPlanJson(buildInitPlanJson(
|
||||
"ENTERPRISE",
|
||||
Map.of(
|
||||
"orgCodeType", "BIC",
|
||||
"orgCode", "AAAABBBBXXX",
|
||||
"orgNameCn", "测试机构",
|
||||
"deployMode", "DUAL",
|
||||
"nodes", Map.of("node01Ip", "10.0.0.1", "node02Ip", "10.0.0.2"),
|
||||
"mq", Map.of("mqType", "RABBITMQ", "channelUsername", "bank_user", "channelPassword", "bank_pass"),
|
||||
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
|
||||
)
|
||||
));
|
||||
|
||||
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
|
||||
renderConfigStep.setStepNo(1);
|
||||
renderConfigStep.setStepCode("RENDER_CONFIG");
|
||||
renderConfigStep.setCommandLine("internal::render_config");
|
||||
InitStepExecutionResult renderConfig = executor.execute(task, renderConfigStep);
|
||||
Assertions.assertTrue(renderConfig.isSuccess());
|
||||
|
||||
InitTaskStepEntity dbRenderStep = new InitTaskStepEntity();
|
||||
dbRenderStep.setStepNo(2);
|
||||
dbRenderStep.setStepCode("DB_RENDER");
|
||||
dbRenderStep.setCommandLine("internal::db_render");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, dbRenderStep);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Path sqlDir = tempRoot.resolve("staging").resolve("TASK-STANDARD-DB-RENDER-1").resolve("db").resolve("sql");
|
||||
Path cmtpSql = sqlDir.resolve("CMTP-01.sql");
|
||||
Path cmepDmlSql = sqlDir.resolve("CMEP-DML.sql");
|
||||
Assertions.assertTrue(Files.exists(cmtpSql));
|
||||
Assertions.assertTrue(Files.exists(cmepDmlSql));
|
||||
Assertions.assertTrue(Files.exists(sqlDir.resolve("CMEP.sql")));
|
||||
Assertions.assertTrue(Files.exists(sqlDir.resolve("SCHEMA-DDL.sql")));
|
||||
Assertions.assertTrue(Files.exists(sqlDir.resolve("UP-ORG-INFO.sql")));
|
||||
String cmtpContent = Files.readString(cmtpSql);
|
||||
String cmepDmlContent = Files.readString(cmepDmlSql);
|
||||
Assertions.assertTrue(cmtpContent.contains("CI01_AAAABBBBXXX"));
|
||||
Assertions.assertTrue(cmtpContent.contains("10.0.0.1"));
|
||||
Assertions.assertTrue(cmtpContent.contains("10.0.0.2"));
|
||||
Assertions.assertFalse(cmtpContent.contains("01tlq.ip"));
|
||||
Assertions.assertFalse(cmtpContent.contains("sptp.01app.ip"));
|
||||
Assertions.assertTrue(cmepDmlContent.contains("AAAABBBBXXX"));
|
||||
Assertions.assertTrue(cmepDmlContent.contains("mybiccode") || cmepDmlContent.contains("AAAABBBBXXX"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveStandardCpconfigFromConfiguredStandardHomeDir() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-home-cpconfig-test");
|
||||
Path standardHome = tempRoot.resolve("custom-standard-home");
|
||||
Files.createDirectories(standardHome);
|
||||
Path cpconfig = standardHome.resolve("cpconfig.cfg");
|
||||
Files.writeString(cpconfig, """
|
||||
MY_CIPSID_OR_BIC=OLD
|
||||
mq_01_ip=127.0.0.1
|
||||
mq_02_ip=127.0.0.2
|
||||
sptp_01app_ip=127.0.0.3
|
||||
sptp_02app_ip=127.0.0.4
|
||||
sptp_01db_ip=127.0.0.5
|
||||
MY_BIC_CODE=OLD
|
||||
MY_CIPS_ID=
|
||||
MY_LEI_CODE=
|
||||
""");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.SIMULATE);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStandardHomeDir(standardHome.toString());
|
||||
properties.setStandardCpconfigPath("");
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-HOME-CPCONFIG-1", "RABBITMQ", "SINGLE");
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("RENDER_CONFIG");
|
||||
step.setCommandLine("internal::render_config");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertTrue(Files.readString(cpconfig).contains("MY_CIPSID_OR_BIC=AAAABBBBXXX"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyStandardDatabaseSqlPackageToLoadDir() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-db-apply-test");
|
||||
Path standardHome = prepareStandardSqlScriptBase(tempRoot);
|
||||
Path cpconfig = standardHome.resolve("cpconfig.cfg");
|
||||
Files.writeString(cpconfig, """
|
||||
MY_CIPSID_OR_BIC=OLD
|
||||
mq_01_ip=127.0.0.1
|
||||
mq_02_ip=127.0.0.2
|
||||
sptp_01app_ip=127.0.0.3
|
||||
sptp_02app_ip=127.0.0.4
|
||||
sptp_01db_ip=127.0.0.5
|
||||
MY_BIC_CODE=OLD
|
||||
MY_CIPS_ID=
|
||||
MY_LEI_CODE=
|
||||
""");
|
||||
Path loadDir = tempRoot.resolve("loadfilepath");
|
||||
Path marker = tempRoot.resolve("db-apply.done");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
|
||||
properties.setStandardHomeDir(standardHome.toString());
|
||||
properties.setStandardCpconfigPath(cpconfig.toString());
|
||||
properties.setStandardDbScriptBaseDir(standardHome.resolve("cpackage").toString());
|
||||
properties.setStandardDbLoadDir(loadDir.toString());
|
||||
properties.setStandardDbApplyCommand("test -f CMEP.sql && echo applied > '" + marker + "'");
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = new InitTaskEntity();
|
||||
task.setTaskId("TASK-STANDARD-DB-APPLY-1");
|
||||
task.setProductType("ENTERPRISE");
|
||||
task.setInitPlanJson(buildInitPlanJson(
|
||||
"ENTERPRISE",
|
||||
Map.of(
|
||||
"orgCodeType", "BIC",
|
||||
"orgCode", "AAAABBBBXXX",
|
||||
"orgNameCn", "测试机构",
|
||||
"deployMode", "DUAL",
|
||||
"nodes", Map.of("node01Ip", "10.0.0.1", "node02Ip", "10.0.0.2"),
|
||||
"mq", Map.of("mqType", "RABBITMQ", "channelUsername", "bank_user", "channelPassword", "bank_pass"),
|
||||
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
|
||||
)
|
||||
));
|
||||
|
||||
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
|
||||
renderConfigStep.setStepNo(1);
|
||||
renderConfigStep.setStepCode("RENDER_CONFIG");
|
||||
renderConfigStep.setCommandLine("internal::render_config");
|
||||
Assertions.assertTrue(executor.execute(task, renderConfigStep).isSuccess());
|
||||
|
||||
InitTaskStepEntity dbRenderStep = new InitTaskStepEntity();
|
||||
dbRenderStep.setStepNo(2);
|
||||
dbRenderStep.setStepCode("DB_RENDER");
|
||||
dbRenderStep.setCommandLine("internal::db_render");
|
||||
Assertions.assertTrue(executor.execute(task, dbRenderStep).isSuccess());
|
||||
|
||||
InitTaskStepEntity dbApplyStep = new InitTaskStepEntity();
|
||||
dbApplyStep.setStepNo(3);
|
||||
dbApplyStep.setStepCode("DB_APPLY");
|
||||
dbApplyStep.setCommandLine("internal::db_apply");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, dbApplyStep);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertTrue(Files.exists(loadDir.resolve("CMTP-01.sql")));
|
||||
Assertions.assertTrue(Files.exists(loadDir.resolve("CMEP-DML.sql")));
|
||||
Assertions.assertTrue(Files.exists(loadDir.resolve("CMEP.sql")));
|
||||
Assertions.assertTrue(Files.exists(marker));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailDbApplyWhenStandardDbApplyCommandMissingInLocalMode() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-db-apply-missing-command-test");
|
||||
Path standardHome = prepareStandardSqlScriptBase(tempRoot);
|
||||
Path cpconfig = standardHome.resolve("cpconfig.cfg");
|
||||
Files.writeString(cpconfig, """
|
||||
MY_CIPSID_OR_BIC=OLD
|
||||
mq_01_ip=127.0.0.1
|
||||
mq_02_ip=127.0.0.2
|
||||
sptp_01app_ip=127.0.0.3
|
||||
sptp_02app_ip=127.0.0.4
|
||||
sptp_01db_ip=127.0.0.5
|
||||
MY_BIC_CODE=OLD
|
||||
MY_CIPS_ID=
|
||||
MY_LEI_CODE=
|
||||
""");
|
||||
Path loadDir = tempRoot.resolve("loadfilepath");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
|
||||
properties.setStandardHomeDir(standardHome.toString());
|
||||
properties.setStandardCpconfigPath(cpconfig.toString());
|
||||
properties.setStandardDbScriptBaseDir(standardHome.resolve("cpackage").toString());
|
||||
properties.setStandardDbLoadDir(loadDir.toString());
|
||||
properties.setStandardDbApplyCommand("");
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-DB-APPLY-MISSING-CMD-1", "RABBITMQ", "DUAL");
|
||||
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
|
||||
renderConfigStep.setStepNo(1);
|
||||
renderConfigStep.setStepCode("RENDER_CONFIG");
|
||||
renderConfigStep.setCommandLine("internal::render_config");
|
||||
Assertions.assertTrue(executor.execute(task, renderConfigStep).isSuccess());
|
||||
|
||||
InitTaskStepEntity dbRenderStep = new InitTaskStepEntity();
|
||||
dbRenderStep.setStepNo(2);
|
||||
dbRenderStep.setStepCode("DB_RENDER");
|
||||
dbRenderStep.setCommandLine("internal::db_render");
|
||||
Assertions.assertTrue(executor.execute(task, dbRenderStep).isSuccess());
|
||||
|
||||
InitTaskStepEntity dbApplyStep = new InitTaskStepEntity();
|
||||
dbApplyStep.setStepNo(3);
|
||||
dbApplyStep.setStepCode("DB_APPLY");
|
||||
dbApplyStep.setCommandLine("internal::db_apply");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, dbApplyStep);
|
||||
|
||||
Assertions.assertFalse(result.isSuccess());
|
||||
Assertions.assertTrue(result.getMessage().contains("apply command is blank"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInjectDatabaseCredentialsWhenExecutingStandardDbApplyCommand() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-db-apply-env-test");
|
||||
Path standardHome = prepareStandardSqlScriptBase(tempRoot);
|
||||
Path cpconfig = standardHome.resolve("cpconfig.cfg");
|
||||
Files.writeString(cpconfig, """
|
||||
MY_CIPSID_OR_BIC=OLD
|
||||
mq_01_ip=127.0.0.1
|
||||
mq_02_ip=127.0.0.2
|
||||
sptp_01app_ip=127.0.0.3
|
||||
sptp_02app_ip=127.0.0.4
|
||||
sptp_01db_ip=127.0.0.5
|
||||
MY_BIC_CODE=OLD
|
||||
MY_CIPS_ID=
|
||||
MY_LEI_CODE=
|
||||
""");
|
||||
Path loadDir = tempRoot.resolve("loadfilepath");
|
||||
Path marker = tempRoot.resolve("db-apply-env.done");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
|
||||
properties.setStandardHomeDir(standardHome.toString());
|
||||
properties.setStandardCpconfigPath(cpconfig.toString());
|
||||
properties.setStandardDbScriptBaseDir(standardHome.resolve("cpackage").toString());
|
||||
properties.setStandardDbLoadDir(loadDir.toString());
|
||||
properties.setStandardDbUsername("db_user");
|
||||
properties.setStandardDbPassword("db_password");
|
||||
properties.setStandardDbApplyCommand(
|
||||
"test \"$DB_USER\" = \"db_user\""
|
||||
+ " && test \"$DB_PASSWORD\" = \"db_password\""
|
||||
+ " && test \"$LOAD_DIR\" = '" + loadDir.toAbsolutePath() + "'"
|
||||
+ " && echo applied > '" + marker + "'"
|
||||
);
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-DB-APPLY-ENV-1", "RABBITMQ", "DUAL");
|
||||
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
|
||||
renderConfigStep.setStepNo(1);
|
||||
renderConfigStep.setStepCode("RENDER_CONFIG");
|
||||
renderConfigStep.setCommandLine("internal::render_config");
|
||||
Assertions.assertTrue(executor.execute(task, renderConfigStep).isSuccess());
|
||||
|
||||
InitTaskStepEntity dbRenderStep = new InitTaskStepEntity();
|
||||
dbRenderStep.setStepNo(2);
|
||||
dbRenderStep.setStepCode("DB_RENDER");
|
||||
dbRenderStep.setCommandLine("internal::db_render");
|
||||
Assertions.assertTrue(executor.execute(task, dbRenderStep).isSuccess());
|
||||
|
||||
InitTaskStepEntity dbApplyStep = new InitTaskStepEntity();
|
||||
dbApplyStep.setStepNo(3);
|
||||
dbApplyStep.setStepCode("DB_APPLY");
|
||||
dbApplyStep.setCommandLine("internal::db_apply");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, dbApplyStep);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertTrue(Files.exists(marker));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyStandardNginxConfigFromTemplate() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-nginx-config-test");
|
||||
Path template = tempRoot.resolve("nginx.conf.template");
|
||||
Path target = tempRoot.resolve("target/nginx.conf");
|
||||
Files.writeString(template, "upstream a { server sptp.01app.ip; }\nupstream b { server sptp.02app.ip; }\n");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
|
||||
properties.setStandardNginxTemplatePath(template.toString());
|
||||
properties.setStandardNginxTargetPath(target.toString());
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-NGINX-1", "RABBITMQ", "DUAL");
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("NGINX_CONFIG_APPLY");
|
||||
step.setCommandLine("internal::nginx_config_apply");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Path staged = tempRoot.resolve("staging").resolve("TASK-STANDARD-NGINX-1").resolve("nginx").resolve("nginx.conf");
|
||||
Assertions.assertTrue(Files.exists(staged));
|
||||
Assertions.assertTrue(Files.exists(target));
|
||||
String rendered = Files.readString(staged);
|
||||
Assertions.assertTrue(rendered.contains("10.0.0.1"));
|
||||
Assertions.assertTrue(rendered.contains("10.0.0.2"));
|
||||
Assertions.assertFalse(rendered.contains("sptp.01app.ip"));
|
||||
Assertions.assertFalse(rendered.contains("sptp.02app.ip"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPublishStandardWebAssetsAndOrganizationJson() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-web-config-test");
|
||||
Path frontZip = tempRoot.resolve("front.zip");
|
||||
Path workDir = tempRoot.resolve("web-work");
|
||||
Path targetDir = tempRoot.resolve("web-target");
|
||||
Path organizationJson = tempRoot.resolve("config/organization.json");
|
||||
writeZip(frontZip, Map.of("index.html", "<html>ok</html>", "assets/app.js", "console.log('ok');"));
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
|
||||
properties.setStandardWebFrontZipPath(frontZip.toString());
|
||||
properties.setStandardWebWorkDir(workDir.toString());
|
||||
properties.setStandardWebTargetDir(targetDir.toString());
|
||||
properties.setStandardWebOrganizationJsonPath(organizationJson.toString());
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-WEB-1", "RABBITMQ", "SINGLE");
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("WEB_CONFIG_APPLY");
|
||||
step.setCommandLine("internal::web_config_apply");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertTrue(Files.exists(targetDir.resolve("index.html")));
|
||||
Assertions.assertTrue(Files.exists(targetDir.resolve("assets/app.js")));
|
||||
Assertions.assertTrue(Files.exists(organizationJson));
|
||||
Assertions.assertTrue(Files.readString(organizationJson).contains("测试机构"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStartStandardCmspCmtpWithConfiguredCommand() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-app-start-test");
|
||||
Path marker = tempRoot.resolve("cmsp-cmtp.started");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStandardHomeDir("/home/custom-standard");
|
||||
properties.setStandardRunUser("custom_user");
|
||||
properties.setStandardAppStartCommand(
|
||||
"test \"$STANDARD_HOME_DIR\" = \"/home/custom-standard\""
|
||||
+ " && test \"$STANDARD_RUN_USER\" = \"custom_user\""
|
||||
+ " && echo started > '" + marker + "'"
|
||||
);
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-APP-START-1", "RABBITMQ", "SINGLE");
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("START_CMSP_CMTP");
|
||||
step.setCommandLine("internal::start_cmsp_cmtp");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertTrue(Files.exists(marker));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStartStandardNginxWithConfiguredCommand() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-nginx-start-test");
|
||||
Path marker = tempRoot.resolve("nginx.started");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStandardNginxStartCommand("echo started > '" + marker + "'");
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-NGINX-START-1", "RABBITMQ", "SINGLE");
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("START_NGINX");
|
||||
step.setCommandLine("internal::start_nginx");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertTrue(Files.exists(marker));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunStandardPostCheck() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-post-check-test");
|
||||
Path marker = tempRoot.resolve("post-check.done");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStandardPostCheckCommand("echo checked > '" + marker + "'");
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-POST-CHECK-1", "RABBITMQ", "SINGLE");
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("POST_CHECK");
|
||||
step.setCommandLine("internal::post_check");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertTrue(Files.exists(marker));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeStandardInitScriptTemplates() throws IOException {
|
||||
Path repoRoot = Paths.get("").toAbsolutePath().normalize();
|
||||
Path scriptDir = repoRoot.resolve("scripts/standard-init");
|
||||
Path dbScript = scriptDir.resolve("apply_standard_db.sh");
|
||||
Path appScript = scriptDir.resolve("start_standard_apps.sh");
|
||||
Path nginxScript = scriptDir.resolve("start_standard_nginx.sh");
|
||||
Path checkScript = scriptDir.resolve("check_standard_runtime.sh");
|
||||
|
||||
Assertions.assertTrue(Files.exists(dbScript));
|
||||
Assertions.assertTrue(Files.exists(appScript));
|
||||
Assertions.assertTrue(Files.exists(nginxScript));
|
||||
Assertions.assertTrue(Files.exists(checkScript));
|
||||
|
||||
String dbContent = Files.readString(dbScript);
|
||||
Assertions.assertTrue(dbContent.contains("SCHEMA-DDL.sql"));
|
||||
Assertions.assertTrue(dbContent.contains("CMEP.sql"));
|
||||
Assertions.assertTrue(dbContent.contains("UP-ORG-INFO.sql"));
|
||||
Assertions.assertTrue(dbContent.contains("LOAD_DIR"));
|
||||
Assertions.assertTrue(dbContent.contains("--force"));
|
||||
Assertions.assertFalse(dbContent.contains("/home/cemp4i/mysql/loadfilepath"));
|
||||
|
||||
String appContent = Files.readString(appScript);
|
||||
Assertions.assertTrue(appContent.contains("STANDARD_HOME_DIR"));
|
||||
Assertions.assertTrue(appContent.contains("STANDARD_RUN_USER"));
|
||||
Assertions.assertTrue(appContent.contains("start_cmsp.sh"));
|
||||
Assertions.assertTrue(appContent.contains("start_cmtp.sh"));
|
||||
Assertions.assertFalse(appContent.contains("/home/cemp4i/cmsp/start_cmsp.sh"));
|
||||
Assertions.assertFalse(appContent.contains("su - cmep4i"));
|
||||
|
||||
String nginxContent = Files.readString(nginxScript);
|
||||
Assertions.assertTrue(nginxContent.contains("/usr/local/nginx/sbin/nginx"));
|
||||
|
||||
String checkContent = Files.readString(checkScript);
|
||||
Assertions.assertTrue(checkContent.contains("CMEP-CMSP"));
|
||||
Assertions.assertTrue(checkContent.contains("CMEP-CMTP"));
|
||||
Assertions.assertTrue(checkContent.contains("organization.json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDocumentStandardInitScriptTemplateDeployment() throws IOException {
|
||||
Path repoRoot = Paths.get("").toAbsolutePath().normalize();
|
||||
Path exampleConfig = repoRoot.resolve("config/application.yml.example");
|
||||
Path applicationConfig = repoRoot.resolve("src/main/resources/application.yml");
|
||||
Path readme = repoRoot.resolve("README.md");
|
||||
|
||||
String exampleContent = Files.readString(exampleConfig);
|
||||
Assertions.assertTrue(exampleContent.contains("standard-db-apply-command: /home/tms/bin/apply_standard_db.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-run-user: cmep4i"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-app-start-command: /home/tms/bin/start_standard_apps.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-nginx-start-command: /home/tms/bin/start_standard_nginx.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-post-check-command: /home/tms/bin/check_standard_runtime.sh"));
|
||||
|
||||
String applicationContent = Files.readString(applicationConfig);
|
||||
Assertions.assertTrue(applicationContent.contains("standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:/home/tms/bin/start_standard_nginx.sh}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-post-check-command: ${TMS_INIT_EXECUTOR_STANDARD_POST_CHECK_COMMAND:/home/tms/bin/check_standard_runtime.sh}"));
|
||||
|
||||
String readmeContent = Files.readString(readme);
|
||||
Assertions.assertTrue(readmeContent.contains("scripts/standard-init/"));
|
||||
Assertions.assertTrue(readmeContent.contains("/home/tms/bin"));
|
||||
Assertions.assertTrue(readmeContent.contains("apply_standard_db.sh"));
|
||||
Assertions.assertTrue(readmeContent.contains("Default module layout: `controller + service + repository + entity + dto`."));
|
||||
Assertions.assertTrue(readmeContent.contains("`/api/**` controllers live under `controller`"));
|
||||
Assertions.assertFalse(readmeContent.contains("Default module layout: `controller/internal + service + repository + entity + dto`."));
|
||||
Assertions.assertFalse(readmeContent.contains("Internal API controllers only in `*/controller/internal`, path prefix `/api/**`."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipStandardAppMqCredentialsForEnterpriseTlq() throws Exception {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-standard-rabbit-cred-skip-test");
|
||||
Path cmspDir = tempRoot.resolve("cmsp");
|
||||
Files.createDirectories(cmspDir);
|
||||
Path cmspCfg = cmspDir.resolve("application-prd-ci01.properties");
|
||||
Files.writeString(cmspCfg, "spring.rabbitmq.username=OLD\nspring.rabbitmq.password=OLD\n");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStandardRabbitAppConfigPatterns(List.of(cmspDir.resolve("application-prd*.properties").toString()));
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = new InitTaskEntity();
|
||||
task.setTaskId("TASK-APP-CRED-SKIP-1");
|
||||
task.setProductType("ENTERPRISE");
|
||||
task.setInitPlanJson(buildInitPlanJson(
|
||||
"ENTERPRISE",
|
||||
Map.of(
|
||||
"orgCodeType", "BIC",
|
||||
"orgCode", "AAAABBBBXXX",
|
||||
"orgNameCn", "测试机构",
|
||||
"deployMode", "SINGLE",
|
||||
"nodes", Map.of("node01Ip", "10.0.0.1"),
|
||||
"mq", Map.of("mqType", "TLQ", "channelUsername", "new_user", "channelPassword", "new_pass", "tlqLicenseFileId", "tlq-001"),
|
||||
"licenses", Map.of("receiverLicenseFileId", "file-receiver-001")
|
||||
)
|
||||
));
|
||||
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("APP_MQ_CREDENTIALS_APPLY");
|
||||
step.setCommandLine("internal::app_mq_credentials_apply");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertTrue(result.getMessage().contains("skipped"));
|
||||
Assertions.assertEquals("spring.rabbitmq.username=OLD\nspring.rabbitmq.password=OLD\n", Files.readString(cmspCfg));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRewriteAndExecuteStandardRabbitSetupScript() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-rabbit-setup-script-test");
|
||||
@@ -445,6 +999,65 @@ class ConfigurableInitStepExecutorTest {
|
||||
Assertions.assertTrue(stagedScriptContent.contains("rabbitmqctl set_permissions -p /RQ 'bank_user' '.*' '.*' '.*'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteStandardRabbitSetupScriptForIndirectRabbitmqTlq() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-rabbit-setup-indirect-tlq-test");
|
||||
Path setupScript = tempRoot.resolve("setuprabq.sh");
|
||||
Path queueScript = tempRoot.resolve("addrabq_R_01.sh");
|
||||
Path cpconfig = tempRoot.resolve("cpconfig.cfg");
|
||||
Path calls = tempRoot.resolve("calls.log");
|
||||
Files.writeString(cpconfig, "MY_CIPSID_OR_BIC=AAAABBBBXXX\n");
|
||||
Files.writeString(
|
||||
setupScript,
|
||||
"#!/bin/bash\n"
|
||||
+ "source /home/cmep4i/cpconfig.cfg\n"
|
||||
+ "cd /home/cmep4i/cpackage/rabq\n"
|
||||
+ "rabbitmqctl(){ echo \"$*\" >> \"" + calls + "\"; return 0; }\n"
|
||||
+ "rabbitmqctl add_user rConn rConn\n"
|
||||
+ "rabbitmqctl set_permissions -p /RQ rConn '.*' '.*' '.*'\n"
|
||||
);
|
||||
Files.writeString(queueScript, "#!/bin/bash\necho queue > /dev/null\n");
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
|
||||
properties.setStandardCpconfigPath(cpconfig.toString());
|
||||
properties.setStandardRabbitSetupScriptPath(setupScript.toString());
|
||||
properties.setStandardRabbitQueueScriptPath(queueScript.toString());
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = new InitTaskEntity();
|
||||
task.setTaskId("TASK-RABBIT-SETUP-INDIRECT-TLQ-1");
|
||||
task.setProductType("INDIRECT");
|
||||
task.setInitPlanJson(buildInitPlanJson(
|
||||
"INDIRECT",
|
||||
Map.of(
|
||||
"orgCodeType", "BIC",
|
||||
"orgCode", "AAAABBBBXXX",
|
||||
"orgNameCn", "测试机构",
|
||||
"deployMode", "SINGLE",
|
||||
"nodes", Map.of("node01Ip", "10.0.0.1"),
|
||||
"mq", Map.of("mqType", "RABBITMQ_TLQ", "channelUsername", "bank_user", "channelPassword", "bank_pass", "tlqLicenseFileId", "tlq-001"),
|
||||
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
|
||||
)
|
||||
));
|
||||
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("MQ_USER_APPLY");
|
||||
step.setCommandLine("internal::mq_user_apply");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
Assertions.assertFalse(result.getMessage().contains("skipped"));
|
||||
Assertions.assertTrue(Files.exists(tempRoot.resolve("staging")
|
||||
.resolve("TASK-RABBIT-SETUP-INDIRECT-TLQ-1")
|
||||
.resolve("scripts")
|
||||
.resolve("rabq")
|
||||
.resolve("setuprabq.sh")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteStandardRabbitQueueScript() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-rabbit-queue-script-test");
|
||||
@@ -546,6 +1159,54 @@ class ConfigurableInitStepExecutorTest {
|
||||
return new ObjectMapper().writeValueAsString(root);
|
||||
}
|
||||
|
||||
private static InitTaskEntity buildStandardEnterpriseTask(String taskId, String mqType, String deployMode) throws IOException {
|
||||
InitTaskEntity task = new InitTaskEntity();
|
||||
task.setTaskId(taskId);
|
||||
task.setProductType("ENTERPRISE");
|
||||
task.setInitPlanJson(buildInitPlanJson(
|
||||
"ENTERPRISE",
|
||||
Map.of(
|
||||
"orgCodeType", "BIC",
|
||||
"orgCode", "AAAABBBBXXX",
|
||||
"orgNameCn", "测试机构",
|
||||
"deployMode", deployMode,
|
||||
"nodes", "DUAL".equals(deployMode)
|
||||
? Map.of("node01Ip", "10.0.0.1", "node02Ip", "10.0.0.2")
|
||||
: Map.of("node01Ip", "10.0.0.1"),
|
||||
"mq", Map.of("mqType", mqType, "channelUsername", "bank_user", "channelPassword", "bank_pass"),
|
||||
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
|
||||
)
|
||||
));
|
||||
return task;
|
||||
}
|
||||
|
||||
private static Path prepareStandardSqlScriptBase(Path tempRoot) throws IOException {
|
||||
Path standardHome = tempRoot.resolve("standard-home");
|
||||
Path sqlBase = standardHome.resolve("cpackage").resolve("sql_script");
|
||||
Path sqlTmp = sqlBase.resolve("sql_script_tmp");
|
||||
Files.createDirectories(sqlTmp);
|
||||
Files.writeString(sqlBase.resolve("CMEP-DML.sql"), "INSERT INTO TEST VALUES ('AAAABBBBXXX', 'mybiccode', 'mycipsid', 'myleicode');\n");
|
||||
Files.writeString(sqlBase.resolve("CMEP.sql"), "source /home/cmep4i/mysql/loadfilepath/CMEP-DML.sql;\nsource /home/cmep4i/mysql/loadfilepath/CMTP-01.sql;\n");
|
||||
Files.writeString(sqlBase.resolve("SCHEMA-DDL.sql"), "CREATE TABLE TEST(ID INT);\n");
|
||||
Files.writeString(sqlBase.resolve("UP-ORG-INFO.sql"), "UPDATE ORG SET CODE='AAAABBBBXXX';\n");
|
||||
Files.writeString(sqlTmp.resolve("CMTP-01-raq.sql"), "INSERT INTO X VALUES('CI01_AAAABBBBXXX','AAAABBBBXXX','01tlq.ip','02tlq.ip','sptp.01app.ip','sptp.02app.ip');\n");
|
||||
Files.writeString(sqlTmp.resolve("CMTP-01-tlq.sql"), "INSERT INTO X VALUES('CI01_AAAABBBBXXX','AAAABBBBXXX','01tlq.ip','02tlq.ip','sptp.01app.ip','sptp.02app.ip');\n");
|
||||
return standardHome;
|
||||
}
|
||||
|
||||
private static void writeZip(Path zipPath, Map<String, String> entries) throws IOException {
|
||||
if (zipPath.getParent() != null) {
|
||||
Files.createDirectories(zipPath.getParent());
|
||||
}
|
||||
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(zipPath))) {
|
||||
for (Map.Entry<String, String> entry : entries.entrySet()) {
|
||||
outputStream.putNextEntry(new ZipEntry(entry.getKey()));
|
||||
outputStream.write(entry.getValue().getBytes());
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path parseRenderedCpconfigPath(String message) {
|
||||
Assertions.assertNotNull(message);
|
||||
int start = message.indexOf("path=");
|
||||
|
||||
@@ -21,12 +21,18 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -52,6 +58,39 @@ class InitServiceTest {
|
||||
Assertions.assertTrue(ex.getMessage().contains("tlqLicenseFileId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectEnterpriseRabbitmqTlqMode() {
|
||||
InitService service = newService(emptyRepository(), emptyStepRepository(), preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
InitPreviewRequest request = validEnterpriseRequest();
|
||||
request.getMq().setMqType("RABBITMQ_TLQ");
|
||||
request.getMq().setTlqLicenseFileId("file-tlq-001");
|
||||
|
||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.preview(request));
|
||||
Assertions.assertTrue(ex.getMessage().contains("mq.mqType"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectIndirectPureTlqMode() {
|
||||
InitService service = newService(emptyRepository(), emptyStepRepository(), preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
InitPreviewRequest request = validEnterpriseRequest();
|
||||
request.getMq().setMqType("TLQ");
|
||||
request.getMq().setTlqLicenseFileId("file-tlq-001");
|
||||
|
||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.preview(request));
|
||||
Assertions.assertTrue(ex.getMessage().contains("mq.mqType"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRequireTlqLicenseForIndirectRabbitmqTlqMode() {
|
||||
InitService service = newService(emptyRepository(), emptyStepRepository(), preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
InitPreviewRequest request = validEnterpriseRequest();
|
||||
request.getMq().setMqType("RABBITMQ_TLQ");
|
||||
request.getMq().setTlqLicenseFileId(null);
|
||||
|
||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.preview(request));
|
||||
Assertions.assertTrue(ex.getMessage().contains("tlqLicenseFileId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPathLikeFileId() {
|
||||
InitService service = newService(emptyRepository(), emptyStepRepository(), preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
@@ -113,6 +152,106 @@ class InitServiceTest {
|
||||
Assertions.assertNotNull(response.getSummary());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPlanEnterpriseTlqWithoutRabbitmqSteps() {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
|
||||
InitPreviewRequest request = validEnterpriseRequest();
|
||||
request.getMq().setMqType("TLQ");
|
||||
request.getMq().setTlqLicenseFileId("file-tlq-001");
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(request);
|
||||
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
|
||||
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
|
||||
|
||||
Assertions.assertTrue(stepCodes.contains("SET_TLQ"));
|
||||
Assertions.assertTrue(stepCodes.contains("TLQ_CONF_APPLY"));
|
||||
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
|
||||
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("DB_INIT"));
|
||||
Assertions.assertFalse(stepCodes.contains("MQ_USER_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("MQ_QUEUE_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("APP_MQ_CREDENTIALS_APPLY"));
|
||||
assertContainsStandardPostDeploySteps(stepCodes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPlanIndirectRabbitmqTlqWithTlqAndRabbitmqSteps() {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
InitService service = newService(repository, stepRepository, preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
|
||||
InitPreviewRequest request = validEnterpriseRequest();
|
||||
request.getMq().setMqType("RABBITMQ_TLQ");
|
||||
request.getMq().setTlqLicenseFileId("file-tlq-001");
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(request);
|
||||
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
|
||||
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
|
||||
|
||||
Assertions.assertTrue(stepCodes.contains("SET_TLQ"));
|
||||
Assertions.assertTrue(stepCodes.contains("TLQ_CONF_APPLY"));
|
||||
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
|
||||
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("DB_INIT"));
|
||||
Assertions.assertTrue(stepCodes.contains("MQ_USER_APPLY"));
|
||||
Assertions.assertTrue(stepCodes.contains("MQ_QUEUE_APPLY"));
|
||||
Assertions.assertTrue(stepCodes.contains("APP_MQ_CREDENTIALS_APPLY"));
|
||||
assertContainsStandardPostDeploySteps(stepCodes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPlanEnterpriseRabbitmqWithDbRenderAndApply() {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
|
||||
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
|
||||
|
||||
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
|
||||
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("DB_INIT"));
|
||||
assertContainsStandardPostDeploySteps(stepCodes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPlanIndirectRabbitmqWithDbRenderAndApply() {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
InitService service = newService(repository, stepRepository, preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
|
||||
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
|
||||
|
||||
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
|
||||
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("DB_INIT"));
|
||||
assertContainsStandardPostDeploySteps(stepCodes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotPlanStandardPostDeployStepsForDirectMode() {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
InitService service = newService(repository, stepRepository, preset("DIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
|
||||
|
||||
InitPreviewRequest request = validDirectRequest();
|
||||
request.getMq().setMqType("RABBITMQ_TLQ");
|
||||
request.getMq().setTlqLicenseFileId("file-tlq-001");
|
||||
request.setDirectTlq(directTlqConfig());
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(request);
|
||||
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
|
||||
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
|
||||
|
||||
assertNotContainsStandardPostDeploySteps(stepCodes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateAndQueryInitTask() {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
@@ -155,10 +294,11 @@ class InitServiceTest {
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
InitTaskExecuteResponse executeResponse = service.executeTask(created.getTaskId());
|
||||
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
|
||||
InitTaskDetailResponse detail = service.getTask(created.getTaskId());
|
||||
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
|
||||
|
||||
Assertions.assertEquals("SUCCESS", executeResponse.getStatus());
|
||||
Assertions.assertEquals("RUNNING", executeResponse.getStatus());
|
||||
Assertions.assertEquals("SUCCESS", detail.getStatus());
|
||||
Assertions.assertTrue(steps.stream().allMatch(s -> "SUCCESS".equals(s.getStatus())));
|
||||
}
|
||||
@@ -171,9 +311,10 @@ class InitServiceTest {
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
InitTaskExecuteResponse executeResponse = service.executeTask(created.getTaskId());
|
||||
awaitTaskStatus(service, created.getTaskId(), "FAILED");
|
||||
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
|
||||
|
||||
Assertions.assertEquals("FAILED", executeResponse.getStatus());
|
||||
Assertions.assertEquals("RUNNING", executeResponse.getStatus());
|
||||
Assertions.assertTrue(steps.stream().anyMatch(s -> "FAILED".equals(s.getStatus())));
|
||||
}
|
||||
|
||||
@@ -186,6 +327,7 @@ class InitServiceTest {
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
service.executeTask(created.getTaskId());
|
||||
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
|
||||
|
||||
InitTaskStepLogResponse log = service.getTaskStepLog(created.getTaskId(), 1);
|
||||
|
||||
@@ -195,6 +337,95 @@ class InitServiceTest {
|
||||
Assertions.assertTrue(log.getLogPath().startsWith(logDir.toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptTaskExecutionAsynchronously() throws Exception {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
BlockingInitStepExecutor executor = new BlockingInitStepExecutor();
|
||||
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs");
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
|
||||
InitTaskExecuteResponse response = Assertions.assertTimeoutPreemptively(
|
||||
Duration.ofMillis(300),
|
||||
() -> service.executeTask(created.getTaskId())
|
||||
);
|
||||
|
||||
Assertions.assertEquals(created.getTaskId(), response.getTaskId());
|
||||
Assertions.assertTrue(executor.awaitFirstStepStarted());
|
||||
executor.release();
|
||||
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResumeFromFirstFailedStepOnRetry() {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
FailOnceOnStepExecutor executor = new FailOnceOnStepExecutor("DEPLOY_RECEIVER");
|
||||
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs");
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
|
||||
service.executeTask(created.getTaskId());
|
||||
awaitTaskStatus(service, created.getTaskId(), "FAILED");
|
||||
|
||||
service.executeTask(created.getTaskId());
|
||||
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
|
||||
|
||||
Assertions.assertEquals(1, executor.executionCount("RENDER_CONFIG"));
|
||||
Assertions.assertEquals(1, executor.executionCount("DB_RENDER"));
|
||||
Assertions.assertEquals(2, executor.executionCount("DEPLOY_RECEIVER"));
|
||||
Assertions.assertEquals(1, executor.executionCount("VERIFY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResumeFromResidualRunningStep() {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
RecordingSuccessExecutor executor = new RecordingSuccessExecutor();
|
||||
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs");
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
List<InitTaskStepEntity> steps = stepRepository.findByTaskIdOrderByStepNo(created.getTaskId());
|
||||
steps.get(0).setStatus("SUCCESS");
|
||||
steps.get(0).setMessage("done");
|
||||
steps.get(1).setStatus("RUNNING");
|
||||
steps.get(1).setMessage("running");
|
||||
stepRepository.updateBatch(steps);
|
||||
|
||||
service.executeTask(created.getTaskId());
|
||||
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
|
||||
|
||||
Assertions.assertEquals(0, executor.executionCount("RENDER_CONFIG"));
|
||||
Assertions.assertEquals(1, executor.executionCount("FILE_PREPARE"));
|
||||
Assertions.assertEquals(1, executor.executionCount("VERIFY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotDispatchDuplicateBackgroundExecutionForSameTask() throws Exception {
|
||||
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
|
||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||
DuplicateDispatchProbeExecutor executor = new DuplicateDispatchProbeExecutor();
|
||||
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs");
|
||||
|
||||
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
|
||||
|
||||
Thread firstCall = new Thread(() -> service.executeTask(created.getTaskId()));
|
||||
firstCall.start();
|
||||
Assertions.assertTrue(executor.awaitFirstStepStarted());
|
||||
|
||||
InitTaskExecuteResponse duplicateResponse = Assertions.assertTimeoutPreemptively(
|
||||
Duration.ofMillis(300),
|
||||
() -> service.executeTask(created.getTaskId())
|
||||
);
|
||||
|
||||
Assertions.assertEquals("RUNNING", duplicateResponse.getStatus());
|
||||
Assertions.assertEquals(1, executor.getInvocationCount());
|
||||
|
||||
executor.release();
|
||||
firstCall.join(2000);
|
||||
}
|
||||
|
||||
private static InitService newService(
|
||||
InitTaskRepository taskRepository,
|
||||
InitTaskStepRepository stepRepository,
|
||||
@@ -254,6 +485,23 @@ class InitServiceTest {
|
||||
};
|
||||
}
|
||||
|
||||
private static void awaitTaskStatus(InitService service, String taskId, String expectedStatus) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (System.nanoTime() < deadline) {
|
||||
String current = service.getTask(taskId).getStatus();
|
||||
if (expectedStatus.equals(current)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(20L);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
Assertions.fail("interrupted while waiting for task status");
|
||||
}
|
||||
}
|
||||
Assertions.fail("expected task status " + expectedStatus + " within timeout");
|
||||
}
|
||||
|
||||
private static InitPreviewRequest validEnterpriseRequest() {
|
||||
InitPreviewRequest request = new InitPreviewRequest();
|
||||
request.setOrgCodeType("BIC");
|
||||
@@ -308,6 +556,28 @@ class InitServiceTest {
|
||||
return config;
|
||||
}
|
||||
|
||||
private static void assertContainsStandardPostDeploySteps(List<String> stepCodes) {
|
||||
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
|
||||
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
|
||||
Assertions.assertTrue(stepCodes.contains("DEPLOY_RECEIVER"));
|
||||
Assertions.assertTrue(stepCodes.contains("NGINX_CONFIG_APPLY"));
|
||||
Assertions.assertTrue(stepCodes.contains("WEB_CONFIG_APPLY"));
|
||||
Assertions.assertTrue(stepCodes.contains("START_CMSP_CMTP"));
|
||||
Assertions.assertTrue(stepCodes.contains("START_NGINX"));
|
||||
Assertions.assertTrue(stepCodes.contains("POST_CHECK"));
|
||||
}
|
||||
|
||||
private static void assertNotContainsStandardPostDeploySteps(List<String> stepCodes) {
|
||||
Assertions.assertFalse(stepCodes.contains("DB_RENDER"));
|
||||
Assertions.assertFalse(stepCodes.contains("DB_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("DEPLOY_RECEIVER"));
|
||||
Assertions.assertFalse(stepCodes.contains("NGINX_CONFIG_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("WEB_CONFIG_APPLY"));
|
||||
Assertions.assertFalse(stepCodes.contains("START_CMSP_CMTP"));
|
||||
Assertions.assertFalse(stepCodes.contains("START_NGINX"));
|
||||
Assertions.assertFalse(stepCodes.contains("POST_CHECK"));
|
||||
}
|
||||
|
||||
private static class InMemoryInitTaskRepository implements InitTaskRepository {
|
||||
|
||||
private final Map<String, InitTaskEntity> data = new HashMap<>();
|
||||
@@ -364,4 +634,117 @@ class InitServiceTest {
|
||||
data.put(taskId, new ArrayList<>(entities));
|
||||
}
|
||||
}
|
||||
|
||||
private static class BlockingInitStepExecutor implements InitStepExecutor {
|
||||
|
||||
private final CountDownLatch started = new CountDownLatch(1);
|
||||
private final CountDownLatch release = new CountDownLatch(1);
|
||||
|
||||
@Override
|
||||
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
|
||||
started.countDown();
|
||||
try {
|
||||
if (!release.await(5, TimeUnit.SECONDS)) {
|
||||
return InitStepExecutionResult.failure("timed out waiting for release", -1, null);
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return InitStepExecutionResult.failure("interrupted", -1, null);
|
||||
}
|
||||
return InitStepExecutionResult.success("ok", 0, null);
|
||||
}
|
||||
|
||||
boolean awaitFirstStepStarted() {
|
||||
try {
|
||||
return started.await(1, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void release() {
|
||||
release.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingSuccessExecutor implements InitStepExecutor {
|
||||
|
||||
private final List<String> executedStepCodes = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Override
|
||||
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
|
||||
executedStepCodes.add(step.getStepCode());
|
||||
return InitStepExecutionResult.success("ok", 0, null);
|
||||
}
|
||||
|
||||
int executionCount(String stepCode) {
|
||||
return (int) executedStepCodes.stream().filter(stepCode::equals).count();
|
||||
}
|
||||
}
|
||||
|
||||
private static class FailOnceOnStepExecutor implements InitStepExecutor {
|
||||
|
||||
private final String failStepCode;
|
||||
private final AtomicBoolean failed = new AtomicBoolean(false);
|
||||
private final List<String> executedStepCodes = new CopyOnWriteArrayList<>();
|
||||
|
||||
private FailOnceOnStepExecutor(String failStepCode) {
|
||||
this.failStepCode = failStepCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
|
||||
executedStepCodes.add(step.getStepCode());
|
||||
if (failStepCode.equals(step.getStepCode()) && failed.compareAndSet(false, true)) {
|
||||
return InitStepExecutionResult.failure("simulated failed", 1, "/tmp/fail.log");
|
||||
}
|
||||
return InitStepExecutionResult.success("ok", 0, "/tmp/success.log");
|
||||
}
|
||||
|
||||
int executionCount(String stepCode) {
|
||||
return (int) executedStepCodes.stream().filter(stepCode::equals).count();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DuplicateDispatchProbeExecutor implements InitStepExecutor {
|
||||
|
||||
private final CountDownLatch started = new CountDownLatch(1);
|
||||
private final CountDownLatch release = new CountDownLatch(1);
|
||||
private final AtomicInteger invocationCount = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
|
||||
int current = invocationCount.incrementAndGet();
|
||||
if (current == 1) {
|
||||
started.countDown();
|
||||
try {
|
||||
if (!release.await(5, TimeUnit.SECONDS)) {
|
||||
return InitStepExecutionResult.failure("timed out waiting for release", -1, null);
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return InitStepExecutionResult.failure("interrupted", -1, null);
|
||||
}
|
||||
}
|
||||
return InitStepExecutionResult.success("ok", 0, null);
|
||||
}
|
||||
|
||||
boolean awaitFirstStepStarted() {
|
||||
try {
|
||||
return started.await(1, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int getInvocationCount() {
|
||||
return invocationCount.get();
|
||||
}
|
||||
|
||||
void release() {
|
||||
release.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user