From d02c6530e8bc4869d7823fba4d55ea96b7df3d5e Mon Sep 17 00:00:00 2001 From: waner Date: Wed, 11 Mar 2026 14:23:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:CISD=E9=85=8D=E7=BD=AE=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 42 +- config/application.yml.example | 22 +- docs/deployment/tms-deployment.md | 521 +++ docs/openapi/cisd-init.openapi.json | 151 +- docs/openapi/cisd-init.openapi.yaml | 100 +- docs/openapi/cryptoopenapi.yml | 3491 +++++++++++------ ...-cisd-standard-init-support-mode-design.md | 70 + ...06-cisd-standard-init-support-mode-plan.md | 121 + docs/plans/2026-03-09-auto-deploy-refactor.md | 289 ++ ...026-03-09-standard-db-render-apply-plan.md | 175 + .../2026-03-10-deployment-doc-zh-plan.md | 51 + .../2026-03-10-file-upload-fileid-design.md | 214 + .../2026-03-10-file-upload-fileid-plan.md | 251 ++ ...-03-10-init-execute-async-resume-design.md | 211 + ...26-03-10-init-execute-async-resume-plan.md | 188 + ...-10-standard-init-script-templates-plan.md | 115 + ...10-standard-post-deploy-completion-plan.md | 249 ++ .../2026-03-10-tms-deployment-doc-design.md | 114 + .../2026-03-10-tms-deployment-doc-plan.md | 156 + ...ller-internal-convention-removal-design.md | 135 + ...roller-internal-convention-removal-plan.md | 193 + openapi.yml | 2427 ++++++++++++ pom.xml | 1 + scripts/standard-init/apply_standard_db.sh | 21 + .../standard-init/check_standard_runtime.sh | 7 + scripts/standard-init/start_standard_apps.sh | 8 + scripts/standard-init/start_standard_nginx.sh | 8 + .../properties/FileStorageProperties.java | 26 + .../crypto/pcie/PcieErrorMapper.java | 1 + .../crypto/pcie/PcieSessionTemplate.java | 3 + .../pcie/service/JnaPcieCryptoService.java | 4 + ...nalController.java => AuthController.java} | 4 +- ...troller.java => CryptoCardController.java} | 4 +- ...lController.java => DeviceController.java} | 4 +- .../file/controller/FileController.java | 36 + .../modules/file/dto/FileDetailResponse.java | 50 + .../modules/file/dto/FileUploadResponse.java | 32 + .../modules/file/entity/FileRecordEntity.java | 45 + .../modules/file/mapper/FileRecordMapper.java | 12 + .../file/repository/FileRecordRepository.java | 11 + .../impl/FileRecordRepositoryImpl.java | 27 + .../tms/modules/file/service/FileService.java | 118 + .../config/InitCommandProfileService.java | 3 - .../init/config/InitExecutorProperties.java | 231 +- ...nalController.java => InitController.java} | 7 +- .../ConfigurableInitStepExecutor.java | 438 ++- .../tms/modules/init/service/InitService.java | 204 +- ...ignController.java => SignController.java} | 6 +- ...lController.java => HealthController.java} | 2 +- src/main/resources/application.yml | 38 + .../migration/V5__create_sys_file_record.sql | 10 + .../initplan/step-command-profiles.json | 39 +- .../mapper/file/FileRecordMapper.xml | 24 + .../MockPcieCryptoServiceExtendedTest.java | 1 + .../file/FileInternalControllerTest.java | 96 + .../tms/modules/file/FileServiceTest.java | 125 + .../ConfigurableInitStepExecutorTest.java | 661 ++++ .../modules/init/service/InitServiceTest.java | 387 +- 58 files changed, 10699 insertions(+), 1281 deletions(-) create mode 100644 docs/deployment/tms-deployment.md create mode 100644 docs/plans/2026-03-06-cisd-standard-init-support-mode-design.md create mode 100644 docs/plans/2026-03-06-cisd-standard-init-support-mode-plan.md create mode 100644 docs/plans/2026-03-09-auto-deploy-refactor.md create mode 100644 docs/plans/2026-03-09-standard-db-render-apply-plan.md create mode 100644 docs/plans/2026-03-10-deployment-doc-zh-plan.md create mode 100644 docs/plans/2026-03-10-file-upload-fileid-design.md create mode 100644 docs/plans/2026-03-10-file-upload-fileid-plan.md create mode 100644 docs/plans/2026-03-10-init-execute-async-resume-design.md create mode 100644 docs/plans/2026-03-10-init-execute-async-resume-plan.md create mode 100644 docs/plans/2026-03-10-standard-init-script-templates-plan.md create mode 100644 docs/plans/2026-03-10-standard-post-deploy-completion-plan.md create mode 100644 docs/plans/2026-03-10-tms-deployment-doc-design.md create mode 100644 docs/plans/2026-03-10-tms-deployment-doc-plan.md create mode 100644 docs/plans/2026-03-11-controller-internal-convention-removal-design.md create mode 100644 docs/plans/2026-03-11-controller-internal-convention-removal-plan.md create mode 100644 openapi.yml create mode 100644 scripts/standard-init/apply_standard_db.sh create mode 100644 scripts/standard-init/check_standard_runtime.sh create mode 100644 scripts/standard-init/start_standard_apps.sh create mode 100644 scripts/standard-init/start_standard_nginx.sh create mode 100644 src/main/java/com/cisd/tms/common/config/properties/FileStorageProperties.java rename src/main/java/com/cisd/tms/modules/auth/controller/{AuthInternalController.java => AuthController.java} (92%) rename src/main/java/com/cisd/tms/modules/device/controller/{CryptoCardInternalController.java => CryptoCardController.java} (99%) rename src/main/java/com/cisd/tms/modules/device/controller/{DeviceInternalController.java => DeviceController.java} (92%) create mode 100644 src/main/java/com/cisd/tms/modules/file/controller/FileController.java create mode 100644 src/main/java/com/cisd/tms/modules/file/dto/FileDetailResponse.java create mode 100644 src/main/java/com/cisd/tms/modules/file/dto/FileUploadResponse.java create mode 100644 src/main/java/com/cisd/tms/modules/file/entity/FileRecordEntity.java create mode 100644 src/main/java/com/cisd/tms/modules/file/mapper/FileRecordMapper.java create mode 100644 src/main/java/com/cisd/tms/modules/file/repository/FileRecordRepository.java create mode 100644 src/main/java/com/cisd/tms/modules/file/repository/impl/FileRecordRepositoryImpl.java create mode 100644 src/main/java/com/cisd/tms/modules/file/service/FileService.java rename src/main/java/com/cisd/tms/modules/init/controller/{InitInternalController.java => InitController.java} (92%) rename src/main/java/com/cisd/tms/modules/sign/controller/{internal/InternalSignController.java => SignController.java} (89%) rename src/main/java/com/cisd/tms/modules/system/controller/{HealthInternalController.java => HealthController.java} (93%) create mode 100644 src/main/resources/db/migration/V5__create_sys_file_record.sql create mode 100644 src/main/resources/mapper/file/FileRecordMapper.xml create mode 100644 src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java create mode 100644 src/test/java/com/cisd/tms/modules/file/FileServiceTest.java diff --git a/README.md b/README.md index 7230472..21f7a0a 100644 --- a/README.md +++ b/README.md @@ -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:/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:/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`. diff --git a/config/application.yml.example b/config/application.yml.example index dbc1526..5c6edc2 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -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 diff --git a/docs/deployment/tms-deployment.md b/docs/deployment/tms-deployment.md new file mode 100644 index 0000000..79c9682 --- /dev/null +++ b/docs/deployment/tms-deployment.md @@ -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 + └── + └── +``` + +目录用途: +- `/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// +``` + +### 9.3 初始化常用文件字段 + +初始化请求中常见的文件引用字段: +- `licenses.receiverLicenseFileId` +- `mq.tlqLicenseFileId` +- `mq.cfmqConfigFileId` +- `licenses.cfgZipFileId` + +### 9.4 初始化执行阶段的解析规则 + +执行器会依次尝试解析: +- `/home/tms/uploads/` +- `/home/tms/uploads//` + +因此文件上传服务和初始化执行器必须使用同一个上传根目录。 + +## 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 +``` diff --git a/docs/openapi/cisd-init.openapi.json b/docs/openapi/cisd-init.openapi.json index d2a7ca4..ebb2958 100644 --- a/docs/openapi/cisd-init.openapi.json +++ b/docs/openapi/cisd-init.openapi.json @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/docs/openapi/cisd-init.openapi.yaml b/docs/openapi/cisd-init.openapi.yaml index 4a9efa6..a0b8150 100644 --- a/docs/openapi/cisd-init.openapi.yaml +++ b/docs/openapi/cisd-init.openapi.yaml @@ -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 diff --git a/docs/openapi/cryptoopenapi.yml b/docs/openapi/cryptoopenapi.yml index 2fb5ec9..8ad567f 100644 --- a/docs/openapi/cryptoopenapi.yml +++ b/docs/openapi/cryptoopenapi.yml @@ -1,546 +1,26 @@ openapi: 3.0.3 info: - title: CISD TMS Crypto Card Internal API - version: 1.0.0 + title: CISD TMS Crypto Card Internal API (Full) + version: 1.1.0 description: | - PCIe 密码卡(GM/T 0018 + SDFE 扩展)内部 HTTP 接口。 - 当前文档覆盖 `CryptoCardInternalController` 已暴露接口,可直接导入 Postman。 + 覆盖 CryptoCardController 与扩展密码卡接口的全部能力。 servers: - url: http://localhost:8080 description: Local tags: - - name: CryptoDevice - description: 设备状态与基础能力 - - name: CryptoAlgorithm - description: 摘要/HMAC/对称/MAC - - name: CryptoFile - description: 设备文件系统操作 - - name: CryptoKey - description: KEK、标准密钥对、数字信封、协商 + - name: CryptoAll + description: 密码卡全量接口 security: - InternalTokenAuth: [] paths: - /api/v1/device/crypto/device-count: - get: - tags: [CryptoDevice] - summary: 获取设备数量 - operationId: getCryptoDeviceCount - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseDeviceCountResult' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/device-conf: - get: - tags: [CryptoDevice] - summary: 获取设备配置 - operationId: getCryptoDeviceConf - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseDeviceConfResult' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/device-info: - get: - tags: [CryptoDevice] - summary: 获取设备信息 - operationId: getCryptoDeviceInfo - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseDeviceInfoResult' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/random: - get: - tags: [CryptoDevice] - summary: 产生随机数 - operationId: generateCryptoRandom - parameters: - - name: length - in: query - required: false - schema: - type: integer - minimum: 1 - default: 16 - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseRandomData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/self-test: - post: - tags: [CryptoDevice] - summary: 设备自检 - operationId: cryptoDeviceSelfTest - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseSelfTestData' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/hmac: - post: - tags: [CryptoAlgorithm] - summary: HMAC 计算(SDFE_HmacInit/Update/Final) - operationId: cryptoHmac - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoHmacInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseHmacData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/digest: - post: - tags: [CryptoAlgorithm] - summary: 摘要计算(SDF_HashInit/Update/Final) - operationId: cryptoDigest - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoDigestInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseDigestData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/encrypt/plain: - post: - tags: [CryptoAlgorithm] - summary: 明文密钥对称加密(SDF_Encrypt) - operationId: cryptoEncryptPlain - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoSymmetricInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseBinaryData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/decrypt/plain: - post: - tags: [CryptoAlgorithm] - summary: 明文密钥对称解密(SDF_Decrypt) - operationId: cryptoDecryptPlain - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoSymmetricInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseBinaryData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/encrypt/kek: - post: - tags: [CryptoAlgorithm] - summary: KEK 包装密钥对称加密 - operationId: cryptoEncryptKek - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoSymmetricKekInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseBinaryData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/decrypt/kek: - post: - tags: [CryptoAlgorithm] - summary: KEK 包装密钥对称解密 - operationId: cryptoDecryptKek - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoSymmetricKekInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseBinaryData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/mac/plain: - post: - tags: [CryptoAlgorithm] - summary: 明文密钥 MAC 计算(SDF_CalculateMAC) - operationId: cryptoMacPlain - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoMacInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseMacData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/file/create: - post: - tags: [CryptoFile] - summary: 创建设备文件(SDF_CreateFile) - operationId: cryptoFileCreate - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoFileCreateInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseVoid' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/file/write: - post: - tags: [CryptoFile] - summary: 写设备文件(SDF_WriteFile) - operationId: cryptoFileWrite - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoFileWriteInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseVoid' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/file/read: - post: - tags: [CryptoFile] - summary: 读设备文件(SDF_ReadFile) - operationId: cryptoFileRead - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoFileReadInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseBinaryData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/file/delete: - post: - tags: [CryptoFile] - summary: 删除设备文件(SDF_DeleteFile) - operationId: cryptoFileDelete - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoFileDeleteInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseVoid' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/kek/generate: - post: - tags: [CryptoKey] - summary: 生成 KEK(SDFE_GenerateKEK) - operationId: cryptoGenerateKek - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoGenerateKekInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseVoid' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/kek/status: - get: - tags: [CryptoKey] - summary: 查询 KEK 状态(SDFE_StatusKey_KEK) - operationId: cryptoKekStatus - parameters: - - name: keyIndex - in: query - required: true - schema: - type: integer - minimum: 0 - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseKeyStatusResult' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/lmk/seed-mac: - get: - tags: [CryptoKey] - summary: 导出 LMK Seed MAC(SDFE_ExportLMKSeedMAC) - operationId: cryptoExportLmkSeedMac - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseLmkSeedMacData' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/lmk/check: - get: - tags: [CryptoKey] - summary: 检查 LMK 是否存在(SDFE_CheckLMK) - operationId: cryptoCheckLmk - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseBoolean' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/std/keypair/rsa: - post: - tags: [CryptoKey] - summary: 生成标准 RSA 密钥对(SDF_GenerateKeyPair_RSA) - operationId: cryptoGenerateStandardRsaKeyPair - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoGenerateStandardRsaKeyPairInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseStandardKeyPairData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/std/keypair/ecc: - post: - tags: [CryptoKey] - summary: 生成标准 ECC 密钥对(SDF_GenerateKeyPair_ECC) - operationId: cryptoGenerateStandardEccKeyPair - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoGenerateStandardEccKeyPairInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseStandardKeyPairData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/envelope/exchange/rsa: - post: - tags: [CryptoKey] - summary: RSA 数字信封转换(SDF_ExchangeDigitEnvelopeBaseOnRSA) - operationId: cryptoExchangeEnvelopeRsa - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoExchangeEnvelopeRsaInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseBinaryData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - - /api/v1/device/crypto/envelope/exchange/ecc: - post: - tags: [CryptoKey] - summary: ECC 数字信封转换(SDF_ExchangeDigitEnvelopeBaseOnECC) - operationId: cryptoExchangeEnvelopeEcc - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CryptoExchangeEnvelopeEccInternalRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponseBinaryData' - '400': - $ref: '#/components/responses/BadRequest' - '500': - $ref: '#/components/responses/InternalError' - /api/v1/device/crypto/agreement/data-key/ecc: post: - tags: [CryptoKey] - summary: ECC 协商数据并生成会话密钥(SDF_GenerateAgreementDataAndKeyWithECC) - operationId: cryptoAgreementDataAndKeyEcc + tags: [CryptoAll] + summary: generateAgreementDataAndKeyEcc + operationId: generateAgreementDataAndKeyEcc requestBody: required: true content: @@ -553,7 +33,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiResponseAgreementDataAndKeyEccData' + $ref: '#/components/schemas/ApiResponseAny' '400': $ref: '#/components/responses/BadRequest' '500': @@ -561,9 +41,9 @@ paths: /api/v1/device/crypto/agreement/session-key/ecc: post: - tags: [CryptoKey] - summary: ECC 协商生成会话密钥(SDF_GenerateAgreementDataWithECC + SDF_GenerateKeyWithECC) - operationId: cryptoAgreementSessionKeyEcc + tags: [CryptoAll] + summary: generateSessionKeyEcc + operationId: generateSessionKeyEcc requestBody: required: true content: @@ -576,7 +56,2121 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiResponseEccAgreementSessionKeyData' + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/decrypt/kek: + post: + tags: [CryptoAll] + summary: decryptKek + operationId: decryptKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/decrypt/plain: + post: + tags: [CryptoAll] + summary: decryptPlain + operationId: decryptPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-conf: + get: + tags: [CryptoAll] + summary: getDeviceConf + operationId: getDeviceConf + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-count: + get: + tags: [CryptoAll] + summary: getDeviceCount + operationId: getDeviceCount + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-info: + get: + tags: [CryptoAll] + summary: getDeviceInfo + operationId: getDeviceInfo + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/digest: + post: + tags: [CryptoAll] + summary: digest + operationId: digest + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoDigestInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/encrypt/kek: + post: + tags: [CryptoAll] + summary: encryptKek + operationId: encryptKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/encrypt/plain: + post: + tags: [CryptoAll] + summary: encryptPlain + operationId: encryptPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/envelope/exchange/ecc: + post: + tags: [CryptoAll] + summary: exchangeEnvelopeEcc + operationId: exchangeEnvelopeEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoExchangeEnvelopeEccInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/envelope/exchange/rsa: + post: + tags: [CryptoAll] + summary: exchangeEnvelopeRsa + operationId: exchangeEnvelopeRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoExchangeEnvelopeRsaInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/all-key/backup: + post: + tags: [CryptoAll] + summary: backupAllKey + operationId: backupAllKey + parameters: + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/all-key/restore: + post: + tags: [CryptoAll] + summary: restoreAllKey + operationId: restoreAllKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/factory-reset: + post: + tags: [CryptoAll] + summary: factoryReset + operationId: factoryReset + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/init-identify: + post: + tags: [CryptoAll] + summary: initIdentify + operationId: initIdentify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64PairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/status: + get: + tags: [CryptoAll] + summary: getDeviceStatus + operationId: getDeviceStatus + parameters: + - name: statusBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/decrypt: + post: + tags: [CryptoAll] + summary: eccExternalDecrypt + operationId: eccExternalDecrypt + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalDecryptRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/encrypt: + post: + tags: [CryptoAll] + summary: eccExternalEncrypt + operationId: eccExternalEncrypt + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalEncryptRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/sign: + post: + tags: [CryptoAll] + summary: eccExternalSign + operationId: eccExternalSign + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalSignRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/verify: + post: + tags: [CryptoAll] + summary: eccExternalVerify + operationId: eccExternalVerify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalVerifyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/internal/sign: + post: + tags: [CryptoAll] + summary: eccInternalSign + operationId: eccInternalSign + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccInternalSignRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/internal/verify: + post: + tags: [CryptoAll] + summary: eccInternalVerify + operationId: eccInternalVerify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccInternalVerifyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/backup: + post: + tags: [CryptoAll] + summary: backupIk + operationId: backupIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/destroy: + post: + tags: [CryptoAll] + summary: destroyIk + operationId: destroyIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/generate: + post: + tags: [CryptoAll] + summary: generateIk + operationId: generateIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/recover: + post: + tags: [CryptoAll] + summary: recoverIk + operationId: recoverIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/status: + get: + tags: [CryptoAll] + summary: statusIk + operationId: statusIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/backup: + post: + tags: [CryptoAll] + summary: backupKek + operationId: backupKek + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/component/export: + post: + tags: [CryptoAll] + summary: exportKekComponent + operationId: exportKekComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportKekComponentRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/component/import: + post: + tags: [CryptoAll] + summary: importKekComponent + operationId: importKekComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/KekComponentRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/delete: + post: + tags: [CryptoAll] + summary: deleteKek + operationId: deleteKek + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/import: + post: + tags: [CryptoAll] + summary: importKek + operationId: importKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/recover: + post: + tags: [CryptoAll] + summary: recoverKek + operationId: recoverKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecoverKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/key/update-backed: + post: + tags: [CryptoAll] + summary: updateBackedKey + operationId: updateBackedKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateBackedKeyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/generate: + post: + tags: [CryptoAll] + summary: generateKeyPairEcc + operationId: generateKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/import: + post: + tags: [CryptoAll] + summary: importKeyPairEcc + operationId: importKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/import-encrypted: + post: + tags: [CryptoAll] + summary: importEncryptedKeyPairEcc + operationId: importEncryptedKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportEncKeyPairEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/generate: + post: + tags: [CryptoAll] + summary: generateKeyPairRsa + operationId: generateKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/import: + post: + tags: [CryptoAll] + summary: importKeyPairRsa + operationId: importKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/import-encrypted: + post: + tags: [CryptoAll] + summary: importEncryptedKeyPairRsa + operationId: importEncryptedKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportEncKeyPairRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/backup: + post: + tags: [CryptoAll] + summary: backupLmk + operationId: backupLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/backup-ex: + post: + tags: [CryptoAll] + summary: backupLmkEx + operationId: backupLmkEx + parameters: + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/destroy: + post: + tags: [CryptoAll] + summary: destroyLmk + operationId: destroyLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/generate: + post: + tags: [CryptoAll] + summary: generateLmk + operationId: generateLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/load: + post: + tags: [CryptoAll] + summary: loadLmk + operationId: loadLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/recover: + post: + tags: [CryptoAll] + summary: recoverLmk + operationId: recoverLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/recover-ex: + post: + tags: [CryptoAll] + summary: recoverLmkEx + operationId: recoverLmkEx + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/access-right/get: + post: + tags: [CryptoAll] + summary: getPrivateKeyAccessRight + operationId: getPrivateKeyAccessRight + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PrivateKeyAccessRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/access-right/release: + post: + tags: [CryptoAll] + summary: releasePrivateKeyAccessRight + operationId: releasePrivateKeyAccessRight + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/password/change: + post: + tags: [CryptoAll] + summary: changePrivateKeyAccessPassword + operationId: changePrivateKeyAccessPassword + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePrivateKeyPasswordRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/ecc/enc/export: + get: + tags: [CryptoAll] + summary: exportEncPublicKeyEcc + operationId: exportEncPublicKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/ecc/sign/export: + get: + tags: [CryptoAll] + summary: exportSignPublicKeyEcc + operationId: exportSignPublicKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/rsa/enc/export: + get: + tags: [CryptoAll] + summary: exportEncPublicKeyRsa + operationId: exportEncPublicKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/rsa/sign/export: + get: + tags: [CryptoAll] + summary: exportSignPublicKeyRsa + operationId: exportSignPublicKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/external/private-op: + post: + tags: [CryptoAll] + summary: rsaExternalPrivateOp + operationId: rsaExternalPrivateOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaExternalPrivateKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/external/public-op: + post: + tags: [CryptoAll] + summary: rsaExternalPublicOp + operationId: rsaExternalPublicOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaExternalPublicKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/internal/private-op: + post: + tags: [CryptoAll] + summary: rsaInternalPrivateOp + operationId: rsaInternalPrivateOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaInternalKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/internal/public-op: + post: + tags: [CryptoAll] + summary: rsaInternalPublicOp + operationId: rsaInternalPublicOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaInternalKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/epk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithEpkEcc + operationId: generateKeyWithEpkEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithEpkEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/ipk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithIpkEcc + operationId: generateKeyWithIpkEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithIpkEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/isk/import: + post: + tags: [CryptoAll] + summary: importKeyWithIskEcc + operationId: importKeyWithIskEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyWithIskEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/kek/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithKek + operationId: generateKeyWithKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/epk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithEpkRsa + operationId: generateKeyWithEpkRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithEpkRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/ipk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithIpkRsa + operationId: generateKeyWithIpkRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithIpkRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/isk/import: + post: + tags: [CryptoAll] + summary: importKeyWithIskRsa + operationId: importKeyWithIskRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyWithIskRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/certificate/generate: + post: + tags: [CryptoAll] + summary: generateCertificate + operationId: generateCertificate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/generate: + post: + tags: [CryptoAll] + summary: generateUkey + operationId: generateUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyGenerateRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/ik-component/read: + post: + tags: [CryptoAll] + summary: readUkeyIkComponent + operationId: readUkeyIkComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/KeyTypePinRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/lmk-component/read: + post: + tags: [CryptoAll] + summary: readLmkComponent + operationId: readLmkComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/login: + post: + tags: [CryptoAll] + summary: loginUkey + operationId: loginUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyLoginRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/login-status: + get: + tags: [CryptoAll] + summary: loginStatus + operationId: loginStatus + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/logout-admin: + post: + tags: [CryptoAll] + summary: logoutAdmin + operationId: logoutAdmin + parameters: + - name: adminIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/pin/change: + post: + tags: [CryptoAll] + summary: changeUkeyPin + operationId: changeUkeyPin + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyPinChangeRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/unlock: + post: + tags: [CryptoAll] + summary: unlockUkey + operationId: unlockUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyUnlockRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/backup: + post: + tags: [CryptoAll] + summary: backupUserKey + operationId: backupUserKey + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/delete: + post: + tags: [CryptoAll] + summary: deleteUserKey + operationId: deleteUserKey + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/ecc/status: + get: + tags: [CryptoAll] + summary: statusUserKeyEcc + operationId: statusUserKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/recover: + post: + tags: [CryptoAll] + summary: recoverUserKey + operationId: recoverUserKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecoverUserKeyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/rsa/status: + get: + tags: [CryptoAll] + summary: statusUserKeyRsa + operationId: statusUserKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/create: + post: + tags: [CryptoAll] + summary: createFile + operationId: createFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileCreateInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/delete: + post: + tags: [CryptoAll] + summary: deleteFile + operationId: deleteFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileDeleteInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/read: + post: + tags: [CryptoAll] + summary: readFile + operationId: readFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileReadInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/write: + post: + tags: [CryptoAll] + summary: writeFile + operationId: writeFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileWriteInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/hmac: + post: + tags: [CryptoAll] + summary: hmac + operationId: hmac + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoHmacInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/kek/generate: + post: + tags: [CryptoAll] + summary: generateKek + operationId: generateKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/kek/status: + get: + tags: [CryptoAll] + summary: kekStatus + operationId: kekStatus + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/lmk/check: + get: + tags: [CryptoAll] + summary: checkLmk + operationId: checkLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/lmk/seed-mac: + get: + tags: [CryptoAll] + summary: exportLmkSeedMac + operationId: exportLmkSeedMac + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/mac/plain: + post: + tags: [CryptoAll] + summary: macPlain + operationId: macPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoMacInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/random: + get: + tags: [CryptoAll] + summary: random + operationId: random + parameters: + - name: length + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/self-test: + post: + tags: [CryptoAll] + summary: selfTest + operationId: selfTest + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/std/keypair/ecc: + post: + tags: [CryptoAll] + summary: generateStandardEccKeyPair + operationId: generateStandardEccKeyPair + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateStandardEccKeyPairInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/std/keypair/rsa: + post: + tags: [CryptoAll] + summary: generateStandardRsaKeyPair + operationId: generateStandardRsaKeyPair + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateStandardRsaKeyPairInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' '400': $ref: '#/components/responses/BadRequest' '500': @@ -595,16 +2189,16 @@ components: content: application/json: schema: - $ref: '#/components/schemas/ApiResponseError' + $ref: '#/components/schemas/ApiResponseAny' InternalError: description: 业务/系统错误(含密码卡调用失败) content: application/json: schema: - $ref: '#/components/schemas/ApiResponseError' + $ref: '#/components/schemas/ApiResponseAny' schemas: - ApiResponseBase: + ApiResponseAny: type: object properties: success: @@ -613,6 +2207,10 @@ components: type: integer msg: type: string + data: + type: object + additionalProperties: true + nullable: true timestamp: type: integer format: int64 @@ -624,655 +2222,206 @@ components: nullable: true required: [success, code, msg, timestamp] - ApiResponseVoid: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - type: object - nullable: true - - ApiResponseError: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - type: object - nullable: true - details: - type: object - additionalProperties: true - nullable: true - - ApiResponseDeviceCountResult: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/DeviceCountResult' - - ApiResponseDeviceConfResult: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/DeviceConfResult' - - ApiResponseDeviceInfoResult: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/DeviceInfoResult' - - ApiResponseRandomData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/RandomData' - - ApiResponseSelfTestData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/SelfTestData' - - ApiResponseHmacData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/HmacData' - - ApiResponseDigestData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/DigestData' - - ApiResponseBinaryData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/BinaryData' - - ApiResponseMacData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/MacData' - - ApiResponseKeyStatusResult: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/KeyStatusResult' - - ApiResponseLmkSeedMacData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/LmkSeedMacData' - - ApiResponseBoolean: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - type: boolean - - ApiResponseStandardKeyPairData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/StandardKeyPairData' - - ApiResponseAgreementDataAndKeyEccData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/AgreementDataAndKeyEccData' - - ApiResponseEccAgreementSessionKeyData: - allOf: - - $ref: '#/components/schemas/ApiResponseBase' - - type: object - properties: - data: - $ref: '#/components/schemas/EccAgreementSessionKeyData' - - DeviceCountResult: + Base64DataRequest: type: object - properties: - deviceNumber: - type: integer - normalDeviceNumber: - type: integer + additionalProperties: true - DeviceConfResult: + Base64PairRequest: type: object - properties: - level: - type: integer - description: 安全级别 - devCount: - type: integer - rsaKeyCount: - type: integer - sm2KeyCount: - type: integer - kekCount: - type: integer - sessKeyCount: - type: integer - fileSysCapacity: - type: integer + additionalProperties: true - DeviceInfoResult: + ChangePrivateKeyPasswordRequest: type: object - properties: - issuerName: - type: string - deviceName: - type: string - deviceSerial: - type: string - deviceVersion: - type: integer - standardVersion: - type: integer - asymAlgAbility0: - type: integer - asymAlgAbility1: - type: integer - symAlgAbility: - type: integer - hashAlgAbility: - type: integer - bufferSize: - type: integer - - KeyStatusResult: - type: object - properties: - keyState: - type: integer - description: 0=NO_VALID, 1=NO_RIGHT, 2=RIGHT, -1=UNKNOWN - keyStateName: - type: string - - RandomData: - type: object - properties: - length: - type: integer - randomBase64: - type: string - required: [length, randomBase64] - - SelfTestData: - type: object - properties: - passed: - type: boolean - required: [passed] - - BinaryData: - type: object - properties: - length: - type: integer - dataBase64: - type: string - required: [length, dataBase64] - - HmacData: - type: object - properties: - hmacLength: - type: integer - hmacBase64: - type: string - required: [hmacLength, hmacBase64] - - DigestData: - type: object - properties: - digestLength: - type: integer - digestBase64: - type: string - required: [digestLength, digestBase64] - - MacData: - type: object - properties: - length: - type: integer - macBase64: - type: string - required: [length, macBase64] - - LmkSeedMacData: - type: object - properties: - length: - type: integer - macBase64: - type: string - required: [length, macBase64] - - StandardKeyPairData: - type: object - properties: - publicKeyLength: - type: integer - publicKeyBase64: - type: string - privateKeyLength: - type: integer - privateKeyBase64: - type: string - required: [publicKeyLength, publicKeyBase64, privateKeyLength, privateKeyBase64] - - AgreementDataAndKeyEccData: - type: object - properties: - responsePublicKeyLength: - type: integer - responsePublicKeyBase64: - type: string - responseTmpPublicKeyLength: - type: integer - responseTmpPublicKeyBase64: - type: string - sessionKeyGenerated: - type: boolean - required: - [responsePublicKeyLength, responsePublicKeyBase64, responseTmpPublicKeyLength, responseTmpPublicKeyBase64, sessionKeyGenerated] - - EccAgreementSessionKeyData: - type: object - properties: - sponsorPublicKeyLength: - type: integer - sponsorPublicKeyBase64: - type: string - sponsorTmpPublicKeyLength: - type: integer - sponsorTmpPublicKeyBase64: - type: string - sessionKeyLength: - type: integer - sessionKeyBase64: - type: string - sessionKeyGenerated: - type: boolean - required: - [sponsorPublicKeyLength, sponsorPublicKeyBase64, sponsorTmpPublicKeyLength, sponsorTmpPublicKeyBase64, sessionKeyLength, sessionKeyBase64, sessionKeyGenerated] - - CryptoHmacInternalRequest: - type: object - properties: - algId: - type: integer - minimum: 1 - description: 算法标识,如 SM3=1 - keyBase64: - type: string - dataBase64: - type: string - required: [algId, keyBase64, dataBase64] - example: - algId: 1 - keyBase64: AQIDBAUGBwgJCgsMDQ4PEA== - dataBase64: aGVsbG8tdG1z + additionalProperties: true CryptoDigestInternalRequest: type: object - properties: - algId: - type: integer - minimum: 1 - dataBase64: - type: string - outBufferSize: - type: integer - minimum: 1 - default: 64 - required: [algId, dataBase64] - example: - algId: 1 - dataBase64: dGVzdC1kaWdlc3Q= - outBufferSize: 64 - - CryptoSymmetricInternalRequest: - type: object - properties: - algId: - type: integer - minimum: 1 - description: 如 SM4_ECB=1025, SM4_CBC=1026 - keyBase64: - type: string - ivBase64: - type: string - nullable: true - dataBase64: - type: string - outBufferSize: - type: integer - minimum: 1 - required: [algId, keyBase64, dataBase64, outBufferSize] - example: - algId: 1026 - keyBase64: MDEyMzQ1Njc4OUFCQ0RFRg== - ivBase64: MDEyMzQ1Njc4OUFCQ0RFRg== - dataBase64: cGxhaW4tdGV4dA== - outBufferSize: 1024 - - CryptoSymmetricKekInternalRequest: - type: object - properties: - algId: - type: integer - minimum: 1 - kekIndex: - type: integer - minimum: 0 - wrappedKeyBase64: - type: string - ivBase64: - type: string - nullable: true - dataBase64: - type: string - outBufferSize: - type: integer - minimum: 1 - required: [algId, kekIndex, wrappedKeyBase64, dataBase64, outBufferSize] - example: - algId: 1026 - kekIndex: 1 - wrappedKeyBase64: AQIDBAUGBwg= - ivBase64: MDEyMzQ1Njc4OUFCQ0RFRg== - dataBase64: c2VjcmV0LWRhdGE= - outBufferSize: 1024 - - CryptoMacInternalRequest: - type: object - properties: - algId: - type: integer - minimum: 1 - description: 如 SM4_MAC=1040 - keyBase64: - type: string - ivBase64: - type: string - nullable: true - dataBase64: - type: string - outBufferSize: - type: integer - minimum: 1 - default: 32 - required: [algId, keyBase64, dataBase64] - example: - algId: 1040 - keyBase64: MDEyMzQ1Njc4OUFCQ0RFRg== - ivBase64: MDEyMzQ1Njc4OUFCQ0RFRg== - dataBase64: bWFjLWRhdGE= - outBufferSize: 32 - - CryptoFileCreateInternalRequest: - type: object - properties: - fileName: - type: string - fileSize: - type: integer - minimum: 1 - required: [fileName, fileSize] - example: - fileName: TEST01 - fileSize: 2048 - - CryptoFileWriteInternalRequest: - type: object - properties: - fileName: - type: string - offset: - type: integer - minimum: 0 - dataBase64: - type: string - required: [fileName, offset, dataBase64] - example: - fileName: TEST01 - offset: 0 - dataBase64: SGVsbG8sQ1JZUFRP - - CryptoFileReadInternalRequest: - type: object - properties: - fileName: - type: string - offset: - type: integer - minimum: 0 - readLength: - type: integer - minimum: 1 - required: [fileName, offset, readLength] - example: - fileName: TEST01 - offset: 0 - readLength: 64 - - CryptoFileDeleteInternalRequest: - type: object - properties: - fileName: - type: string - required: [fileName] - example: - fileName: TEST01 - - CryptoGenerateKekInternalRequest: - type: object - properties: - keyBits: - type: integer - minimum: 1 - keyIndex: - type: integer - minimum: 0 - required: [keyBits, keyIndex] - example: - keyBits: 128 - keyIndex: 1 - - CryptoGenerateStandardRsaKeyPairInternalRequest: - type: object - properties: - keyBits: - type: integer - minimum: 1 - publicKeyBufferSize: - type: integer - minimum: 1 - privateKeyBufferSize: - type: integer - minimum: 1 - required: [keyBits, publicKeyBufferSize, privateKeyBufferSize] - example: - keyBits: 2048 - publicKeyBufferSize: 1024 - privateKeyBufferSize: 4096 - - CryptoGenerateStandardEccKeyPairInternalRequest: - type: object - properties: - algId: - type: integer - minimum: 1 - keyBits: - type: integer - minimum: 1 - publicKeyBufferSize: - type: integer - minimum: 1 - privateKeyBufferSize: - type: integer - minimum: 1 - required: [algId, keyBits, publicKeyBufferSize, privateKeyBufferSize] - example: - algId: 132096 - keyBits: 256 - publicKeyBufferSize: 256 - privateKeyBufferSize: 256 - - CryptoExchangeEnvelopeRsaInternalRequest: - type: object - properties: - keyIndex: - type: integer - minimum: 0 - publicKeyBase64: - type: string - inputEnvelopeBase64: - type: string - outBufferSize: - type: integer - minimum: 1 - required: [keyIndex, publicKeyBase64, inputEnvelopeBase64, outBufferSize] - example: - keyIndex: 1 - publicKeyBase64: AQIDBAUG - inputEnvelopeBase64: BwgJCgsM - outBufferSize: 1024 + additionalProperties: true CryptoExchangeEnvelopeEccInternalRequest: type: object - properties: - keyIndex: - type: integer - minimum: 0 - algId: - type: integer - minimum: 1 - publicKeyBase64: - type: string - inputEnvelopeBase64: - type: string - outBufferSize: - type: integer - minimum: 1 - required: [keyIndex, algId, publicKeyBase64, inputEnvelopeBase64, outBufferSize] - example: - keyIndex: 1 - algId: 132096 - publicKeyBase64: AQIDBAUG - inputEnvelopeBase64: BwgJCgsM - outBufferSize: 1024 + additionalProperties: true + + CryptoExchangeEnvelopeRsaInternalRequest: + type: object + additionalProperties: true + + CryptoFileCreateInternalRequest: + type: object + additionalProperties: true + + CryptoFileDeleteInternalRequest: + type: object + additionalProperties: true + + CryptoFileReadInternalRequest: + type: object + additionalProperties: true + + CryptoFileWriteInternalRequest: + type: object + additionalProperties: true CryptoGenerateAgreementDataAndKeyEccInternalRequest: type: object - properties: - iskIndex: - type: integer - minimum: 0 - keyBits: - type: integer - minimum: 1 - responseIdBase64: - type: string - sponsorIdBase64: - type: string - sponsorPublicKeyBase64: - type: string - sponsorTmpPublicKeyBase64: - type: string - responsePublicKeyBufferSize: - type: integer - minimum: 1 - responseTmpPublicKeyBufferSize: - type: integer - minimum: 1 - required: - [iskIndex, keyBits, responseIdBase64, sponsorIdBase64, sponsorPublicKeyBase64, sponsorTmpPublicKeyBase64, responsePublicKeyBufferSize, responseTmpPublicKeyBufferSize] - example: - iskIndex: 1 - keyBits: 128 - responseIdBase64: AQID - sponsorIdBase64: BAUG - sponsorPublicKeyBase64: BwgJCg== - sponsorTmpPublicKeyBase64: CwwNDg== - responsePublicKeyBufferSize: 256 - responseTmpPublicKeyBufferSize: 256 + additionalProperties: true + + CryptoGenerateKekInternalRequest: + type: object + additionalProperties: true CryptoGenerateSessionKeyEccInternalRequest: type: object - properties: - iskIndex: - type: integer - minimum: 0 - keyBits: - type: integer - minimum: 1 - sponsorIdBase64: - type: string - responseIdBase64: - type: string - responsePublicKeyBase64: - type: string - responseTmpPublicKeyBase64: - type: string - sponsorPublicKeyBufferSize: - type: integer - minimum: 1 - sponsorTmpPublicKeyBufferSize: - type: integer - minimum: 1 - sessionKeyBufferSize: - type: integer - minimum: 1 - required: - [iskIndex, keyBits, sponsorIdBase64, responseIdBase64, responsePublicKeyBase64, responseTmpPublicKeyBase64, sponsorPublicKeyBufferSize, sponsorTmpPublicKeyBufferSize, sessionKeyBufferSize] - example: - iskIndex: 1 - keyBits: 128 - sponsorIdBase64: AQID - responseIdBase64: BAUG - responsePublicKeyBase64: BwgJCg== - responseTmpPublicKeyBase64: CwwNDg== - sponsorPublicKeyBufferSize: 256 - sponsorTmpPublicKeyBufferSize: 256 - sessionKeyBufferSize: 64 + additionalProperties: true + + CryptoGenerateStandardEccKeyPairInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateStandardRsaKeyPairInternalRequest: + type: object + additionalProperties: true + + CryptoHmacInternalRequest: + type: object + additionalProperties: true + + CryptoMacInternalRequest: + type: object + additionalProperties: true + + CryptoSymmetricInternalRequest: + type: object + additionalProperties: true + + CryptoSymmetricKekInternalRequest: + type: object + additionalProperties: true + + EccExternalDecryptRequest: + type: object + additionalProperties: true + + EccExternalEncryptRequest: + type: object + additionalProperties: true + + EccExternalSignRequest: + type: object + additionalProperties: true + + EccExternalVerifyRequest: + type: object + additionalProperties: true + + EccInternalSignRequest: + type: object + additionalProperties: true + + EccInternalVerifyRequest: + type: object + additionalProperties: true + + ExportKekComponentRequest: + type: object + additionalProperties: true + + GenerateKeyPairRequest: + type: object + additionalProperties: true + + GenerateKeyWithEpkEccRequest: + type: object + additionalProperties: true + + GenerateKeyWithEpkRsaRequest: + type: object + additionalProperties: true + + GenerateKeyWithIpkEccRequest: + type: object + additionalProperties: true + + GenerateKeyWithIpkRsaRequest: + type: object + additionalProperties: true + + GenerateKeyWithKekRequest: + type: object + additionalProperties: true + + ImportEncKeyPairEccRequest: + type: object + additionalProperties: true + + ImportEncKeyPairRsaRequest: + type: object + additionalProperties: true + + ImportKekRequest: + type: object + additionalProperties: true + + ImportKeyPairRequest: + type: object + additionalProperties: true + + ImportKeyWithIskEccRequest: + type: object + additionalProperties: true + + ImportKeyWithIskRsaRequest: + type: object + additionalProperties: true + + KekComponentRequest: + type: object + additionalProperties: true + + KeyTypePinRequest: + type: object + additionalProperties: true + + PrivateKeyAccessRequest: + type: object + additionalProperties: true + + RecoverKekRequest: + type: object + additionalProperties: true + + RecoverUserKeyRequest: + type: object + additionalProperties: true + + RsaExternalPrivateKeyOpRequest: + type: object + additionalProperties: true + + RsaExternalPublicKeyOpRequest: + type: object + additionalProperties: true + + RsaInternalKeyOpRequest: + type: object + additionalProperties: true + + UkeyGenerateRequest: + type: object + additionalProperties: true + + UkeyLoginRequest: + type: object + additionalProperties: true + + UkeyPinChangeRequest: + type: object + additionalProperties: true + + UkeyUnlockRequest: + type: object + additionalProperties: true + + UpdateBackedKeyRequest: + type: object + additionalProperties: true diff --git a/docs/plans/2026-03-06-cisd-standard-init-support-mode-design.md b/docs/plans/2026-03-06-cisd-standard-init-support-mode-design.md new file mode 100644 index 0000000..dc3cf5c --- /dev/null +++ b/docs/plans/2026-03-06-cisd-standard-init-support-mode-design.md @@ -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` 下执行。 diff --git a/docs/plans/2026-03-06-cisd-standard-init-support-mode-plan.md b/docs/plans/2026-03-06-cisd-standard-init-support-mode-plan.md new file mode 100644 index 0000000..826c9af --- /dev/null +++ b/docs/plans/2026-03-06-cisd-standard-init-support-mode-plan.md @@ -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` 的任务步骤和命令模板。 diff --git a/docs/plans/2026-03-09-auto-deploy-refactor.md b/docs/plans/2026-03-09-auto-deploy-refactor.md new file mode 100644 index 0000000..5182bdd --- /dev/null +++ b/docs/plans/2026-03-09-auto-deploy-refactor.md @@ -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 的模板职责。 diff --git a/docs/plans/2026-03-09-standard-db-render-apply-plan.md b/docs/plans/2026-03-09-standard-db-render-apply-plan.md new file mode 100644 index 0000000..051acd6 --- /dev/null +++ b/docs/plans/2026-03-09-standard-db-render-apply-plan.md @@ -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//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//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 diff --git a/docs/plans/2026-03-10-deployment-doc-zh-plan.md b/docs/plans/2026-03-10-deployment-doc-zh-plan.md new file mode 100644 index 0000000..5159164 --- /dev/null +++ b/docs/plans/2026-03-10-deployment-doc-zh-plan.md @@ -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. diff --git a/docs/plans/2026-03-10-file-upload-fileid-design.md b/docs/plans/2026-03-10-file-upload-fileid-design.md new file mode 100644 index 0000000..4320614 --- /dev/null +++ b/docs/plans/2026-03-10-file-upload-fileid-design.md @@ -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/` 或 `upload-base-dir//` 解析文件。 + +## 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//` +- `/home/tms/uploads//` + +这样做的原因: +- 与当前执行器解析逻辑兼容 +- 保留原始文件名,便于排查 +- 每个 `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` 流程对齐 + +不在第一版加入: +- 下载 +- 删除 +- 清理任务 +- 哈希去重 +- 扩展名白名单 diff --git a/docs/plans/2026-03-10-file-upload-fileid-plan.md b/docs/plans/2026-03-10-file-upload-fileid-plan.md new file mode 100644 index 0000000..dd8dea1 --- /dev/null +++ b/docs/plans/2026-03-10-file-upload-fileid-plan.md @@ -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 `//` +- 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. diff --git a/docs/plans/2026-03-10-init-execute-async-resume-design.md b/docs/plans/2026-03-10-init-execute-async-resume-design.md new file mode 100644 index 0000000..19789d4 --- /dev/null +++ b/docs/plans/2026-03-10-init-execute-async-resume-design.md @@ -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>` 或等价结构维护活动任务。 + +## 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` 视为失败点恢复 + +不在第一版加入: +- 分布式调度 +- 自动恢复未完成任务 +- 新状态枚举或额外任务队列表 diff --git a/docs/plans/2026-03-10-init-execute-async-resume-plan.md b/docs/plans/2026-03-10-init-execute-async-resume-plan.md new file mode 100644 index 0000000..7596bf9 --- /dev/null +++ b/docs/plans/2026-03-10-init-execute-async-resume-plan.md @@ -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. diff --git a/docs/plans/2026-03-10-standard-init-script-templates-plan.md b/docs/plans/2026-03-10-standard-init-script-templates-plan.md new file mode 100644 index 0000000..edac172 --- /dev/null +++ b/docs/plans/2026-03-10-standard-init-script-templates-plan.md @@ -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. diff --git a/docs/plans/2026-03-10-standard-post-deploy-completion-plan.md b/docs/plans/2026-03-10-standard-post-deploy-completion-plan.md new file mode 100644 index 0000000..911d595 --- /dev/null +++ b/docs/plans/2026-03-10-standard-post-deploy-completion-plan.md @@ -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//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//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 diff --git a/docs/plans/2026-03-10-tms-deployment-doc-design.md b/docs/plans/2026-03-10-tms-deployment-doc-design.md new file mode 100644 index 0000000..ea1ecb7 --- /dev/null +++ b/docs/plans/2026-03-10-tms-deployment-doc-design.md @@ -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` 作为完整部署手册 diff --git a/docs/plans/2026-03-10-tms-deployment-doc-plan.md b/docs/plans/2026-03-10-tms-deployment-doc-plan.md new file mode 100644 index 0000000..faeaecc --- /dev/null +++ b/docs/plans/2026-03-10-tms-deployment-doc-plan.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//` 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. diff --git a/docs/plans/2026-03-11-controller-internal-convention-removal-design.md b/docs/plans/2026-03-11-controller-internal-convention-removal-design.md new file mode 100644 index 0000000..39baeb0 --- /dev/null +++ b/docs/plans/2026-03-11-controller-internal-convention-removal-design.md @@ -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 包命名统一 +- 额外的模块结构重构 diff --git a/docs/plans/2026-03-11-controller-internal-convention-removal-plan.md b/docs/plans/2026-03-11-controller-internal-convention-removal-plan.md new file mode 100644 index 0000000..313bb02 --- /dev/null +++ b/docs/plans/2026-03-11-controller-internal-convention-removal-plan.md @@ -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. diff --git a/openapi.yml b/openapi.yml new file mode 100644 index 0000000..8ad567f --- /dev/null +++ b/openapi.yml @@ -0,0 +1,2427 @@ +openapi: 3.0.3 +info: + title: CISD TMS Crypto Card Internal API (Full) + version: 1.1.0 + description: | + 覆盖 CryptoCardController 与扩展密码卡接口的全部能力。 +servers: + - url: http://localhost:8080 + description: Local + +tags: + - name: CryptoAll + description: 密码卡全量接口 + +security: + - InternalTokenAuth: [] + +paths: + /api/v1/device/crypto/agreement/data-key/ecc: + post: + tags: [CryptoAll] + summary: generateAgreementDataAndKeyEcc + operationId: generateAgreementDataAndKeyEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateAgreementDataAndKeyEccInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/agreement/session-key/ecc: + post: + tags: [CryptoAll] + summary: generateSessionKeyEcc + operationId: generateSessionKeyEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateSessionKeyEccInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/decrypt/kek: + post: + tags: [CryptoAll] + summary: decryptKek + operationId: decryptKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/decrypt/plain: + post: + tags: [CryptoAll] + summary: decryptPlain + operationId: decryptPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-conf: + get: + tags: [CryptoAll] + summary: getDeviceConf + operationId: getDeviceConf + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-count: + get: + tags: [CryptoAll] + summary: getDeviceCount + operationId: getDeviceCount + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-info: + get: + tags: [CryptoAll] + summary: getDeviceInfo + operationId: getDeviceInfo + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/digest: + post: + tags: [CryptoAll] + summary: digest + operationId: digest + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoDigestInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/encrypt/kek: + post: + tags: [CryptoAll] + summary: encryptKek + operationId: encryptKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/encrypt/plain: + post: + tags: [CryptoAll] + summary: encryptPlain + operationId: encryptPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/envelope/exchange/ecc: + post: + tags: [CryptoAll] + summary: exchangeEnvelopeEcc + operationId: exchangeEnvelopeEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoExchangeEnvelopeEccInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/envelope/exchange/rsa: + post: + tags: [CryptoAll] + summary: exchangeEnvelopeRsa + operationId: exchangeEnvelopeRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoExchangeEnvelopeRsaInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/all-key/backup: + post: + tags: [CryptoAll] + summary: backupAllKey + operationId: backupAllKey + parameters: + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/all-key/restore: + post: + tags: [CryptoAll] + summary: restoreAllKey + operationId: restoreAllKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/factory-reset: + post: + tags: [CryptoAll] + summary: factoryReset + operationId: factoryReset + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/init-identify: + post: + tags: [CryptoAll] + summary: initIdentify + operationId: initIdentify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64PairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/status: + get: + tags: [CryptoAll] + summary: getDeviceStatus + operationId: getDeviceStatus + parameters: + - name: statusBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/decrypt: + post: + tags: [CryptoAll] + summary: eccExternalDecrypt + operationId: eccExternalDecrypt + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalDecryptRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/encrypt: + post: + tags: [CryptoAll] + summary: eccExternalEncrypt + operationId: eccExternalEncrypt + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalEncryptRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/sign: + post: + tags: [CryptoAll] + summary: eccExternalSign + operationId: eccExternalSign + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalSignRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/verify: + post: + tags: [CryptoAll] + summary: eccExternalVerify + operationId: eccExternalVerify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalVerifyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/internal/sign: + post: + tags: [CryptoAll] + summary: eccInternalSign + operationId: eccInternalSign + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccInternalSignRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/internal/verify: + post: + tags: [CryptoAll] + summary: eccInternalVerify + operationId: eccInternalVerify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccInternalVerifyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/backup: + post: + tags: [CryptoAll] + summary: backupIk + operationId: backupIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/destroy: + post: + tags: [CryptoAll] + summary: destroyIk + operationId: destroyIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/generate: + post: + tags: [CryptoAll] + summary: generateIk + operationId: generateIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/recover: + post: + tags: [CryptoAll] + summary: recoverIk + operationId: recoverIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/status: + get: + tags: [CryptoAll] + summary: statusIk + operationId: statusIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/backup: + post: + tags: [CryptoAll] + summary: backupKek + operationId: backupKek + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/component/export: + post: + tags: [CryptoAll] + summary: exportKekComponent + operationId: exportKekComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportKekComponentRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/component/import: + post: + tags: [CryptoAll] + summary: importKekComponent + operationId: importKekComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/KekComponentRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/delete: + post: + tags: [CryptoAll] + summary: deleteKek + operationId: deleteKek + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/import: + post: + tags: [CryptoAll] + summary: importKek + operationId: importKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/recover: + post: + tags: [CryptoAll] + summary: recoverKek + operationId: recoverKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecoverKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/key/update-backed: + post: + tags: [CryptoAll] + summary: updateBackedKey + operationId: updateBackedKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateBackedKeyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/generate: + post: + tags: [CryptoAll] + summary: generateKeyPairEcc + operationId: generateKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/import: + post: + tags: [CryptoAll] + summary: importKeyPairEcc + operationId: importKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/import-encrypted: + post: + tags: [CryptoAll] + summary: importEncryptedKeyPairEcc + operationId: importEncryptedKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportEncKeyPairEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/generate: + post: + tags: [CryptoAll] + summary: generateKeyPairRsa + operationId: generateKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/import: + post: + tags: [CryptoAll] + summary: importKeyPairRsa + operationId: importKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/import-encrypted: + post: + tags: [CryptoAll] + summary: importEncryptedKeyPairRsa + operationId: importEncryptedKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportEncKeyPairRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/backup: + post: + tags: [CryptoAll] + summary: backupLmk + operationId: backupLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/backup-ex: + post: + tags: [CryptoAll] + summary: backupLmkEx + operationId: backupLmkEx + parameters: + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/destroy: + post: + tags: [CryptoAll] + summary: destroyLmk + operationId: destroyLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/generate: + post: + tags: [CryptoAll] + summary: generateLmk + operationId: generateLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/load: + post: + tags: [CryptoAll] + summary: loadLmk + operationId: loadLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/recover: + post: + tags: [CryptoAll] + summary: recoverLmk + operationId: recoverLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/recover-ex: + post: + tags: [CryptoAll] + summary: recoverLmkEx + operationId: recoverLmkEx + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/access-right/get: + post: + tags: [CryptoAll] + summary: getPrivateKeyAccessRight + operationId: getPrivateKeyAccessRight + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PrivateKeyAccessRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/access-right/release: + post: + tags: [CryptoAll] + summary: releasePrivateKeyAccessRight + operationId: releasePrivateKeyAccessRight + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/password/change: + post: + tags: [CryptoAll] + summary: changePrivateKeyAccessPassword + operationId: changePrivateKeyAccessPassword + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePrivateKeyPasswordRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/ecc/enc/export: + get: + tags: [CryptoAll] + summary: exportEncPublicKeyEcc + operationId: exportEncPublicKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/ecc/sign/export: + get: + tags: [CryptoAll] + summary: exportSignPublicKeyEcc + operationId: exportSignPublicKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/rsa/enc/export: + get: + tags: [CryptoAll] + summary: exportEncPublicKeyRsa + operationId: exportEncPublicKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/rsa/sign/export: + get: + tags: [CryptoAll] + summary: exportSignPublicKeyRsa + operationId: exportSignPublicKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/external/private-op: + post: + tags: [CryptoAll] + summary: rsaExternalPrivateOp + operationId: rsaExternalPrivateOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaExternalPrivateKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/external/public-op: + post: + tags: [CryptoAll] + summary: rsaExternalPublicOp + operationId: rsaExternalPublicOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaExternalPublicKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/internal/private-op: + post: + tags: [CryptoAll] + summary: rsaInternalPrivateOp + operationId: rsaInternalPrivateOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaInternalKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/internal/public-op: + post: + tags: [CryptoAll] + summary: rsaInternalPublicOp + operationId: rsaInternalPublicOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaInternalKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/epk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithEpkEcc + operationId: generateKeyWithEpkEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithEpkEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/ipk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithIpkEcc + operationId: generateKeyWithIpkEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithIpkEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/isk/import: + post: + tags: [CryptoAll] + summary: importKeyWithIskEcc + operationId: importKeyWithIskEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyWithIskEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/kek/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithKek + operationId: generateKeyWithKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/epk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithEpkRsa + operationId: generateKeyWithEpkRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithEpkRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/ipk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithIpkRsa + operationId: generateKeyWithIpkRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithIpkRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/isk/import: + post: + tags: [CryptoAll] + summary: importKeyWithIskRsa + operationId: importKeyWithIskRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyWithIskRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/certificate/generate: + post: + tags: [CryptoAll] + summary: generateCertificate + operationId: generateCertificate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/generate: + post: + tags: [CryptoAll] + summary: generateUkey + operationId: generateUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyGenerateRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/ik-component/read: + post: + tags: [CryptoAll] + summary: readUkeyIkComponent + operationId: readUkeyIkComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/KeyTypePinRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/lmk-component/read: + post: + tags: [CryptoAll] + summary: readLmkComponent + operationId: readLmkComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/login: + post: + tags: [CryptoAll] + summary: loginUkey + operationId: loginUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyLoginRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/login-status: + get: + tags: [CryptoAll] + summary: loginStatus + operationId: loginStatus + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/logout-admin: + post: + tags: [CryptoAll] + summary: logoutAdmin + operationId: logoutAdmin + parameters: + - name: adminIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/pin/change: + post: + tags: [CryptoAll] + summary: changeUkeyPin + operationId: changeUkeyPin + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyPinChangeRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/unlock: + post: + tags: [CryptoAll] + summary: unlockUkey + operationId: unlockUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyUnlockRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/backup: + post: + tags: [CryptoAll] + summary: backupUserKey + operationId: backupUserKey + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/delete: + post: + tags: [CryptoAll] + summary: deleteUserKey + operationId: deleteUserKey + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/ecc/status: + get: + tags: [CryptoAll] + summary: statusUserKeyEcc + operationId: statusUserKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/recover: + post: + tags: [CryptoAll] + summary: recoverUserKey + operationId: recoverUserKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecoverUserKeyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/rsa/status: + get: + tags: [CryptoAll] + summary: statusUserKeyRsa + operationId: statusUserKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/create: + post: + tags: [CryptoAll] + summary: createFile + operationId: createFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileCreateInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/delete: + post: + tags: [CryptoAll] + summary: deleteFile + operationId: deleteFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileDeleteInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/read: + post: + tags: [CryptoAll] + summary: readFile + operationId: readFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileReadInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/write: + post: + tags: [CryptoAll] + summary: writeFile + operationId: writeFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileWriteInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/hmac: + post: + tags: [CryptoAll] + summary: hmac + operationId: hmac + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoHmacInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/kek/generate: + post: + tags: [CryptoAll] + summary: generateKek + operationId: generateKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/kek/status: + get: + tags: [CryptoAll] + summary: kekStatus + operationId: kekStatus + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/lmk/check: + get: + tags: [CryptoAll] + summary: checkLmk + operationId: checkLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/lmk/seed-mac: + get: + tags: [CryptoAll] + summary: exportLmkSeedMac + operationId: exportLmkSeedMac + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/mac/plain: + post: + tags: [CryptoAll] + summary: macPlain + operationId: macPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoMacInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/random: + get: + tags: [CryptoAll] + summary: random + operationId: random + parameters: + - name: length + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/self-test: + post: + tags: [CryptoAll] + summary: selfTest + operationId: selfTest + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/std/keypair/ecc: + post: + tags: [CryptoAll] + summary: generateStandardEccKeyPair + operationId: generateStandardEccKeyPair + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateStandardEccKeyPairInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/std/keypair/rsa: + post: + tags: [CryptoAll] + summary: generateStandardRsaKeyPair + operationId: generateStandardRsaKeyPair + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateStandardRsaKeyPairInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + +components: + securitySchemes: + InternalTokenAuth: + type: apiKey + in: header + name: X-Internal-Token + + responses: + BadRequest: + description: 参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + InternalError: + description: 业务/系统错误(含密码卡调用失败) + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + + schemas: + ApiResponseAny: + type: object + properties: + success: + type: boolean + code: + type: integer + msg: + type: string + data: + type: object + additionalProperties: true + nullable: true + timestamp: + type: integer + format: int64 + traceId: + type: string + nullable: true + path: + type: string + nullable: true + required: [success, code, msg, timestamp] + + Base64DataRequest: + type: object + additionalProperties: true + + Base64PairRequest: + type: object + additionalProperties: true + + ChangePrivateKeyPasswordRequest: + type: object + additionalProperties: true + + CryptoDigestInternalRequest: + type: object + additionalProperties: true + + CryptoExchangeEnvelopeEccInternalRequest: + type: object + additionalProperties: true + + CryptoExchangeEnvelopeRsaInternalRequest: + type: object + additionalProperties: true + + CryptoFileCreateInternalRequest: + type: object + additionalProperties: true + + CryptoFileDeleteInternalRequest: + type: object + additionalProperties: true + + CryptoFileReadInternalRequest: + type: object + additionalProperties: true + + CryptoFileWriteInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateAgreementDataAndKeyEccInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateKekInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateSessionKeyEccInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateStandardEccKeyPairInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateStandardRsaKeyPairInternalRequest: + type: object + additionalProperties: true + + CryptoHmacInternalRequest: + type: object + additionalProperties: true + + CryptoMacInternalRequest: + type: object + additionalProperties: true + + CryptoSymmetricInternalRequest: + type: object + additionalProperties: true + + CryptoSymmetricKekInternalRequest: + type: object + additionalProperties: true + + EccExternalDecryptRequest: + type: object + additionalProperties: true + + EccExternalEncryptRequest: + type: object + additionalProperties: true + + EccExternalSignRequest: + type: object + additionalProperties: true + + EccExternalVerifyRequest: + type: object + additionalProperties: true + + EccInternalSignRequest: + type: object + additionalProperties: true + + EccInternalVerifyRequest: + type: object + additionalProperties: true + + ExportKekComponentRequest: + type: object + additionalProperties: true + + GenerateKeyPairRequest: + type: object + additionalProperties: true + + GenerateKeyWithEpkEccRequest: + type: object + additionalProperties: true + + GenerateKeyWithEpkRsaRequest: + type: object + additionalProperties: true + + GenerateKeyWithIpkEccRequest: + type: object + additionalProperties: true + + GenerateKeyWithIpkRsaRequest: + type: object + additionalProperties: true + + GenerateKeyWithKekRequest: + type: object + additionalProperties: true + + ImportEncKeyPairEccRequest: + type: object + additionalProperties: true + + ImportEncKeyPairRsaRequest: + type: object + additionalProperties: true + + ImportKekRequest: + type: object + additionalProperties: true + + ImportKeyPairRequest: + type: object + additionalProperties: true + + ImportKeyWithIskEccRequest: + type: object + additionalProperties: true + + ImportKeyWithIskRsaRequest: + type: object + additionalProperties: true + + KekComponentRequest: + type: object + additionalProperties: true + + KeyTypePinRequest: + type: object + additionalProperties: true + + PrivateKeyAccessRequest: + type: object + additionalProperties: true + + RecoverKekRequest: + type: object + additionalProperties: true + + RecoverUserKeyRequest: + type: object + additionalProperties: true + + RsaExternalPrivateKeyOpRequest: + type: object + additionalProperties: true + + RsaExternalPublicKeyOpRequest: + type: object + additionalProperties: true + + RsaInternalKeyOpRequest: + type: object + additionalProperties: true + + UkeyGenerateRequest: + type: object + additionalProperties: true + + UkeyLoginRequest: + type: object + additionalProperties: true + + UkeyPinChangeRequest: + type: object + additionalProperties: true + + UkeyUnlockRequest: + type: object + additionalProperties: true + + UpdateBackedKeyRequest: + type: object + additionalProperties: true diff --git a/pom.xml b/pom.xml index c7bcf06..9bd5405 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,7 @@ + tms-framework org.apache.maven.plugins diff --git a/scripts/standard-init/apply_standard_db.sh b/scripts/standard-init/apply_standard_db.sh new file mode 100644 index 0000000..84b7180 --- /dev/null +++ b/scripts/standard-init/apply_standard_db.sh @@ -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" diff --git a/scripts/standard-init/check_standard_runtime.sh b/scripts/standard-init/check_standard_runtime.sh new file mode 100644 index 0000000..fad7b28 --- /dev/null +++ b/scripts/standard-init/check_standard_runtime.sh @@ -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 diff --git a/scripts/standard-init/start_standard_apps.sh b/scripts/standard-init/start_standard_apps.sh new file mode 100644 index 0000000..890fed5 --- /dev/null +++ b/scripts/standard-init/start_standard_apps.sh @@ -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" diff --git a/scripts/standard-init/start_standard_nginx.sh b/scripts/standard-init/start_standard_nginx.sh new file mode 100644 index 0000000..58efc88 --- /dev/null +++ b/scripts/standard-init/start_standard_nginx.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 diff --git a/src/main/java/com/cisd/tms/common/config/properties/FileStorageProperties.java b/src/main/java/com/cisd/tms/common/config/properties/FileStorageProperties.java new file mode 100644 index 0000000..4d8131b --- /dev/null +++ b/src/main/java/com/cisd/tms/common/config/properties/FileStorageProperties.java @@ -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; + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieErrorMapper.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieErrorMapper.java index 45770d1..9eebd57 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieErrorMapper.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieErrorMapper.java @@ -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) { diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplate.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplate.java index adf0172..bdcb99d 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplate.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplate.java @@ -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 withSession(String apiName, SessionCallback 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); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java index b0f2b46..338f148 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java @@ -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; diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthInternalController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java similarity index 92% rename from src/main/java/com/cisd/tms/modules/auth/controller/AuthInternalController.java rename to src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java index 3e13acb..c8f1405 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthInternalController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java @@ -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; } diff --git a/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardInternalController.java b/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java similarity index 99% rename from src/main/java/com/cisd/tms/modules/device/controller/CryptoCardInternalController.java rename to src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java index 8086487..42d5bf7 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardInternalController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java @@ -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; } diff --git a/src/main/java/com/cisd/tms/modules/device/controller/DeviceInternalController.java b/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java similarity index 92% rename from src/main/java/com/cisd/tms/modules/device/controller/DeviceInternalController.java rename to src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java index fee8898..6e4f03a 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/DeviceInternalController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java @@ -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; } diff --git a/src/main/java/com/cisd/tms/modules/file/controller/FileController.java b/src/main/java/com/cisd/tms/modules/file/controller/FileController.java new file mode 100644 index 0000000..d3162ff --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/controller/FileController.java @@ -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 upload(@RequestParam("file") MultipartFile file) { + return ApiResponse.success(fileService.upload(file)); + } + + // 返回已存储的文件元数据,便于调用方确认 fileId 到实际文件的映射关系。 + @GetMapping("/{fileId}") + public ApiResponse detail(@PathVariable String fileId) { + return ApiResponse.success(fileService.getDetail(fileId)); + } +} diff --git a/src/main/java/com/cisd/tms/modules/file/dto/FileDetailResponse.java b/src/main/java/com/cisd/tms/modules/file/dto/FileDetailResponse.java new file mode 100644 index 0000000..31bd680 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/dto/FileDetailResponse.java @@ -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; + } +} diff --git a/src/main/java/com/cisd/tms/modules/file/dto/FileUploadResponse.java b/src/main/java/com/cisd/tms/modules/file/dto/FileUploadResponse.java new file mode 100644 index 0000000..4b67dbf --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/dto/FileUploadResponse.java @@ -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; + } +} diff --git a/src/main/java/com/cisd/tms/modules/file/entity/FileRecordEntity.java b/src/main/java/com/cisd/tms/modules/file/entity/FileRecordEntity.java new file mode 100644 index 0000000..3fd79eb --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/entity/FileRecordEntity.java @@ -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; + } +} diff --git a/src/main/java/com/cisd/tms/modules/file/mapper/FileRecordMapper.java b/src/main/java/com/cisd/tms/modules/file/mapper/FileRecordMapper.java new file mode 100644 index 0000000..0054bae --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/mapper/FileRecordMapper.java @@ -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 selectByFileId(@Param("fileId") String fileId); +} diff --git a/src/main/java/com/cisd/tms/modules/file/repository/FileRecordRepository.java b/src/main/java/com/cisd/tms/modules/file/repository/FileRecordRepository.java new file mode 100644 index 0000000..4303a3c --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/repository/FileRecordRepository.java @@ -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 findByFileId(String fileId); + + void save(FileRecordEntity entity); +} diff --git a/src/main/java/com/cisd/tms/modules/file/repository/impl/FileRecordRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/file/repository/impl/FileRecordRepositoryImpl.java new file mode 100644 index 0000000..7c84fe6 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/repository/impl/FileRecordRepositoryImpl.java @@ -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 findByFileId(String fileId) { + return Optional.ofNullable(fileRecordMapper.selectByFileId(fileId)); + } + + @Override + public void save(FileRecordEntity entity) { + fileRecordMapper.insert(entity); + } +} diff --git a/src/main/java/com/cisd/tms/modules/file/service/FileService.java b/src/main/java/com/cisd/tms/modules/file/service/FileService.java new file mode 100644 index 0000000..a816d55 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/service/FileService.java @@ -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(); + // 固定为 //,这样初始化流程只靠 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(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/init/config/InitCommandProfileService.java b/src/main/java/com/cisd/tms/modules/init/config/InitCommandProfileService.java index 513667e..e9072b3 100644 --- a/src/main/java/com/cisd/tms/modules/init/config/InitCommandProfileService.java +++ b/src/main/java/com/cisd/tms/modules/init/config/InitCommandProfileService.java @@ -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); } diff --git a/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java b/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java index 8ec9edf..391650c 100644 --- a/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java +++ b/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java @@ -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 standardRabbitAppConfigPatterns = List.of( - "/home/cemp4i/cmsp/application-prd*.properties", - "/home/cemp4i/cmtp/application-prd*.properties" - ); + private List standardRabbitAppConfigPatterns = List.of(); // 标准版应用 RabbitMQ 连接账号/密码写回时是否加密 private boolean standardRabbitCredentialEncryptEnabled = true; // 加密密钥,默认与官方 encrypt-decrypt.jar 保持一致 private String standardRabbitCredentialSecretKey = "ABCDEFGHIJKLMN1234567890"; // LOCAL 模式命令白名单前缀;仅命中前缀的命令允许执行 private List 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 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 merged = new java.util.ArrayList<>(); + merged.add("bash " + getStandardHomeDir() + "/cpackage/"); + merged.addAll(whitelistPrefixes); + return merged; } public void setWhitelistPrefixes(List 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 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(); + } } diff --git a/src/main/java/com/cisd/tms/modules/init/controller/InitInternalController.java b/src/main/java/com/cisd/tms/modules/init/controller/InitController.java similarity index 92% rename from src/main/java/com/cisd/tms/modules/init/controller/InitInternalController.java rename to src/main/java/com/cisd/tms/modules/init/controller/InitController.java index c8284e2..f318a81 100644 --- a/src/main/java/com/cisd/tms/modules/init/controller/InitInternalController.java +++ b/src/main/java/com/cisd/tms/modules/init/controller/InitController.java @@ -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 executeTask(@PathVariable String taskId) { diff --git a/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java b/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java index aa235fa..49fb439 100644 --- a/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java +++ b/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java @@ -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 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 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 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 buildStandardSqlReplacements(PlanContext context) { + Map replacements = new LinkedHashMap<>(); + Map 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 buildDirectReplacements(PlanContext context) { Map replacements = new LinkedHashMap<>(); String node01 = normalize(context.node01Ip()); @@ -1117,7 +1386,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { } List 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 replacements) throws IOException { + String rendered = templateContent; + for (Map.Entry 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 excludeSet = Set.of(excludes); + try (Stream 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 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 command, boolean ignoreFailure, Path workingDirectory) throws IOException, InterruptedException { + return runCommand(logFile, command, ignoreFailure, workingDirectory, null); + } + + private int runCommand( + Path logFile, + List command, + boolean ignoreFailure, + Path workingDirectory, + Map 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 buildStandardDbApplyEnvironment(Path loadDir) { + Map 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 buildStandardStartEnvironment(String stepName) { + Map 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 resolveGlobFiles(List patterns) throws IOException { List result = new ArrayList<>(); if (patterns == null || patterns.isEmpty()) { diff --git a/src/main/java/com/cisd/tms/modules/init/service/InitService.java b/src/main/java/com/cisd/tms/modules/init/service/InitService.java index d519d2c..b02c1eb 100644 --- a/src/main/java/com/cisd/tms/modules/init/service/InitService.java +++ b/src/main/java/com/cisd/tms/modules/init/service/InitService.java @@ -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 PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT"); private static final Set DEPLOY_STANDARD = Set.of("SINGLE", "DUAL"); private static final Set DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD"); - private static final Set MQ_STANDARD = Set.of("RABBITMQ", "TLQ"); + private static final Set MQ_ENTERPRISE = Set.of("RABBITMQ", "TLQ"); + private static final Set MQ_INDIRECT = Set.of("RABBITMQ", "RABBITMQ_TLQ"); private static final Set 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> 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> 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 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 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 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 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 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 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); diff --git a/src/main/java/com/cisd/tms/modules/sign/controller/internal/InternalSignController.java b/src/main/java/com/cisd/tms/modules/sign/controller/SignController.java similarity index 89% rename from src/main/java/com/cisd/tms/modules/sign/controller/internal/InternalSignController.java rename to src/main/java/com/cisd/tms/modules/sign/controller/SignController.java index 0bfd340..43d6feb 100644 --- a/src/main/java/com/cisd/tms/modules/sign/controller/internal/InternalSignController.java +++ b/src/main/java/com/cisd/tms/modules/sign/controller/SignController.java @@ -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; } diff --git a/src/main/java/com/cisd/tms/modules/system/controller/HealthInternalController.java b/src/main/java/com/cisd/tms/modules/system/controller/HealthController.java similarity index 93% rename from src/main/java/com/cisd/tms/modules/system/controller/HealthInternalController.java rename to src/main/java/com/cisd/tms/modules/system/controller/HealthController.java index db7313c..c98fdfb 100644 --- a/src/main/java/com/cisd/tms/modules/system/controller/HealthInternalController.java +++ b/src/main/java/com/cisd/tms/modules/system/controller/HealthController.java @@ -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> status() { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a9d3852..bcb3e2a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,6 +38,12 @@ springdoc: operations-sorter: alpha tms: + file: + storage: + # 上传文件根目录;上传接口会将文件保存到 //。 + 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// 解析)。 direct-cpconfig-file-name: ${TMS_INIT_EXECUTOR_DIRECT_CPCONFIG_FILE_NAME:cpconfig.cfg} # 上传文件根目录;所有 fileId 均从该目录解析。 diff --git a/src/main/resources/db/migration/V5__create_sys_file_record.sql b/src/main/resources/db/migration/V5__create_sys_file_record.sql new file mode 100644 index 0000000..ef6b109 --- /dev/null +++ b/src/main/resources/db/migration/V5__create_sys_file_record.sql @@ -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) +); diff --git a/src/main/resources/initplan/step-command-profiles.json b/src/main/resources/initplan/step-command-profiles.json index 9452d95..0c922df 100644 --- a/src/main/resources/initplan/step-command-profiles.json +++ b/src/main/resources/initplan/step-command-profiles.json @@ -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" } }, diff --git a/src/main/resources/mapper/file/FileRecordMapper.xml b/src/main/resources/mapper/file/FileRecordMapper.xml new file mode 100644 index 0000000..53daec1 --- /dev/null +++ b/src/main/resources/mapper/file/FileRecordMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoServiceExtendedTest.java b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoServiceExtendedTest.java index 6f47ad9..8f24665 100644 --- a/src/test/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoServiceExtendedTest.java +++ b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoServiceExtendedTest.java @@ -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; diff --git a/src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java b/src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java new file mode 100644 index 0000000..815dff3 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/file/FileInternalControllerTest.java @@ -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("先上传文件")); + } +} diff --git a/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java b/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java new file mode 100644 index 0000000..7d99160 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java @@ -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 data = new LinkedHashMap<>(); + + @Override + public Optional findByFileId(String fileId) { + return Optional.ofNullable(data.get(fileId)); + } + + @Override + public void save(FileRecordEntity entity) { + data.put(entity.getFileId(), entity); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java b/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java index b97ea9c..c6174cb 100644 --- a/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java +++ b/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java @@ -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", "ok", "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 entries) throws IOException { + if (zipPath.getParent() != null) { + Files.createDirectories(zipPath.getParent()); + } + try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(zipPath))) { + for (Map.Entry 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="); diff --git a/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java b/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java index ff49323..e9c68d1 100644 --- a/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java @@ -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 steps = service.getTaskSteps(created.getTaskId()); + List 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 steps = service.getTaskSteps(created.getTaskId()); + List 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 steps = service.getTaskSteps(created.getTaskId()); + List 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 steps = service.getTaskSteps(created.getTaskId()); + List 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 steps = service.getTaskSteps(created.getTaskId()); + List 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 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 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 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 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 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 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 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 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(); + } + } }