feat(elevator): 对齐 V1 lib 的 Davinci/扫描/事件与部署配置

- davinci-manager-storage:FilePart 路径与基址按 V1 JAR(/portal/file、/part/*、GET /download)
- 启动类:扫描 cn.cloudwalk.serial 与 cn.cloudwalk.cwos.client.resource,补 UUIDSerial 与 ApplicationService
- deploy:v1/v2 application 中 cloudwalk.serial.enabled、Kafka 指向 192.168.3.12:9092;deploy/.gitignore 忽略日志
- cloudwalk-common-serial:补充 META-INF/spring.factories(Boot 自动配置)
- 电梯:Session 配置、Davinci Bean、Feign 包、MQTT/Visitor/Zone Feign;部署脚本与 API parity 工具更新
- 文档与根脚本若干;未纳入大体积 jar/zip 与 v1 CFR 对比目录

Made-with: Cursor

Former-commit-id: b76d142d13ebb5c0898de2d9d11bc583876829c2
This commit is contained in:
反编译工作区
2026-04-28 01:02:31 +08:00
parent be7a8e9d89
commit 418c7db202
61 changed files with 2967 additions and 461 deletions
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
systemctl restart ninca-qk-alarm-app.service systemctl restart ninca-qk-alarm-app.service
@@ -43,10 +43,11 @@ management.health.db.enabled=false
cloudwalk.datafield.enable=true cloudwalk.datafield.enable=true
cloudwalk.datafield.securityKey=d4b2aabc97394a12a27fc3cca6cd9ba1 cloudwalk.datafield.securityKey=d4b2aabc97394a12a27fc3cca6cd9ba1
cloudwalk.datafield.encrypt=AES cloudwalk.datafield.encrypt=AES
# redis\u914D\u7F6E # redis\u914D\u7F6E\uFF08\u672C\u673A Docker\uFF1Aybs-redis 6379->6379\uFF0C\u82E5\u7528 craftlabs-redis \u6539\u4E3A 6380\uFF09
spring.redis.host=10.128.161.95 spring.redis.host=127.0.0.1
spring.redis.port=6379 spring.redis.port=6379
spring.redis.password=1qaz!QAZ # \u65E0\u5BC6\u7801\u65F6\u4E0D\u914D\u7F6E password \uFF08\u82E5\u5BB9\u5668\u5F00\u4E86\u5BC6\u7801\u8BF7\u53D6\u6D88\u6CE8\u91CA\u5E76\u586B\u5199\uFF09
# spring.redis.password=
spring.redis.database=5 spring.redis.database=5
spring.redis.timeout=0 spring.redis.timeout=0
spring.redis.pool.max-active=10 spring.redis.pool.max-active=10
@@ -57,9 +58,9 @@ spring.redis.pool.min-idle=0
spring.shardingsphere.datasource.names=ds0 spring.shardingsphere.datasource.names=ds0
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://10.128.161.95:3306/cw-elevator-application?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.3.12:3307/cw-elevator-application?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
spring.shardingsphere.datasource.ds0.username=root spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=1qaz!QAZ spring.shardingsphere.datasource.ds0.password=123456
spring.shardingsphere.datasource.ds0.connection-timeout=60000 spring.shardingsphere.datasource.ds0.connection-timeout=60000
spring.shardingsphere.datasource.ds0.maximum-pool-size=20 spring.shardingsphere.datasource.ds0.maximum-pool-size=20
spring.shardingsphere.datasource.ds0.minimum-idle=5 spring.shardingsphere.datasource.ds0.minimum-idle=5
@@ -3,7 +3,7 @@
> **范围**`/media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/星河湾星中星/反编译` 下 `maven-*` 目录内全部 `pom.xml`(**主聚合工程 5 个 + 补充反应堆 7 个**,合计 **44** 个 `pom.xml` 文件)。 > **范围**`/media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/星河湾星中星/反编译` 下 `maven-*` 目录内全部 `pom.xml`(**主聚合工程 5 个 + 补充反应堆 7 个**,合计 **44** 个 `pom.xml` 文件)。
> **说明**:子模块未单独声明 `<version>` 时,与**反应堆(reactor)父 POM** 的 `<version>` 一致。 > **说明**:子模块未单独声明 `<version>` 时,与**反应堆(reactor)父 POM** 的 `<version>` 一致。
> **生成方式**:走查各 `pom.xml` 的 `parent`、`groupId`、`artifactId`、`version`、`properties` 关键项。 > **生成方式**:走查各 `pom.xml` 的 `parent`、`groupId`、`artifactId`、`version`、`properties` 关键项。
> **版本演进(主版本升级)**:相对历史反编译/私服线,本工作区工程坐标已整体抬升主版本号以便区分 —— 电梯 **2.0-SNAPSHOT**、intelligent **3.0.0-xinghewan**、cloudwalk-cloud **4.0.0-Brussels-SRX**、ninca-crk **2.0.0**、ninca-qk-alarm **1.0.0-SNAPSHOT**`cloudwalk.internal.version` 与 intelligent 依赖属性已同步 > **版本演进(主版本升级)**:相对历史反编译/私服线,本工作区工程坐标抬升主版本号以便区分。**电梯运行口径**:依赖 **`intelligent-cwoscomponent` 2.9.2-xinghewan**(与 `cw_lib` 一致,**不使用** 3.0.0);**`maven-intelligent-cwoscomponent` 源码反应堆** 仍为 **3.0.0-xinghewan**(与其它工程/历史 3.x 线兼容)。另:cloudwalk-cloud **4.0.0-Brussels-SRX**、ninca-crk **2.0.0**、ninca-qk-alarm **1.0.0-SNAPSHOT**。
--- ---
@@ -45,7 +45,7 @@
| `cw-elevator-application-service/` | `cw-elevator-application-service` | **2.0-SNAPSHOT**(继承) | 同上 | | `cw-elevator-application-service/` | `cw-elevator-application-service` | **2.0-SNAPSHOT**(继承) | 同上 |
| `cw-elevator-application-web/` | `cw-elevator-application-web` | **2.0-SNAPSHOT**(继承) | 同上 | | `cw-elevator-application-web/` | `cw-elevator-application-web` | **2.0-SNAPSHOT**(继承) | 同上 |
**反应堆内常用属性(节选)**`cloudwalk.internal.version` **4.0.0-Brussels-SRX**`intelligent.cwoscomponent.version` **3.0.0-xinghewan**`fastjson.version` **1.2.83**`guava.version` **28.2-jre**`poi.version` **4.1.2**`java.version` **1.8** **反应堆内常用属性(节选)**`cloudwalk.internal.version` / `cloudwalk.legacy.public.version` **3.7.2-Brussels-SRX**`intelligent.cwoscomponent.version` **2.9.2-xinghewan**(与 **`cw_lib`** / 上线口径一致)`fastjson.version` **1.2.73**`guava.version` **28.2-jre**`poi.version` **4.1.2**`java.version` **1.8**
--- ---
@@ -187,8 +187,8 @@
| 概念 | 常见取值 | 出现在 | | 概念 | 常见取值 | 出现在 |
|------|----------|--------| |------|----------|--------|
| 云从内部线版本 | **4.0.0-Brussels-SRX** | 电梯、intelligent、cloudwalk-cloud、ninca-crk、ninca-qk-alarm `cloudwalk.internal.version` | | 云从内部线版本 | **3.7.2**(电梯常用) / **4.0.0**(部分工程) | 电梯、ninca-qk-alarm `cloudwalk.internal.version` 多为 **3.7.2****cloudwalk-cloud / ninca-crk** 等仍可能 **4.0.0** |
| intelligent 组件线 | **3.0.0-xinghewan** | intelligent 反应堆;电梯 `intelligent.cwoscomponent.version`ninca-crk `intelligent.cwoscomponent.rest.version` | | intelligent 组件线 | **2.9.2-xinghewan**(电梯) / **3.0.0-xinghewan**(反应堆源码、ninca-crk | **电梯** `intelligent.cwoscomponent.version` **必须为 2.9.2**(与 `cw_lib` 一致);**`maven-intelligent-cwoscomponent`** 反应堆仍为 **3.0.0****ninca-crk** `intelligent.cwoscomponent.rest.version` 多为 **3.0.0** |
| 设备 SDK 协议实体 | **2.2.0** | intelligent `cloudwalk.device.sdk.version`**`maven-cloudwalk-device-sdk`** 反应堆版本 | | 设备 SDK 协议实体 | **2.2.0** | intelligent `cloudwalk.device.sdk.version`**`maven-cloudwalk-device-sdk`** 反应堆版本 |
| 设备管理 common/interface | **2.0.2** | **`maven-cloudwalk-device-manager`**;依赖 **`cloudwalk-common-result` 3.7.2****`maven-cloudwalk-legacy-public`** | | 设备管理 common/interface | **2.0.2** | **`maven-cloudwalk-device-manager`**;依赖 **`cloudwalk-common-result` 3.7.2****`maven-cloudwalk-legacy-public`** |
| AKS / 设备认证 interface | **1.0.0-SNAPSHOT** | **`maven-cwos-common-aks`****`cwos-common-aks-interface`**)、**`maven-cwos-device-authentication`****`cwos-device-authentication-interface`**);与 **`cw_lib`** 同名 JAR 对齐;**device-authentication** 另依赖 **`cloudwalk-common-service`****`maven-cloudwalk-cloud`**)、**device-manager-interface**、**protocol-entity** | | AKS / 设备认证 interface | **1.0.0-SNAPSHOT** | **`maven-cwos-common-aks`****`cwos-common-aks-interface`**)、**`maven-cwos-device-authentication`****`cwos-device-authentication-interface`**);与 **`cw_lib`** 同名 JAR 对齐;**device-authentication** 另依赖 **`cloudwalk-common-service`****`maven-cloudwalk-cloud`**)、**device-manager-interface**、**protocol-entity** |
@@ -272,6 +272,7 @@ maven-ninca-qk-alarm/ninca-qk-alarm-app-starter/pom.xml
| 2026-04-24 | 补充反应堆 **`maven-cloudwalk-device-manager`****`cloudwalk-device-manager` 2.0.2**、**common**、**interface****`反1`** zip);`pom` 总数 **37 → 40**;须在 **`maven-cloudwalk-legacy-public`** 之后 install;见 [本地编译说明.md](../build/本地编译说明.md) §3 | | 2026-04-24 | 补充反应堆 **`maven-cloudwalk-device-manager`****`cloudwalk-device-manager` 2.0.2**、**common**、**interface****`反1`** zip);`pom` 总数 **37 → 40**;须在 **`maven-cloudwalk-legacy-public`** 之后 install;见 [本地编译说明.md](../build/本地编译说明.md) §3 |
| 2026-04-24 | 补充反应堆 **`maven-cwos-common-aks`**、**`maven-cwos-device-authentication`****`反1`**`cwos-common-aks-interface``cwos-device-authentication-interface`);`pom` 总数 **40 → 44****device-authentication** 须在 **`maven-cloudwalk-cloud`** 与 **`maven-cwos-common-aks`** 之后 install;见 [本地编译说明.md](../build/本地编译说明.md) §3 | | 2026-04-24 | 补充反应堆 **`maven-cwos-common-aks`**、**`maven-cwos-device-authentication`****`反1`**`cwos-common-aks-interface``cwos-device-authentication-interface`);`pom` 总数 **40 → 44****device-authentication** 须在 **`maven-cloudwalk-cloud`** 与 **`maven-cwos-common-aks`** 之后 install;见 [本地编译说明.md](../build/本地编译说明.md) §3 |
| 2026-04-24 | **`intelligent-cwoscomponent-rest`**`FeignClient`**`org.springframework.cloud.netflix.feign`** 改为 **`org.springframework.cloud.openfeign`**,与 **Greenwich + `spring-cloud-starter-openfeign`** 一致 | | 2026-04-24 | **`intelligent-cwoscomponent-rest`**`FeignClient`**`org.springframework.cloud.netflix.feign`** 改为 **`org.springframework.cloud.openfeign`**,与 **Greenwich + `spring-cloud-starter-openfeign`** 一致 |
| 2026-04-27 | **电梯** `intelligent.cwoscomponent.version` 定为 **2.9.2-xinghewan**(与 **`cw_lib`**、上线口径一致,**禁止 3.0.0**);`build_nexus_only.sh` 改为从 **`cw_lib` + 父 POM 桩** install-file,不再默认编译 **`maven-intelligent-cwoscomponent` 3.0.0** |
--- ---
@@ -1,227 +1,227 @@
# 对外接口不变:走查任务与状态 # 对外接口不变:走查任务与状态
> **依据**:[对外接口不变-远程调用与性能优化约定](对外接口不变-远程调用与性能优化约定.md)(§2 总原则、§3 场景、§4 优先级)。 > **依据**:[对外接口不变-远程调用与性能优化约定](对外接口不变-远程调用与性能优化约定.md)(§2 总原则、§3 场景、§4 优先级)。
> **走查代码根**`maven-cw-elevator-application/cw-elevator-application-service`2026-04-24 静态走查)。 > **走查代码根**`maven-cw-elevator-application/cw-elevator-application-service`2026-04-24 静态走查)。
> **说明**:下表「子任务数」指**与约定相关的 RPC/可优化循环次数上界**(随运行时数据规模变化);**状态**表示在**不扩展 Feign/HTTP 契约**前提下是否建议动代码。 > **说明**:下表「子任务数」指**与约定相关的 RPC/可优化循环次数上界**(随运行时数据规模变化);**状态**表示在**不扩展 Feign/HTTP 契约**前提下是否建议动代码。
> **排期(2026-04**`frontend/` 暂无可用前端工程;**凡需改前端、登记页、物业管理端 UI 的迭代,暂跳过**(详见 [docs/README 当前排期与范围](../README.md#当前排期与范围2026-04))。本文档仅跟踪**电梯等服务端**在约定下的走查与修正。 > **排期(2026-04**`frontend/` 暂无可用前端工程;**凡需改前端、登记页、物业管理端 UI 的迭代,暂跳过**(详见 [docs/README 当前排期与范围](../README.md#当前排期与范围2026-04))。本文档仅跟踪**电梯等服务端**在约定下的走查与修正。
--- ---
## 1. 总览表(子任务量 + 可修正 / 不可修正) ## 1. 总览表(子任务量 + 可修正 / 不可修正)
| 约定 § | 代码锚点 | 子任务数(上界) | 下一可修正动作(建议顺序) | 不可修正或须前置确认 | | 约定 § | 代码锚点 | 子任务数(上界) | 下一可修正动作(建议顺序) | 不可修正或须前置确认 |
|--------|----------|------------------|----------------------------|------------------------| |--------|----------|------------------|----------------------------|------------------------|
| **§3.1** | `ImageRuleRefServiceImpl#delete`(约 575598 行) | **`N = param.getIds().size()`** 次 `updateGroupPersonRef`(每规则删后各 1 次) | **P0**:全部 `deleteById` 完成后,对本轮涉及的 `labelIds``organizationIds` **去重并集**,调用 **1 次** `updateGroupPersonRef`;为每次 RPC 增加 **`isSuccess` 校验**(与 §2.2 一致) | **须图库/通行确认**:合并调用是否为「刷新引用」语义、是否等价于当前 N 次效果;若不能确认则**不得合并**,仅可补返回值校验与日志(**工作区走查与方案审核见 §6**) | | **§3.1** | `ImageRuleRefServiceImpl#delete`(约 575598 行) | **`N = param.getIds().size()`** 次 `updateGroupPersonRef`(每规则删后各 1 次) | **P0**:全部 `deleteById` 完成后,对本轮涉及的 `labelIds``organizationIds` **去重并集**,调用 **1 次** `updateGroupPersonRef`;为每次 RPC 增加 **`isSuccess` 校验**(与 §2.2 一致) | **须图库/通行确认**:合并调用是否为「刷新引用」语义、是否等价于当前 N 次效果;若不能确认则**不得合并**,仅可补返回值校验与日志(**工作区走查与方案审核见 §6**) |
| **§3.2** | `AcsPersonServiceImpl#delete` | **`P = param.getPersonIds().size()`** 次 `imageStorePersonService.delete` | **P1 首轮已实施****§8**):`elevatorRemoteBoundedExecutor` 按批 `invokeAll`(默认并发 **6**),子线程 **`FeignThreadLocalUtil.callWithContext`**;遇失败**整批后**即返回 `CloudwalkResult.fail`(与原先顺序循环遇错即停一致,**非**单条失败即取消同批其它在途 RPC) | **无批量 delete**;同批内并行语义见 **§8.1** | | **§3.2** | `AcsPersonServiceImpl#delete` | **`P = param.getPersonIds().size()`** 次 `imageStorePersonService.delete` | **P1 首轮已实施****§8**):`elevatorRemoteBoundedExecutor` 按批 `invokeAll`(默认并发 **6**),子线程 **`FeignThreadLocalUtil.callWithContext`**;遇失败**整批后**即返回 `CloudwalkResult.fail`(与原先顺序循环遇错即停一致,**非**单条失败即取消同批其它在途 RPC) | **无批量 delete**;同批内并行语义见 **§8.1** |
| **§3.3** | `AcsPassRuleServiceImpl#listFloor` | **`F = passRuleResults.size()`** 次 `acsPersonService.page` | **P1 首轮已实施****§8**):楼层 `page` 有界并行(默认 **6**),`personTotals[]` 按下标写回,**列表顺序不变** | **禁止**本地 count 替代 `totalRows`RPC 上界仍为 **F** | | **§3.3** | `AcsPassRuleServiceImpl#listFloor` | **`F = passRuleResults.size()`** 次 `acsPersonService.page` | **P1 首轮已实施****§8**):楼层 `page` 有界并行(默认 **6**),`personTotals[]` 按下标写回,**列表顺序不变** | **禁止**本地 count 替代 `totalRows`RPC 上界仍为 **F** |
| **§3.4** | `AcsPassRuleServiceImpl#addImageStore` 内设备绑图库 | **`D = deviceList.size()`** 次 `bindDeviceAndImageStore` | **P1 首轮已实施****§8**):`bind` 有界并行;失败仍 **`rollbackImageStoreAfterBindFailure`** 后抛 `ServiceException`(与同批已绑设备竞态与顺序循环**同类**) | **无批量 bind** | | **§3.4** | `AcsPassRuleServiceImpl#addImageStore` 内设备绑图库 | **`D = deviceList.size()`** 次 `bindDeviceAndImageStore` | **P1 首轮已实施****§8**):`bind` 有界并行;失败仍 **`rollbackImageStoreAfterBindFailure`** 后抛 `ServiceException`(与同批已绑设备竞态与顺序循环**同类**) | **无批量 bind** |
| **§3.5** | `AcsDeviceTaskServiceImpl#updateFloors` | 增删楼层远程调用**墙钟**上界仍分别为 **A**、**D** 次;**有界并行**可缩短多楼层场景耗时(**迭代 5**,见 **§9** | 内层 `imageRuleRefService.delete` 仍受 **§3.1 冻结** 时的 RPC 上界与语义约束;`@Async("updateFloorsExecutor")` 下背压与 `catch` 细粒度见 **§7.3** / **§9** | | **§3.5** | `AcsDeviceTaskServiceImpl#updateFloors` | 增删楼层远程调用**墙钟**上界仍分别为 **A**、**D** 次;**有界并行**可缩短多楼层场景耗时(**迭代 5**,见 **§9** | 内层 `imageRuleRefService.delete` 仍受 **§3.1 冻结** 时的 RPC 上界与语义约束;`@Async("updateFloorsExecutor")` 下背压与 `catch` 细粒度见 **§7.3** / **§9** |
--- ---
## 2. 数量小结(便于排期) ## 2. 数量小结(便于排期)
| 类型 | 计数符号 | 含义 | | 类型 | 计数符号 | 含义 |
|------|----------|------| |------|----------|------|
| **§3.1 可合并 RPC** | 由 N 降为 **1**(在语义确认后) | 规则批量删除场景收益最大 | | **§3.1 可合并 RPC** | 由 N 降为 **1**(在语义确认后) | 规则批量删除场景收益最大 |
| **§3.2 并行度** | P | 人员多 ID 删除 | | **§3.2 并行度** | P | 人员多 ID 删除 |
| **§3.3 并行度** | F | 楼层列表人数统计 | | **§3.3 并行度** | F | 楼层列表人数统计 |
| **§3.4 并行度** | D | 设备绑图库 | | **§3.4 并行度** | D | 设备绑图库 |
| **§3.5** | A + D | 异步任务按层调用 | | **§3.5** | A + D | 异步任务按层调用 |
--- ---
## 3. 迭代规划与下一迭代范围(不改 HTTP/Feign 签名) ## 3. 迭代规划与下一迭代范围(不改 HTTP/Feign 签名)
### 冻结范围(2026-04-25 ### 冻结范围(2026-04-25
| 项 | 说明 | | 项 | 说明 |
|----|------| |----|------|
| **约定 §3.1 全范围** | 未取得图库/通行对 `updateGroupPersonRef` 语义确认前,**不进行**与该约定相关的**任何**代码修正与优化(含 **合并 N→1** 及 §6.4 所述**仅 `isSuccess` 校验**小步),避免在无确认期分散实现与回滚成本。 | | **约定 §3.1 全范围** | 未取得图库/通行对 `updateGroupPersonRef` 语义确认前,**不进行**与该约定相关的**任何**代码修正与优化(含 **合并 N→1** 及 §6.4 所述**仅 `isSuccess` 校验**小步),避免在无确认期分散实现与回滚成本。 |
| **恢复条件** | 图库书面或接口说明确认 + 在 [约定文档 §3.1](对外接口不变-远程调用与性能优化约定.md) 文末回填对接人、日期;再按 §1 表拆分 PR(合并与返回值校验可分步)。 | | **恢复条件** | 图库书面或接口说明确认 + 在 [约定文档 §3.1](对外接口不变-远程调用与性能优化约定.md) 文末回填对接人、日期;再按 §1 表拆分 PR(合并与返回值校验可分步)。 |
| **前端相关** | 仓库**暂无可用前端**;所有需改**前端 / 登记页 / 物业端页面**的迭代**暂不排期、不执行**(与本文服务端走查无冲突;产品方案中 UI 类阶段见 [docs/README](../README.md#当前排期与范围2026-04))。 | | **前端相关** | 仓库**暂无可用前端**;所有需改**前端 / 登记页 / 物业端页面**的迭代**暂不排期、不执行**(与本文服务端走查无冲突;产品方案中 UI 类阶段见 [docs/README](../README.md#当前排期与范围2026-04))。 |
### 迭代 3**§3.5 `updateFloors`** ### 迭代 3**§3.5 `updateFloors`**
| 字段 | 内容 | | 字段 | 内容 |
|------|------| |------|------|
| **状态** | **走查与首轮修正已完成**(见 **§7**):`getById` 空防护、步骤级 `CloudwalkResult` 校验、`keepAliveSeconds` 绑定线程池。 | | **状态** | **走查与首轮修正已完成**(见 **§7**):`getById` 空防护、步骤级 `CloudwalkResult` 校验、`keepAliveSeconds` 绑定线程池。 |
| **约定锚点** | **§3.5** `AcsDeviceTaskServiceImpl#updateFloors` | | **约定锚点** | **§3.5** `AcsDeviceTaskServiceImpl#updateFloors` |
| **暂缓项** | 楼层有界并行**已**见 **§9**`AbortPolicy``catch` 语义、其它见 **§7.3**。 | | **暂缓项** | 楼层有界并行**已**见 **§9**`AbortPolicy``catch` 语义、其它见 **§7.3**。 |
### 迭代 4**P1:§3.2 / §3.3 / §3.4 + 统一有界池** ### 迭代 4**P1:§3.2 / §3.3 / §3.4 + 统一有界池**
| 字段 | 内容 | | 字段 | 内容 |
|------|------| |------|------|
| **状态** | **已实施**(实现说明与语义边界见 **§8**)。 | | **状态** | **已实施**(实现说明与语义边界见 **§8**)。 |
| **线程池 Bean** | `elevatorRemoteBoundedExecutor``ElevatorRemoteIoExecutorConfig`),配置前缀 **`ninca.elevator.remote-io.pool`**(默认 core=max=**6**queue=512`CallerRunsPolicy`)。**未**与 `updateFloorsExecutor` 合并,避免异步任务与同步 RPC 抢同池。 | | **线程池 Bean** | `elevatorRemoteBoundedExecutor``ElevatorRemoteIoExecutorConfig`),配置前缀 **`ninca.elevator.remote-io.pool`**(默认 core=max=**6**queue=512`CallerRunsPolicy`)。**未**与 `updateFloorsExecutor` 合并,避免异步任务与同步 RPC 抢同池。 |
| **公共能力** | `FeignThreadLocalUtil.callWithContext``cw-elevator-application-common`):子线程执行 Feign 前绑定/恢复 ThreadLocal 请求头。 | | **公共能力** | `FeignThreadLocalUtil.callWithContext``cw-elevator-application-common`):子线程执行 Feign 前绑定/恢复 ThreadLocal 请求头。 |
### 迭代 5**§3.5 `updateFloors` 楼层有界并行** ### 迭代 5**§3.5 `updateFloors` 楼层有界并行**
| 字段 | 内容 | | 字段 | 内容 |
|------|------| |------|------|
| **状态** | **已实施**(见 **§9**):增楼列表、删楼列表在各自阶段内以最多 **6** 路**并发**执行 `add` / `addOnlyRule` / `delete` 等;**`BIND_DEVICES` 仍按原列表顺序**、每层成功**仍 +1** 重读任务行,与纯串行「进度语义」一致;子线程 Feign 经 `FeignThreadLocalUtil``ruleMap``zoneId` 时用 **`getOrDefault(…, "")`** 防 NPE。 | | **状态** | **已实施**(见 **§9**):增楼列表、删楼列表在各自阶段内以最多 **6** 路**并发**执行 `add` / `addOnlyRule` / `delete` 等;**`BIND_DEVICES` 仍按原列表顺序**、每层成功**仍 +1** 重读任务行,与纯串行「进度语义」一致;子线程 Feign 经 `FeignThreadLocalUtil``ruleMap``zoneId` 时用 **`getOrDefault(…, "")`** 防 NPE。 |
| **不变更** | **§3.1 全冻结** 期间未改 `ImageRuleRefServiceImpl#delete``updateGroupPersonRef` 次数。 | | **不变更** | **§3.1 全冻结** 期间未改 `ImageRuleRefServiceImpl#delete``updateGroupPersonRef` 次数。 |
### 迭代 6 及以后(可选) ### 迭代 6 及以后(可选)
- **调参 / 观测**`ninca.elevator.remote-io.pool` 按环境压测;必要时为 `updateFloors` 批处理增加**指标/耗时日志**(不扩展 HTTP 响应)。 - **调参 / 观测**`ninca.elevator.remote-io.pool` 按环境压测;必要时为 `updateFloors` 批处理增加**指标/耗时日志**(不扩展 HTTP 响应)。
- **§3.1**:图库确认后再评估合并 N→1 与(若允许)`updateGroupPersonRef``isSuccess` 小步。 - **§3.1**:图库确认后再评估合并 N→1 与(若允许)`updateGroupPersonRef``isSuccess` 小步。
**已完成回顾**:迭代 1 — **§5**;迭代 2 — **§6**(§3.1 冻结);迭代 3 — **§7**;迭代 4 — **§8**;迭代 5 — **§9**。 **已完成回顾**:迭代 1 — **§5**;迭代 2 — **§6**(§3.1 冻结);迭代 3 — **§7**;迭代 4 — **§8**;迭代 5 — **§9**。
--- ---
## 4. 文档维护 ## 4. 文档维护
| 项目 | 内容 | | 项目 | 内容 |
|------|------| |------|------|
| 更新触发 | `ImageRuleRefServiceImpl#delete``AcsPersonServiceImpl#delete``AcsPassRuleServiceImpl#listFloor` / `#addImageStore``AcsDeviceTaskServiceImpl#updateFloors` 任一处重构或签契约变更 | | 更新触发 | `ImageRuleRefServiceImpl#delete``AcsPersonServiceImpl#delete``AcsPassRuleServiceImpl#listFloor` / `#addImageStore``AcsDeviceTaskServiceImpl#updateFloors` 任一处重构或签契约变更 |
| 结论回填 | 图库对 §3.1 的确认结论请写回 [约定文档 §3.1](对外接口不变-远程调用与性能优化约定.md) 文末建议行(对接人 + 日期) | | 结论回填 | 图库对 §3.1 的确认结论请写回 [约定文档 §3.1](对外接口不变-远程调用与性能优化约定.md) 文末建议行(对接人 + 日期) |
--- ---
## 5. 迭代 1`AcsPassRuleServiceImpl#listFloor` 走查结论(§3.3 / §2.2 ## 5. 迭代 1`AcsPassRuleServiceImpl#listFloor` 走查结论(§3.3 / §2.2
**走查日期**2026-04-25 **走查日期**2026-04-25
**代码位置**`maven-cw-elevator-application/.../passrule/impl/AcsPassRuleServiceImpl.java` 方法 `listFloor` **代码位置**`maven-cw-elevator-application/.../passrule/impl/AcsPassRuleServiceImpl.java` 方法 `listFloor`
| 检查项 | 现状 | 结论 | | 检查项 | 现状 | 结论 |
|--------|------|------| |--------|------|------|
| `zoneService.tree` 返回值 | 已校验 `zoneTree.isSuccess()`,失败抛 `ServiceException` | **通过** | | `zoneService.tree` 返回值 | 已校验 `zoneTree.isSuccess()`,失败抛 `ServiceException` | **通过** |
| `acsPersonService.page` 返回值 | 循环内**未**校验 `page.isSuccess()`,直接 `page.getData()``totalRows` | **不通过**:违反约定 **§2.2**Feign 失败时 `getData()` 可能为 null,存在 **NPE** 风险,且可能把失败误当「0 人」 | | `acsPersonService.page` 返回值 | 循环内**未**校验 `page.isSuccess()`,直接 `page.getData()``totalRows` | **不通过**:违反约定 **§2.2**Feign 失败时 `getData()` 可能为 null,存在 **NPE** 风险,且可能把失败误当「0 人」 |
| `page.getData()` 空指针 | 未防护 | **不通过**:与上项合并修正 | | `page.getData()` 空指针 | 未防护 | **不通过**:与上项合并修正 |
| `rowsOfPage` | 当前为 `10`,仅使用 `totalRows` | **建议**:改为 **`1`**(约定 §3.3:仅取总数,略减负载),**不改变** HTTP 响应字段 | | `rowsOfPage` | 当前为 `10`,仅使用 `totalRows` | **建议**:改为 **`1`**(约定 §3.3:仅取总数,略减负载),**不改变** HTTP 响应字段 |
**评估结论(是否允许进入代码修正)****通过进入修正** — 仅补充与 `zoneTree` 分支一致的失败处理及空数据防护,**不**改变对外 JSON 字段语义;`rowsOfPage=1` 与现逻辑(只读 `totalRows`)等价。 **评估结论(是否允许进入代码修正)****通过进入修正** — 仅补充与 `zoneTree` 分支一致的失败处理及空数据防护,**不**改变对外 JSON 字段语义;`rowsOfPage=1` 与现逻辑(只读 `totalRows`)等价。
**代码修正(已应用,2026-04-25**`AcsPassRuleServiceImpl#listFloor` — 增加 `page.isSuccess()` 失败抛错、`page.getData()` 为空时 `personNumber=0``CloudwalkPageInfo(1,1)` 仅取总数。 **代码修正(已应用,2026-04-25**`AcsPassRuleServiceImpl#listFloor` — 增加 `page.isSuccess()` 失败抛错、`page.getData()` 为空时 `personNumber=0``CloudwalkPageInfo(1,1)` 仅取总数。
**修正实施后**:提交 **`e652eb3`**(分支 `v0.11`)。 **修正实施后**:提交 **`e652eb3`**(分支 `v0.11`)。
--- ---
## 6. 迭代 2`ImageRuleRefServiceImpl#delete` 与 §3.1 方案走查(仅评估,未改代码) ## 6. 迭代 2`ImageRuleRefServiceImpl#delete` 与 §3.1 方案走查(仅评估,未改代码)
**走查日期**2026-04-25 **走查日期**2026-04-25
**目标**:在全工作区定位**图库(intelligent 图库人员服务)**与**通行规则(电梯应用本地)**相关代码,审核「合并 `updateGroupPersonRef`」方案是否需图库侧语义确认后方可实施。 **目标**:在全工作区定位**图库(intelligent 图库人员服务)**与**通行规则(电梯应用本地)**相关代码,审核「合并 `updateGroupPersonRef`」方案是否需图库侧语义确认后方可实施。
### 6.1 图库 / 通行相关代码位置(工作区) ### 6.1 图库 / 通行相关代码位置(工作区)
| 层级 | 路径/符号 | 职责 | | 层级 | 路径/符号 | 职责 |
|------|-----------|------| |------|-----------|------|
| **Feign 契约** | `maven-intelligent-cwoscomponent/intelligent-cwoscomponent-rest/.../feign/ImageStorePersonFeignClient.java` | `POST .../updateGroupPersonRef`,请求体 `UpdateGroupPersonRefParam` | | **Feign 契约** | `maven-intelligent-cwoscomponent/intelligent-cwoscomponent-rest/.../feign/ImageStorePersonFeignClient.java` | `POST .../updateGroupPersonRef`,请求体 `UpdateGroupPersonRefParam` |
| **DTO** | `maven-intelligent-cwoscomponent/intelligent-cwoscomponent-interface/.../param/UpdateGroupPersonRefParam.java` | `businessId``imageStoreId``personIds``labelIds``organizationIds`(可同时只填部分字段) | | **DTO** | `maven-intelligent-cwoscomponent/intelligent-cwoscomponent-interface/.../param/UpdateGroupPersonRefParam.java` | `businessId``imageStoreId``personIds``labelIds``organizationIds`(可同时只填部分字段) |
| **客户端封装** | `.../service/RestImageStorePersonServiceImpl.java` | 透传 Feign | | **客户端封装** | `.../service/RestImageStorePersonServiceImpl.java` | 透传 Feign |
| **电梯侧 §3.1 锚点** | `maven-cw-elevator-application/.../passrule/impl/ImageRuleRefServiceImpl.java` 方法 **`delete`**(约 571605 行) | 按 `param.getIds()` 循环:`listByParentRule` → 收集子规则 `includeLabels` / `includeOrganizations`**`deleteById`** → **`imageStorePersonService.updateGroupPersonRef`**(每删一条父规则 1 次 RPC) | | **电梯侧 §3.1 锚点** | `maven-cw-elevator-application/.../passrule/impl/ImageRuleRefServiceImpl.java` 方法 **`delete`**(约 571605 行) | 按 `param.getIds()` 循环:`listByParentRule` → 收集子规则 `includeLabels` / `includeOrganizations`**`deleteById`** → **`imageStorePersonService.updateGroupPersonRef`**(每删一条父规则 1 次 RPC) |
| **同文件其它 `updateGroupPersonRef`** | `addOnlyRule`(约 434439)、`update` 内分支(约 556~562) | 新增/编辑规则后刷新;**同样未校验** `CloudwalkResult.isSuccess()` | | **同文件其它 `updateGroupPersonRef`** | `addOnlyRule`(约 434439)、`update` 内分支(约 556~562) | 新增/编辑规则后刷新;**同样未校验** `CloudwalkResult.isSuccess()` |
| **人员通行规则** | `maven-cw-elevator-application/.../person/impl/PersonRuleServiceImpl.java` | `add` / `addVisitor` / `delete` 末尾各 1 次 `updateGroupPersonRef`,入参以 **`personIds`** 为主(与 `delete`**label/org** 为主不同) | | **人员通行规则** | `maven-cw-elevator-application/.../person/impl/PersonRuleServiceImpl.java` | `add` / `addVisitor` / `delete` 末尾各 1 次 `updateGroupPersonRef`,入参以 **`personIds`** 为主(与 `delete`**label/org** 为主不同) |
| **HTTP 入口** | `cw-elevator-application-web/.../AcsPassRuleController.java` | `imageRuleRefService.delete` | | **HTTP 入口** | `cw-elevator-application-web/.../AcsPassRuleController.java` | `imageRuleRefService.delete` |
| **异步任务调用** | `AcsDeviceTaskServiceImpl#updateFloors` | 删楼层时多构造 `deleteParam.setIds(Collections.singletonList(ruleId))` **逐层**调 `imageRuleRefService.delete`;单次 `delete` 内仍可能 1 次或多次 `updateGroupPersonRef`(视该层 `ids` 数量) | | **异步任务调用** | `AcsDeviceTaskServiceImpl#updateFloors` | 删楼层时多构造 `deleteParam.setIds(Collections.singletonList(ruleId))` **逐层**调 `imageRuleRefService.delete`;单次 `delete` 内仍可能 1 次或多次 `updateGroupPersonRef`(视该层 `ids` 数量) |
**说明**:本仓库**无** intelligent 图库服务端的 `updateGroupPersonRef` 业务实现源码,仅能依据 DTO 与调用方推断语义;**与图库/通行团队确认**仍属 §3.1 前置条件。 **说明**:本仓库**无** intelligent 图库服务端的 `updateGroupPersonRef` 业务实现源码,仅能依据 DTO 与调用方推断语义;**与图库/通行团队确认**仍属 §3.1 前置条件。
### 6.2 当前 `delete` 行为摘要(与合并相关) ### 6.2 当前 `delete` 行为摘要(与合并相关)
- `imageStoreId` 来自 **`deviceImageStoreDao.getByBuildingId(param.getParentId())`**,整次批量删除共用同一图库。 - `imageStoreId` 来自 **`deviceImageStoreDao.getByBuildingId(param.getParentId())`**,整次批量删除共用同一图库。
- 对每个待删父规则 `id`:先读**该父规则下子规则**的 label/org(子行若带 label 则 `continue`,**同一子行不会同时写入 org**,与数据模型一致),再删父规则,再带着**本轮** `includeLabels` / `includeOrganizations` 调图库刷新。 - 对每个待删父规则 `id`:先读**该父规则下子规则**的 label/org(子行若带 label 则 `continue`,**同一子行不会同时写入 org**,与数据模型一致),再删父规则,再带着**本轮** `includeLabels` / `includeOrganizations` 调图库刷新。
- **`updateGroupPersonRef` 的返回值未做 `isSuccess` 校验**(违反约定 §2.2;与 `getImageStorePerson` 等分支不一致)。 - **`updateGroupPersonRef` 的返回值未做 `isSuccess` 校验**(违反约定 §2.2;与 `getImageStorePerson` 等分支不一致)。
### 6.3 约定中的「合并」方案审核 ### 6.3 约定中的「合并」方案审核
| 维度 | 结论 | | 维度 | 结论 |
|------|------| |------|------|
| **与现网 N 次调用的等价性** | 若图库侧语义为:在**给定 `imageStoreId`** 下,对传入的 **labelIds / organizationIds 集合**做**增量刷新或按维度重算引用**(各维度独立、与顺序无关),则「删库前汇总所有待删父规则的子维度 → **去重并集** → 删库完成后 **1 次** `updateGroupPersonRef`」与「每删一条父规则刷新其维度子集」在**最终一致**上通常等价。 | | **与现网 N 次调用的等价性** | 若图库侧语义为:在**给定 `imageStoreId`** 下,对传入的 **labelIds / organizationIds 集合**做**增量刷新或按维度重算引用**(各维度独立、与顺序无关),则「删库前汇总所有待删父规则的子维度 → **去重并集** → 删库完成后 **1 次** `updateGroupPersonRef`」与「每删一条父规则刷新其维度子集」在**最终一致**上通常等价。 |
| **必须向图库确认的风险** | 若远端实现为「以本次入参**覆盖/裁剪**图库可见范围」或依赖**调用顺序**产生副作用,则合并后的**单次并集**与 N 次**子集递进**可能不等价。约定文档 §3.1 所述「非破坏性刷新」即针对此。 | | **必须向图库确认的风险** | 若远端实现为「以本次入参**覆盖/裁剪**图库可见范围」或依赖**调用顺序**产生副作用,则合并后的**单次并集**与 N 次**子集递进**可能不等价。约定文档 §3.1 所述「非破坏性刷新」即针对此。 |
| **`personIds``labelIds`/`organizationIds` 混用** | `PersonRuleServiceImpl``personIds` 路径;`ImageRuleRefServiceImpl#delete` 走 label/org。合并方案**不改变** `delete` 仅设 label/org 的现状;但若图库服务在**未传 `personIds`** 时对空列表有特殊含义,仍须一并确认。 | | **`personIds``labelIds`/`organizationIds` 混用** | `PersonRuleServiceImpl``personIds` 路径;`ImageRuleRefServiceImpl#delete` 走 label/org。合并方案**不改变** `delete` 仅设 label/org 的现状;但若图库服务在**未传 `personIds`** 时对空列表有特殊含义,仍须一并确认。 |
| **空列表仍 RPC** | 当前循环在子规则为空时仍调用 `updateGroupPersonRef`(两列表皆空)。合并后是否**跳过空并集**可减少无效 RPC,但属于**行为微调**,若图库依赖「空刷」触发全量重算,须图库确认后再定。 | | **空列表仍 RPC** | 当前循环在子规则为空时仍调用 `updateGroupPersonRef`(两列表皆空)。合并后是否**跳过空并集**可减少无效 RPC,但属于**行为微调**,若图库依赖「空刷」触发全量重算,须图库确认后再定。 |
| **不合并时的安全增量** | 在未获图库书面确认前,**仅**可为每次 `updateGroupPersonRef` 增加 **`isSuccess` 校验 + 失败抛 `ServiceException`**(及可选日志),**不改变** RPC 次数;与约定「退化为循环调用 + 返回值校验」一致。 | | **不合并时的安全增量** | 在未获图库书面确认前,**仅**可为每次 `updateGroupPersonRef` 增加 **`isSuccess` 校验 + 失败抛 `ServiceException`**(及可选日志),**不改变** RPC 次数;与约定「退化为循环调用 + 返回值校验」一致。 |
### 6.4 评估结论(是否允许进入 §3.1「合并」类代码修正) ### 6.4 评估结论(是否允许进入 §3.1「合并」类代码修正)
- **合并 N→1**:**不允许在图库/通行确认前实施** — 与 §1 表及约定 §3.1 前置条件一致。 - **合并 N→1**:**不允许在图库/通行确认前实施** — 与 §1 表及约定 §3.1 前置条件一致。
- **仅返回值校验(及可选空并集跳过,若产品同意)**:**允许作为独立小步** — 不依赖远端语义新假设,符合 §2.2。 - **仅返回值校验(及可选空并集跳过,若产品同意)**:**允许作为独立小步** — 不依赖远端语义新假设,符合 §2.2。
**图库确认建议提问(可复制)**:「对同一 `imageStoreId``updateGroupPersonRef` 在仅设置 `labelIds``organizationIds``personIds` 为空)时,是否为**按这些维度刷新人员引用**且**不会**将图库维度裁剪为仅等于本次入参?多次调用子集与单次调用**并集**是否在业务上等价?」 **图库确认建议提问(可复制)**:「对同一 `imageStoreId``updateGroupPersonRef` 在仅设置 `labelIds``organizationIds``personIds` 为空)时,是否为**按这些维度刷新人员引用**且**不会**将图库维度裁剪为仅等于本次入参?多次调用子集与单次调用**并集**是否在业务上等价?」
**实施后回填**:确认结论、对接人、日期写入 [约定文档 §3.1 文末](对外接口不变-远程调用与性能优化约定.md)(见约定 §5)。 **实施后回填**:确认结论、对接人、日期写入 [约定文档 §3.1 文末](对外接口不变-远程调用与性能优化约定.md)(见约定 §5)。
**排期决策(2026-04-25**:在取得图库确认前,**冻结**约定 **§3.1** 相关全部代码变更;下一迭代转 **§3.5**(见上文 **「迭代 3」**)。 **排期决策(2026-04-25**:在取得图库确认前,**冻结**约定 **§3.1** 相关全部代码变更;下一迭代转 **§3.5**(见上文 **「迭代 3」**)。
--- ---
## 7. 迭代 3`AcsDeviceTaskServiceImpl#updateFloors` 走查结论(§3.5 ## 7. 迭代 3`AcsDeviceTaskServiceImpl#updateFloors` 走查结论(§3.5
**走查日期**2026-04-25 **走查日期**2026-04-25
**代码位置**`cw-elevator-application-service/.../device/impl/AcsDeviceTaskServiceImpl.java` 方法 `updateFloors`;线程池 `.../common/UpdateFloorsTaskExecutor.java`;配置 `UpdateFloorsPoolProperties``ninca.update.floor.pool.*`,默认 core=3、max=5、queue=100、`AbortPolicy`)。 **代码位置**`cw-elevator-application-service/.../device/impl/AcsDeviceTaskServiceImpl.java` 方法 `updateFloors`;线程池 `.../common/UpdateFloorsTaskExecutor.java`;配置 `UpdateFloorsPoolProperties``ninca.update.floor.pool.*`,默认 core=3、max=5、queue=100、`AbortPolicy`)。
### 7.1 调用链与 RPC 上界(与 §1 表对齐) ### 7.1 调用链与 RPC 上界(与 §1 表对齐)
| 分支 | 行为 | 上界 | | 分支 | 行为 | 上界 |
|------|------|------| |------|------|------|
| 增楼层 | `personRuleService.add` **或** `imageRuleRefService.addOnlyRule`,成功后 `updateBingDevices` | `addFloors.size()` | | 增楼层 | `personRuleService.add` **或** `imageRuleRefService.addOnlyRule`,成功后 `updateBingDevices` | `addFloors.size()` |
| 删楼层 | `personRuleService.delete` **或** `imageRuleRefService.delete`(单 id**或** 仅 DAO `deleteByOrgAndLabel`,成功后 `updateBingDevices` | `delFloorIds.size()` | | 删楼层 | `personRuleService.delete` **或** `imageRuleRefService.delete`(单 id**或** 仅 DAO `deleteByOrgAndLabel`,成功后 `updateBingDevices` | `delFloorIds.size()` |
| 内层放大 | `imageRuleRefService.delete` 仍受约定 **§3.1** 冻结影响(`updateGroupPersonRef` 多次);本迭代**未**改该内层。 | 不变 | | 内层放大 | `imageRuleRefService.delete` 仍受约定 **§3.1** 冻结影响(`updateGroupPersonRef` 多次);本迭代**未**改该内层。 | 不变 |
**入口**`AcsElevatorDeviceServiceImpl#bindingFloors` / `#bindingPerson` 在插入任务行后**同步**调用 `updateFloors`;方法带 `@Async`,实际在 **`updateFloorsExecutor`** 线程执行;HTTP 已返回 `taskId` 后,**异步内失败不会回写该 HTTP 响应**(现网行为保持;运维依赖任务进度与日志)。 **入口**`AcsElevatorDeviceServiceImpl#bindingFloors` / `#bindingPerson` 在插入任务行后**同步**调用 `updateFloors`;方法带 `@Async`,实际在 **`updateFloorsExecutor`** 线程执行;HTTP 已返回 `taskId` 后,**异步内失败不会回写该 HTTP 响应**(现网行为保持;运维依赖任务进度与日志)。
### 7.2 检查项与结论 ### 7.2 检查项与结论
| 检查项 | 现状(走查时) | 结论 | | 检查项 | 现状(走查时) | 结论 |
|--------|----------------|------| |--------|----------------|------|
| `acsDeviceTaskDao.getById` | 未判空即 `task.getIsStop()`,存在 **NPE** 风险(数据异常或竞态) | **不通过** → 已修正:空则记录并 `ServiceException` | | `acsDeviceTaskDao.getById` | 未判空即 `task.getIsStop()`,存在 **NPE** 风险(数据异常或竞态) | **不通过** → 已修正:空则记录并 `ServiceException` |
| `personRuleService.add/delete``imageRuleRefService.addOnlyRule/delete` 返回值 | 未校验 `CloudwalkResult.isSuccess()`,失败时仍 **`updateBingDevices`**,进度与真实绑定不一致 | **不通过**(违反 §2.2)→ 已修正:统一 `requireTaskStepSuccess`,失败抛错且**不**递增 | | `personRuleService.add/delete``imageRuleRefService.addOnlyRule/delete` 返回值 | 未校验 `CloudwalkResult.isSuccess()`,失败时仍 **`updateBingDevices`**,进度与真实绑定不一致 | **不通过**(违反 §2.2)→ 已修正:统一 `requireTaskStepSuccess`,失败抛错且**不**递增 |
| `catch``ServiceException(e.getMessage())` | 丢失根因类型与栈信息到调用方;异步场景仅日志含 `{}` 与异常 | **记录**:是否改为 `ServiceException(code, msg)``initCause` 属产品/运维范围,**本轮不改** | | `catch``ServiceException(e.getMessage())` | 丢失根因类型与栈信息到调用方;异步场景仅日志含 `{}` 与异常 | **记录**:是否改为 `ServiceException(code, msg)``initCause` 属产品/运维范围,**本轮不改** |
| 线程池 `keepAliveSeconds` | `UpdateFloorsPoolProperties` 有字段,**Bean 未 `setKeepAliveSeconds`**,配置项无效 | **缺陷** → 已在 `UpdateFloorsTaskExecutor` 绑定 | | 线程池 `keepAliveSeconds` | `UpdateFloorsPoolProperties` 有字段,**Bean 未 `setKeepAliveSeconds`**,配置项无效 | **缺陷** → 已在 `UpdateFloorsTaskExecutor` 绑定 |
| `RejectedExecutionHandler` | `AbortPolicy`,队列满时拒绝提交 | **记录**:与背压策略相关,**本轮不改**(须与运维对齐) | | `RejectedExecutionHandler` | `AbortPolicy`,队列满时拒绝提交 | **记录**:与背压策略相关,**本轮不改**(须与运维对齐) |
| 删楼 `ruleMap.get(delFloorId)` | 若 `listZoneInfoByIds` 未覆盖某 `delFloorId` 可能 **null** 拼接 `ruleName` | **记录**:数据正常时风险低;**本轮未改**(可后续与 DAO 对齐) | | 删楼 `ruleMap.get(delFloorId)` | 若 `listZoneInfoByIds` 未覆盖某 `delFloorId` 可能 **null** 拼接 `ruleName` | **记录**:数据正常时风险低;**本轮未改**(可后续与 DAO 对齐) |
### 7.3 评估结论(是否允许进入代码修正) ### 7.3 评估结论(是否允许进入代码修正)
- **允许并已实施(本轮)**`task` 空指针防护;对 **`personRuleService.add` / `delete`**、**`imageRuleRefService.addOnlyRule` / `delete`** 的 **`CloudwalkResult` 成功校验**(约定 §2.2);`updateFloorsExecutor` 绑定 **`keepAliveSeconds`**。 - **允许并已实施(本轮)**`task` 空指针防护;对 **`personRuleService.add` / `delete`**、**`imageRuleRefService.addOnlyRule` / `delete`** 的 **`CloudwalkResult` 成功校验**(约定 §2.2);`updateFloorsExecutor` 绑定 **`keepAliveSeconds`**。
- **暂缓(须单独评审)**:拒绝策略、`catch` 异常语义增强;**楼层有界并行**与 `ruleMap` 缺键防护**已**见 **§9**。 - **暂缓(须单独评审)**:拒绝策略、`catch` 异常语义增强;**楼层有界并行**与 `ruleMap` 缺键防护**已**见 **§9**。
**修正实施后**:提交 **`0ddeedc`**(分支 `v0.11`)。 **修正实施后**:提交 **`0ddeedc`**(分支 `v0.11`)。
--- ---
## 8. 迭代 4P1 有界并行(§3.2 / §3.3 / §3.4 ## 8. 迭代 4P1 有界并行(§3.2 / §3.3 / §3.4
**实施日期**2026-04-25 **实施日期**2026-04-25
### 8.1 行为与约定对齐说明 ### 8.1 行为与约定对齐说明
| 项 | 说明 | | 项 | 说明 |
|----|------| |----|------|
| **并发度** | 代码常量与默认池 **`corePoolSize=maxPoolSize=6`**(约定 48 区间内),可通过 **`ninca.elevator.remote-io.pool.core-pool-size` / `max-pool-size`** 覆盖。 | | **并发度** | 代码常量与默认池 **`corePoolSize=maxPoolSize=6`**(约定 48 区间内),可通过 **`ninca.elevator.remote-io.pool.core-pool-size` / `max-pool-size`** 覆盖。 |
| **§3.2 `delete`** | 多 `personId` 时按批 `ThreadPoolExecutor.invokeAll`;单 ID 仍走主线程(无 Feign 子线程问题)。失败时返回 **`76260407`** 风格 `CloudwalkResult.fail`,与改造前**一致**;**同批内**若某 RPC 失败,`invokeAll` 仍会等本批其它任务结束后再统一 `get()` 抛出/返回,与**严格单线程「第一条失败即不再发起后续」**在「已发起请求数」上略有差异,属典型有界并行取舍。 | | **§3.2 `delete`** | 多 `personId` 时按批 `ThreadPoolExecutor.invokeAll`;单 ID 仍走主线程(无 Feign 子线程问题)。失败时返回 **`76260407`** 风格 `CloudwalkResult.fail`,与改造前**一致**;**同批内**若某 RPC 失败,`invokeAll` 仍会等本批其它任务结束后再统一 `get()` 抛出/返回,与**严格单线程「第一条失败即不再发起后续」**在「已发起请求数」上略有差异,属典型有界并行取舍。 |
| **§3.3 `listFloor`** | 设备数仍顺序 DAO`acsPersonService.page` 按批并行,结果写入 `personTotals[idx]` 后顺序 `setPersonNumber`**响应楼层顺序不变**。 | | **§3.3 `listFloor`** | 设备数仍顺序 DAO`acsPersonService.page` 按批并行,结果写入 `personTotals[idx]` 后顺序 `setPersonNumber`**响应楼层顺序不变**。 |
| **§3.4 `addImageStore`** | `bindAppImageStoreDevice` 仍顺序执行;仅 **`bindDeviceAndImageStore`** 按批并行;任一批次中失败则 **`rollbackImageStoreAfterBindFailure`**(抽方法)后抛 `ServiceException`,与原先 try/catch 回滚路径一致。 | | **§3.4 `addImageStore`** | `bindAppImageStoreDevice` 仍顺序执行;仅 **`bindDeviceAndImageStore`** 按批并行;任一批次中失败则 **`rollbackImageStoreAfterBindFailure`**(抽方法)后抛 `ServiceException`,与原先 try/catch 回滚路径一致。 |
| **Feign ThreadLocal** | 所有子线程 RPC 经 **`FeignThreadLocalUtil.callWithContext`**,避免池化线程串请求头。 | | **Feign ThreadLocal** | 所有子线程 RPC 经 **`FeignThreadLocalUtil.callWithContext`**,避免池化线程串请求头。 |
**实施后提交****`fe571aa`**(分支 `v0.11`)。 **实施后提交****`fe571aa`**(分支 `v0.11`)。
--- ---
## 9. 迭代 5`updateFloors` 楼层有界并行(§3.5 ## 9. 迭代 5`updateFloors` 楼层有界并行(§3.5
**实施日期**2026-04-24(工作区) **实施日期**2026-04-24(工作区)
### 9.1 设计要点 ### 9.1 设计要点
| 项 | 说明 | | 项 | 说明 |
|----|------| |----|------|
| **并发度** | 与 §8 一致使用 **`elevatorRemoteBoundedExecutor`** 的 **`ThreadPoolExecutor#invokeAll`**,批大小 **`UPDATE_FLOORS_FLOOR_PARALLEL = 6`**(与 `AcsPassRuleServiceImpl` 等处一致)。 | | **并发度** | 与 §8 一致使用 **`elevatorRemoteBoundedExecutor`** 的 **`ThreadPoolExecutor#invokeAll`**,批大小 **`UPDATE_FLOORS_FLOOR_PARALLEL = 6`**(与 `AcsPassRuleServiceImpl` 等处一致)。 |
| **bind 推进** | 同批内各楼层 RPC **可并发**`Future#get()` **按列表下标顺序** 等待;每遇返回值 **1** 则主线程**再** `getById`**`BIND_DEVICES`+1**,与旧实现「每层成功后立即 +1」的**终态**一致。 | | **bind 推进** | 同批内各楼层 RPC **可并发**`Future#get()` **按列表下标顺序** 等待;每遇返回值 **1** 则主线程**再** `getById`**`BIND_DEVICES`+1**,与旧实现「每层成功后立即 +1」的**终态**一致。 |
| **停任务** | 每层子步骤开头仍 `getById``isStop != 0` 时该层贡献 **0**、不推进 bind。 | | **停任务** | 每层子步骤开头仍 `getById``isStop != 0` 时该层贡献 **0**、不推进 bind。 |
| **DAO 异常** | 删楼分支中 `getByRuleName` / `deleteByOrgAndLabel`**`DataAccessException`** 在子步内转 **`ServiceException`**,以配合 `Callable``invokeAll` 的异常链。 | | **DAO 异常** | 删楼分支中 `getByRuleName` / `deleteByOrgAndLabel`**`DataAccessException`** 在子步内转 **`ServiceException`**,以配合 `Callable``invokeAll` 的异常链。 |
| **§3.1** | 未改 `imageRuleRefService.delete` 实现,内层 `updateGroupPersonRef` 行为与冻结前一致。 | | **§3.1** | 未改 `imageRuleRefService.delete` 实现,内层 `updateGroupPersonRef` 行为与冻结前一致。 |
### 9.2 同批多楼层与「遇错即停」 ### 9.2 同批多楼层与「遇错即停」
**§8.1** 类似:同批中若一 Floor 的 RPC 失败,同批**其它在途** Floor 的 RPC 可能已执行完毕;`get()` 按顺序在**首败**时抛出。与严格串行「前一层失败则后层不再发起」在**已发出请求**上可存在差异,属有界并行常见取舍。 **§8.1** 类似:同批中若一 Floor 的 RPC 失败,同批**其它在途** Floor 的 RPC 可能已执行完毕;`get()` 按顺序在**首败**时抛出。与严格串行「前一层失败则后层不再发起」在**已发出请求**上可存在差异,属有界并行常见取舍。
### 9.3 代码位置 ### 9.3 代码位置
- `AcsDeviceTaskServiceImpl#updateFloors``#runAddFloorsInBoundedParallel` / `#runDelFloorsInBoundedParallel``#addOneFloorStep` / `#delOneFloorStep``#advanceBindProgressOne` - `AcsDeviceTaskServiceImpl#updateFloors``#runAddFloorsInBoundedParallel` / `#runDelFloorsInBoundedParallel``#addOneFloorStep` / `#delOneFloorStep``#advanceBindProgressOne`
@@ -1,96 +1,96 @@
# 对外接口不变前提下的远程调用与性能优化约定 # 对外接口不变前提下的远程调用与性能优化约定
**适用范围**:本仓库电梯应用(`maven-cw-elevator-application`)及与之集成的 intelligent / 图库等 Feign 调用路径。 **适用范围**:本仓库电梯应用(`maven-cw-elevator-application`)及与之集成的 intelligent / 图库等 Feign 调用路径。
**订立日期**2026-04-24 **订立日期**2026-04-24
**状态**:团队约定(实施前对「远端语义」类条目需与图库/通行服务二次确认)。 **状态**:团队约定(实施前对「远端语义」类条目需与图库/通行服务二次确认)。
**走查落地**[对外接口不变-走查任务与状态](对外接口不变-走查任务与状态.md)(子任务计数、可修正 / 不可修正与下一步)。 **走查落地**[对外接口不变-走查任务与状态](对外接口不变-走查任务与状态.md)(子任务计数、可修正 / 不可修正与下一步)。
--- ---
## 1. 「对外接口不变」的可操作定义 ## 1. 「对外接口不变」的可操作定义
| 层级 | 是否允许变更 | 说明 | | 层级 | 是否允许变更 | 说明 |
|------|----------------|------| |------|----------------|------|
| **对客户端 HTTP** | **否** | URL、Method、请求/响应 JSON 字段与语义、成功/失败码含义与现网一致。 | | **对客户端 HTTP** | **否** | URL、Method、请求/响应 JSON 字段与语义、成功/失败码含义与现网一致。 |
| **本模块内部实现** | **是** | `ServiceImpl`、DAO、私有方法、远程调用次数、并行策略等,只要最终 HTTP 行为与业务语义一致。 | | **本模块内部实现** | **是** | `ServiceImpl`、DAO、私有方法、远程调用次数、并行策略等,只要最终 HTTP 行为与业务语义一致。 |
| **intelligent / 图库等 Feign 契约** | **默认否** | 不新增 `batchDelete`、多 zone 一次统计等接口时,仅允许在**现有方法**上组合、批处理逻辑或**有界并行**。若单独立项扩展 Feign,再按新版本评审。 | | **intelligent / 图库等 Feign 契约** | **默认否** | 不新增 `batchDelete`、多 zone 一次统计等接口时,仅允许在**现有方法**上组合、批处理逻辑或**有界并行**。若单独立项扩展 Feign,再按新版本评审。 |
下文默认 **HTTP 与 Feign 契约均不扩展** 下文默认 **HTTP 与 Feign 契约均不扩展**
--- ---
## 2. 总原则 ## 2. 总原则
1. **等价优先**:任何减少 RPC 或改并行的地方,须保证与改造前**同一业务语义**(尤其 `totalRows`、删除范围、刷新范围);不得用「近似」本地统计替代远端分页结果,除非完成**对账验收**并文档留痕。 1. **等价优先**:任何减少 RPC 或改并行的地方,须保证与改造前**同一业务语义**(尤其 `totalRows`、删除范围、刷新范围);不得用「近似」本地统计替代远端分页结果,除非完成**对账验收**并文档留痕。
2. **返回值必检**:对 `CloudwalkResult` / Feign 封装结果须校验 `isSuccess`(或项目统一规范);禁止依赖「失败静默」作为常态路径。 2. **返回值必检**:对 `CloudwalkResult` / Feign 封装结果须校验 `isSuccess`(或项目统一规范);禁止依赖「失败静默」作为常态路径。
3. **合并优于 N 次**:若远端操作对同一维度**幂等重算**(如按 label/org 刷新引用),优先在本地删库结束后**去重并集单次调用**,而非循环内重复调用。 3. **合并优于 N 次**:若远端操作对同一维度**幂等重算**(如按 label/org 刷新引用),优先在本地删库结束后**去重并集单次调用**,而非循环内重复调用。
4. **无法合并时的默认手段**:在契约无批量 API 时,采用**有界并行**(固定线程池或 `Semaphore`,建议并发度 4~8,可按环境调参),并明确**失败聚合策略**与现网「遇错即停」等行为一致。 4. **无法合并时的默认手段**:在契约无批量 API 时,采用**有界并行**(固定线程池或 `Semaphore`,建议并发度 4~8,可按环境调参),并明确**失败聚合策略**与现网「遇错即停」等行为一致。
5. **事务边界**:涉及多 RPC + 本地 DB 时,在约定中明确是否 `@Transactional`、失败是否需补偿;禁止在未定义产品语义时擅自「部分成功」。 5. **事务边界**:涉及多 RPC + 本地 DB 时,在约定中明确是否 `@Transactional`、失败是否需补偿;禁止在未定义产品语义时擅自「部分成功」。
--- ---
## 3. 按场景的具体约定 ## 3. 按场景的具体约定
### 3.1 `ImageRuleRefServiceImpl.delete`(循环内 `updateGroupPersonRef` ### 3.1 `ImageRuleRefServiceImpl.delete`(循环内 `updateGroupPersonRef`
- **目标**:降低重复刷新带来的 RPC 与负载。 - **目标**:降低重复刷新带来的 RPC 与负载。
- **约定**:在**所有** `deleteById`(或等价删库步骤)完成后,对本轮涉及的 `labelIds``organizationIds` **去重并集**,再调用**一次** `updateGroupPersonRef` - **约定**:在**所有** `deleteById`(或等价删库步骤)完成后,对本轮涉及的 `labelIds``organizationIds` **去重并集**,再调用**一次** `updateGroupPersonRef`
- **前置条件**:与图库/通行侧确认「仅传 label/org 列表」为**刷新引用**语义,而非会把图库裁剪为仅含这些维度的破坏性语义。若不能确认,**退化为**保持循环调用 + **仍须做返回值校验** - **前置条件**:与图库/通行侧确认「仅传 label/org 列表」为**刷新引用**语义,而非会把图库裁剪为仅含这些维度的破坏性语义。若不能确认,**退化为**保持循环调用 + **仍须做返回值校验**
- **HTTP**:不变。 - **HTTP**:不变。
### 3.2 `AcsPersonServiceImpl.delete`(循环内 `imageStorePersonService.delete` ### 3.2 `AcsPersonServiceImpl.delete`(循环内 `imageStorePersonService.delete`
- **约束**`ImageStorePersonDelParam` 仅支持单 `personId`,无批量 delete 时不扩展契约。 - **约束**`ImageStorePersonDelParam` 仅支持单 `personId`,无批量 delete 时不扩展契约。
- **约定**:采用**有界并行** `delete`;失败策略与现实现**一致**(例如任一失败则整体失败);若需与现网「严格顺序失败」完全一致,须在实现中定义完成顺序与异常聚合方式。 - **约定**:采用**有界并行** `delete`;失败策略与现实现**一致**(例如任一失败则整体失败);若需与现网「严格顺序失败」完全一致,须在实现中定义完成顺序与异常聚合方式。
- **HTTP**:不变。 - **HTTP**:不变。
### 3.3 `AcsPassRuleServiceImpl.listFloor`(每层 `acsPersonService.page` 仅取 `totalRows` ### 3.3 `AcsPassRuleServiceImpl.listFloor`(每层 `acsPersonService.page` 仅取 `totalRows`
- **约束**`AcsPersonQueryParam` 单 zone;图库侧无「多楼层一次返回人数」API 时,不能靠单次 RPC 消除 N。 - **约束**`AcsPersonQueryParam` 单 zone;图库侧无「多楼层一次返回人数」API 时,不能靠单次 RPC 消除 N。
- **禁止**:用本地 `countPersonIdByZoneId` 等 SQL 人数**直接替代** `imageStorePersonService.page``totalRows`(与 `PersonRuleServiceImpl.page` 路径下的 label/org/del 等过滤**不等价**),除非完成**逐层对账**并书面验收。 - **禁止**:用本地 `countPersonIdByZoneId` 等 SQL 人数**直接替代** `imageStorePersonService.page``totalRows`(与 `PersonRuleServiceImpl.page` 路径下的 label/org/del 等过滤**不等价**),除非完成**逐层对账**并书面验收。
- **约定**:在契约不扩展时,**最优为按楼层有界并行** `page`(如 `rowsOfPage=1` 仅取总数),按树顺序**合并结果**,保证响应列表顺序与字段不变。 - **约定**:在契约不扩展时,**最优为按楼层有界并行** `page`(如 `rowsOfPage=1` 仅取总数),按树顺序**合并结果**,保证响应列表顺序与字段不变。
- **HTTP**:不变。 - **HTTP**:不变。
### 3.4 `AcsPassRuleServiceImpl.addImageStore`(循环 `bindDeviceAndImageStore` ### 3.4 `AcsPassRuleServiceImpl.addImageStore`(循环 `bindDeviceAndImageStore`
- **约定**:无批量 bind 时,**有界并行 bind**;异常时的回滚(如删除已建图库)须与现逻辑一致,并注意并行下与顺序相关的竞态。 - **约定**:无批量 bind 时,**有界并行 bind**;异常时的回滚(如删除已建图库)须与现逻辑一致,并注意并行下与顺序相关的竞态。
- **HTTP**:不变。 - **HTTP**:不变。
### 3.5 `AcsDeviceTaskServiceImpl` 等按楼层调用 `personRuleService.delete` / `imageRuleRefService.delete` ### 3.5 `AcsDeviceTaskServiceImpl` 等按楼层调用 `personRuleService.delete` / `imageRuleRefService.delete`
- **约定**:优先受益于 **3.1** 的合并刷新;若仍为每层 `delete`,可叠加**楼层级有界并行**,前提是错误语义与资源侧限流可接受。 - **约定**:优先受益于 **3.1** 的合并刷新;若仍为每层 `delete`,可叠加**楼层级有界并行**,前提是错误语义与资源侧限流可接受。
--- ---
## 4. 实施优先级(契约不扩展时的 ROI) ## 4. 实施优先级(契约不扩展时的 ROI)
| 优先级 | 项 | 说明 | | 优先级 | 项 | 说明 |
|--------|-----|------| |--------|-----|------|
| P0 | 3.1 合并 `updateGroupPersonRef` + 返回值校验 | 收益大、变更面相对集中;依赖远端语义确认。 | | P0 | 3.1 合并 `updateGroupPersonRef` + 返回值校验 | 收益大、变更面相对集中;依赖远端语义确认。 |
| P1 | 3.3 / 3.2 / 3.4 / 3.5 有界并行 | 不改 DTO/URL,主要降低墙钟时间;注意线程池生命周期与超时。 | | P1 | 3.3 / 3.2 / 3.4 / 3.5 有界并行 | 不改 DTO/URL,主要降低墙钟时间;注意线程池生命周期与超时。 |
| 远期 | Feign 批量/聚合接口 | 契约可扩展时,再评估批量 delete、多 zone 统计等结构性优化。 | | 远期 | Feign 批量/聚合接口 | 契约可扩展时,再评估批量 delete、多 zone 统计等结构性优化。 |
--- ---
## 5. 变更与评审 ## 5. 变更与评审
- 任何偏离本约定(例如采用本地 count 替代 `totalRows`)须在 PR/变更说明中**单列风险**并附对账或测试证据。 - 任何偏离本约定(例如采用本地 count 替代 `totalRows`)须在 PR/变更说明中**单列风险**并附对账或测试证据。
- 与 intelligent 团队对齐「`updateGroupPersonRef` 语义」后,建议在本文 **3.1** 节追加**结论日期与对接人**一行,便于审计。 - 与 intelligent 团队对齐「`updateGroupPersonRef` 语义」后,建议在本文 **3.1** 节追加**结论日期与对接人**一行,便于审计。
--- ---
## 6. 相关代码锚点(便于检索) ## 6. 相关代码锚点(便于检索)
| 场景 | 典型类/方法 | | 场景 | 典型类/方法 |
|------|-------------| |------|-------------|
| 人员分页与规则 | `AcsPersonServiceImpl#page``getRuleListByZoneId` | | 人员分页与规则 | `AcsPersonServiceImpl#page``getRuleListByZoneId` |
| 楼层人数 | `AcsPassRuleServiceImpl#listFloor` | | 楼层人数 | `AcsPassRuleServiceImpl#listFloor` |
| 规则与图库口径对照 | `PersonRuleServiceImpl#page` | | 规则与图库口径对照 | `PersonRuleServiceImpl#page` |
| 规则引用删除 | `ImageRuleRefServiceImpl#delete` | | 规则引用删除 | `ImageRuleRefServiceImpl#delete` |
| 人员删除与图库 | `AcsPersonServiceImpl#delete` | | 人员删除与图库 | `AcsPersonServiceImpl#delete` |
--- ---
## 7. 走查任务索引(子任务与状态) ## 7. 走查任务索引(子任务与状态)
**[对外接口不变-走查任务与状态.md](对外接口不变-走查任务与状态.md)**:按 §3.1~§3.5 与当前代码对齐,列出 **RPC 次数上界**、**下一可实施修正**、**在契约不扩展前提下不可做或须先确认** 的项。 **[对外接口不变-走查任务与状态.md](对外接口不变-走查任务与状态.md)**:按 §3.1~§3.5 与当前代码对齐,列出 **RPC 次数上界**、**下一可实施修正**、**在契约不扩展前提下不可做或须先确认** 的项。
+3 -3
View File
@@ -28,8 +28,8 @@ java -version # 应显示 1.8.x
6. **`maven-cloudwalk-cloud`**`mvn -DskipTests clean install` — 提供 **`cloudwalk-common-event`**、**`cloudwalk-common-service`** 等(**`maven-cwos-device-authentication`** 依赖 **`cloudwalk-common-service`**,故 **aks / device-authentication 须排在本步之后**)。 6. **`maven-cloudwalk-cloud`**`mvn -DskipTests clean install` — 提供 **`cloudwalk-common-event`**、**`cloudwalk-common-service`** 等(**`maven-cwos-device-authentication`** 依赖 **`cloudwalk-common-service`**,故 **aks / device-authentication 须排在本步之后**)。
7. **`maven-cwos-common-aks`**`mvn -DskipTests clean install` — 自 **`反1/cwos-common-aks-interface-1.0.0-SNAPSHOT.jar.src.zip`** 提供 **`cn.cloudwalk.cloud:cwos-common-aks:1.0.0-SNAPSHOT`** 与 **`cwos-common-aks-interface`**(依赖第 2 步 **`cloudwalk-common-result`**)。 7. **`maven-cwos-common-aks`**`mvn -DskipTests clean install` — 自 **`反1/cwos-common-aks-interface-1.0.0-SNAPSHOT.jar.src.zip`** 提供 **`cn.cloudwalk.cloud:cwos-common-aks:1.0.0-SNAPSHOT`** 与 **`cwos-common-aks-interface`**(依赖第 2 步 **`cloudwalk-common-result`**)。
8. **`maven-cwos-device-authentication`**`mvn -DskipTests clean install` — 自 **`反1/cwos-device-authentication-interface-1.0.0-SNAPSHOT.jar.src.zip`** 提供 **`cn.cloudwalk.cloud:cwos-device-authentication:1.0.0-SNAPSHOT`** 与 **`cwos-device-authentication-interface`**(依赖第 1、3、6、7 步及 **`cwos-common-aks-interface`**)。 8. **`maven-cwos-device-authentication`**`mvn -DskipTests clean install` — 自 **`反1/cwos-device-authentication-interface-1.0.0-SNAPSHOT.jar.src.zip`** 提供 **`cn.cloudwalk.cloud:cwos-device-authentication:1.0.0-SNAPSHOT`** 与 **`cwos-device-authentication-interface`**(依赖第 1、3、6、7 步及 **`cwos-common-aks-interface`**)。
9. **`maven-intelligent-cwoscomponent`**`mvn -DskipTests clean install`(依赖第 1 步的 **protocol-entity** 与 cloudwalk 模块) 9. **`maven-intelligent-cwoscomponent`**若需维护 **3.0.0-xinghewan** 源码线:`mvn -DskipTests clean install`(依赖第 1 步的 **protocol-entity** 与 cloudwalk 模块)。**仅编电梯**且上线口径为 **`intelligent-cwoscomponent` 2.9.2`cw_lib`)** 时,**不要**依赖本步;请使用 `maven-cw-elevator-application/scripts/build_nexus_only.sh`(从 **`cw_lib`** 安装 2.9.2 JAR+POM + 父 POM 桩)。
10. **`maven-cw-elevator-application`**`mvn -DskipTests clean install``service` 若仍失败,多为其它私服构件或业务源码问题,见下文) 10. **`maven-cw-elevator-application`**`mvn -DskipTests clean install`或用 **`build_nexus_only.sh`** 隔离本地库 + Nexus`service` 若仍失败,多为其它私服构件或业务源码问题,见下文)
11. 其余:`maven-ninca-crk``maven-ninca-qk-alarm` 按需单独编译。 11. 其余:`maven-ninca-crk``maven-ninca-qk-alarm` 按需单独编译。
单工程示例: 单工程示例:
@@ -156,7 +156,7 @@ mvn -pl cw-elevator-application-service -am -DskipTests clean compile
| `cwos-component-resource:pom:1.0.0-SNAPSHOT` absent | 接口包 **`cwos-component-resource-interface`** 所引用的 **父 POM** 未在私服。 | | `cwos-component-resource:pom:1.0.0-SNAPSHOT` absent | 接口包 **`cwos-component-resource-interface`** 所引用的 **父 POM** 未在私服。 |
| `cwos-portal:pom:1.0.0-SNAPSHOT` absent | 同上,**`cwos-portal-interface`** 的父工程未发布。 | | `cwos-portal:pom:1.0.0-SNAPSHOT` absent | 同上,**`cwos-portal-interface`** 的父工程未发布。 |
| `cloudwalk-intelligent-davinci-manager:pom:1.1.7-SNAPSHOT` absent | **`davinci-manager-storage`** 的父 POM 未在私服(或曾失败被缓存)。 | | `cloudwalk-intelligent-davinci-manager:pom:1.1.7-SNAPSHOT` absent | **`davinci-manager-storage`** 的父 POM 未在私服(或曾失败被缓存)。 |
| `The POM for ... intelligent-cwoscomponent-rest:jar:3.0.0-xinghewan is missing` | 因 **6.1** 失败,**interface/rest 未 install** 到本地仓库,电梯解析 `intelligent-cwoscomponent-rest` 时只能报「无 POM/无依赖信息」。 | | `The POM for ... intelligent-cwoscomponent-rest:jar:2.9.2-xinghewan is missing` | **interface/rest** 未写入本地仓库(未从 `cw_lib`/`build_nexus_only.sh` 安装,或 **6.1** 仍失败),电梯解析 `intelligent-cwoscomponent-rest` 时只能报「无 POM/无依赖信息」。
**结论**`service` 的失败 **不是** 电梯业务源码语法问题,而是 **Maven 依赖图不完整**(私服缺件 + 上一步 intelligent 未成功)。 **结论**`service` 的失败 **不是** 电梯业务源码语法问题,而是 **Maven 依赖图不完整**(私服缺件 + 上一步 intelligent 未成功)。
@@ -4,6 +4,7 @@ import cn.cloudwalk.intelligent.davinci.common.result.DavinciResult;
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartFinishDTO; import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartFinishDTO;
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitDTO; import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitDTO;
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitResultDTO; import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitResultDTO;
import feign.Response;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@@ -13,14 +14,17 @@ import org.springframework.web.multipart.MultipartFile;
public interface FilePartFeign { public interface FilePartFeign {
@RequestMapping(value = { "/init" }, method = { RequestMethod.POST }) @RequestMapping(value = { "/part/init" }, method = { RequestMethod.POST })
DavinciResult<PartInitResultDTO> init(@RequestBody PartInitDTO paramPartInitDTO); DavinciResult<PartInitResultDTO> init(@RequestBody PartInitDTO paramPartInitDTO);
@RequestMapping(value = { "/append" }, method = { RequestMethod.POST }, consumes = { "multipart/form-data" }) @RequestMapping(value = { "/part/append" }, method = { RequestMethod.POST }, consumes = { "multipart/form-data" })
DavinciResult<PartInitResultDTO> append(@RequestParam("filePath") String paramString1, DavinciResult<PartInitResultDTO> append(@RequestParam("filePath") String paramString1,
@RequestParam("partNumber") Integer paramInteger, @RequestParam("uploadId") String paramString2, @RequestParam("partNumber") Integer paramInteger, @RequestParam("uploadId") String paramString2,
@RequestPart("file") MultipartFile paramMultipartFile); @RequestPart("file") MultipartFile paramMultipartFile);
@RequestMapping(value = { "/finish" }, method = { RequestMethod.POST }) @RequestMapping(value = { "/part/finish" }, method = { RequestMethod.POST })
DavinciResult<String> finish(@RequestBody PartFinishDTO paramPartFinishDTO); DavinciResult<String> finish(@RequestBody PartFinishDTO paramPartFinishDTO);
@RequestMapping(value = { "/download" }, method = { RequestMethod.GET })
Response bigFileDownload(@RequestParam("path") String path);
} }
@@ -4,6 +4,7 @@ import cn.cloudwalk.intelligent.davinci.common.exception.DavinciServiceException
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartFinishDTO; import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartFinishDTO;
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitDTO; import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitDTO;
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitResultDTO; import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitResultDTO;
import java.io.InputStream;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
public interface FilePartManager { public interface FilePartManager {
@@ -14,4 +15,6 @@ public interface FilePartManager {
MultipartFile paramMultipartFile) throws DavinciServiceException; MultipartFile paramMultipartFile) throws DavinciServiceException;
String finish(PartFinishDTO paramPartFinishDTO) throws DavinciServiceException; String finish(PartFinishDTO paramPartFinishDTO) throws DavinciServiceException;
InputStream bigFileDownload(String paramString) throws DavinciServiceException;
} }
@@ -9,28 +9,22 @@ import cn.cloudwalk.intelligent.davinci.storage.feign.FilePartFeign;
import cn.cloudwalk.intelligent.davinci.storage.manager.FilePartManager; import cn.cloudwalk.intelligent.davinci.storage.manager.FilePartManager;
import feign.Client; import feign.Client;
import feign.Feign; import feign.Feign;
import feign.Response;
import feign.codec.Decoder; import feign.codec.Decoder;
import feign.codec.Encoder; import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder; import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException;
import org.springframework.beans.factory.annotation.Value; import java.io.InputStream;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.cloud.openfeign.support.SpringMvcContract; import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@Component
@Import({ FeignClientsConfiguration.class })
public class FilePartManagerImpl implements FilePartManager { public class FilePartManagerImpl implements FilePartManager {
private FilePartFeign filePartFeign; private FilePartFeign filePartFeign;
private FilePartFeign filePartRestFeign; private FilePartFeign filePartRestFeign;
@Autowired public FilePartManagerImpl(String serviceName, Decoder decoder, Encoder encoder, Client client) {
public FilePartManagerImpl(@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName, String url = "http://" + serviceName + "/portal/file";
Decoder decoder, Encoder encoder, Client client) {
String url = "http://" + serviceName + "/portal/file/part";
this.filePartFeign = Feign.builder().client(client).decode404().encoder(new SpringFormEncoder()) this.filePartFeign = Feign.builder().client(client).decode404().encoder(new SpringFormEncoder())
.decoder(decoder).contract(new SpringMvcContract()).target(FilePartFeign.class, url); .decoder(decoder).contract(new SpringMvcContract()).target(FilePartFeign.class, url);
@@ -66,4 +60,17 @@ public class FilePartManagerImpl implements FilePartManager {
} }
throw new DavinciServiceException(result.getCode(), result.getMessage()); throw new DavinciServiceException(result.getCode(), result.getMessage());
} }
@Override
public InputStream bigFileDownload(String path) throws DavinciServiceException {
try {
Response response = this.filePartFeign.bigFileDownload(path);
if (response.body() == null) {
return null;
}
return response.body().asInputStream();
} catch (IOException e) {
throw new DavinciServiceException("", "调用Davinci-portal服务,获取大文件流接口异常");
}
}
} }
@@ -25,15 +25,12 @@ import java.util.Locale;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.cloud.openfeign.support.SpringMvcContract; import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@Component @Component
@Import({ FeignClientsConfiguration.class })
public class FileStorageManagerImpl implements FileStorageManager { public class FileStorageManagerImpl implements FileStorageManager {
private FileManagerFeign fileManagerFeign; private FileManagerFeign fileManagerFeign;
@@ -6,17 +6,20 @@ import cn.cloudwalk.serial.code.GeneralSerialCode;
import cn.cloudwalk.serial.code.MacGeneralSerial; import cn.cloudwalk.serial.code.MacGeneralSerial;
import cn.cloudwalk.serial.code.RedisGeneralCode; import cn.cloudwalk.serial.code.RedisGeneralCode;
import cn.cloudwalk.serial.redis.CloudwalkRedisService; import cn.cloudwalk.serial.redis.CloudwalkRedisService;
import cn.cloudwalk.serial.strategy.ServerIdStrategyBeanConfig;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@Configuration @Configuration
@EnableConfigurationProperties({ CloudwalkSerialProperties.class }) @Import({ServerIdStrategyBeanConfig.class, CloudwalkSnowflakeConfiguration.class})
@ConditionalOnProperty(prefix = "cloudwalk.serial", value = { "enabled" }, havingValue = "true", matchIfMissing = true) @EnableConfigurationProperties({CloudwalkSerialProperties.class})
@ConditionalOnProperty(prefix = "cloudwalk.serial", value = {"enabled"}, havingValue = "true", matchIfMissing = true)
public class CloudwalkSerialAutoConfiguration { public class CloudwalkSerialAutoConfiguration {
@Autowired(required = false) @Autowired(required = false)
private RedisTemplate<String, String> redisTemplate; private RedisTemplate<String, String> redisTemplate;
@@ -0,0 +1,3 @@
# Spring Boot 2.1:注册序列号与 Snowflake 相关自动配置(此前缺失导致 AbstractGeneralCode 等 Bean 未创建)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.cloudwalk.serial.autoconfig.serial.CloudwalkSerialAutoConfiguration
+3
View File
@@ -1,2 +1,5 @@
# 发布目录中的可执行 JAR 体积大,默认不纳入 Git;说明与清单可单独跟踪。 # 发布目录中的可执行 JAR 体积大,默认不纳入 Git;说明与清单可单独跟踪。
releases/**/*.jar releases/**/*.jar
# scripts/build_nexus_only.sh 使用的隔离 Maven 本地仓库(仅 Nexus 依赖缓存)
.m2-elevator-nexus-only/
@@ -58,6 +58,15 @@
<groupId>cn.cloudwalk.cloud</groupId> <groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cloudwalk-common-web</artifactId> <artifactId>cloudwalk-common-web</artifactId>
</dependency> </dependency>
<!-- cloudwalk-common-web 在部分私服 POM 上传递依赖不完整;隔离仓库构建时显式补齐编译 classpath -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.google.zxing</groupId> <groupId>com.google.zxing</groupId>
<artifactId>core</artifactId> <artifactId>core</artifactId>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>
</configuration>
@@ -56,6 +56,10 @@
<groupId>cn.cloudwalk.cloud</groupId> <groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cloudwalk-common-event</artifactId> <artifactId>cloudwalk-common-event</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cwos-sdk-event</artifactId>
</dependency>
<dependency> <dependency>
<groupId>cn.cloudwalk.elevator</groupId> <groupId>cn.cloudwalk.elevator</groupId>
<artifactId>cw-elevator-application-data</artifactId> <artifactId>cw-elevator-application-data</artifactId>
@@ -73,6 +77,14 @@
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId> <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency> </dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -0,0 +1,15 @@
package cn.cloudwalk.elevator;
import cn.cloudwalk.cloud.context.CloudwalkSessionContextHolder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/** 未扫描 {@code cn.cloudwalk.web} 时,等价于 LocaleConfiguration 中的 SessionHolder Bean。 */
@Configuration
public class CloudwalkSessionHolderConfiguration {
@Bean
public CloudwalkSessionContextHolder cloudwalkSessionContextHolder() {
return new CloudwalkSessionContextHolder();
}
}
@@ -4,7 +4,7 @@ import cn.cloudwalk.cloud.exception.ServiceException;
import cn.cloudwalk.cloud.result.CloudwalkResult; import cn.cloudwalk.cloud.result.CloudwalkResult;
import cn.cloudwalk.elevator.mqtt.fallback.MqttFeignClientFallback; import cn.cloudwalk.elevator.mqtt.fallback.MqttFeignClientFallback;
import cn.cloudwalk.elevator.mqtt.param.MqttSendMessageParam; import cn.cloudwalk.elevator.mqtt.param.MqttSendMessageParam;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@@ -5,7 +5,7 @@ import cn.cloudwalk.cloud.result.CloudwalkResult;
import cn.cloudwalk.elevator.visitor.fallback.VisitorFeignClientFallback; import cn.cloudwalk.elevator.visitor.fallback.VisitorFeignClientFallback;
import cn.cloudwalk.elevator.visitor.param.VisitorRecordQueryParam; import cn.cloudwalk.elevator.visitor.param.VisitorRecordQueryParam;
import cn.cloudwalk.elevator.visitor.result.VisitorQueryResult; import cn.cloudwalk.elevator.visitor.result.VisitorQueryResult;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@@ -9,7 +9,7 @@ import cn.cloudwalk.elevator.zone.param.ZoneQueryParam;
import cn.cloudwalk.elevator.zone.result.ZoneResult; import cn.cloudwalk.elevator.zone.result.ZoneResult;
import cn.cloudwalk.elevator.zone.result.ZoneTreeResult; import cn.cloudwalk.elevator.zone.result.ZoneTreeResult;
import java.util.List; import java.util.List;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@@ -6,7 +6,7 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
@@ -14,12 +14,29 @@ import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync @EnableAsync
@EnableCaching @EnableCaching
@EnableAspectJAutoProxy(exposeProxy = true) @EnableAspectJAutoProxy(exposeProxy = true)
@EnableFeignClients(basePackages = "cn.cloudwalk.elevator") @EnableFeignClients(basePackages = {
@MapperScan("cn.cloudwalk.elevator") "cn.cloudwalk.elevator",
@SpringBootApplication(exclude = {PageHelperAutoConfiguration.class}) "cn.cloudwalk.rest.cwoscomponent",
"cn.cloudwalk.cwos.client.resource"
})
@MapperScan({
"cn.cloudwalk.elevator.record.mapper",
"cn.cloudwalk.elevator.device.mapper",
"cn.cloudwalk.elevator.passrule.mapper",
"cn.cloudwalk.elevator.person.mapper",
"cn.cloudwalk.elevator.codeElevatorArea.mapper"
})
@SpringBootApplication(
exclude = {PageHelperAutoConfiguration.class},
scanBasePackages = {
"cn.cloudwalk.elevator",
"cn.cloudwalk.rest.cwoscomponent",
"cn.cloudwalk.serial",
"cn.cloudwalk.cwos.client.resource"
})
public class ElevatorApplication { public class ElevatorApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(ElevatorApplication.class, args); SpringApplication.run(ElevatorApplication.class, args);
} }
} }
@@ -0,0 +1,37 @@
package cn.cloudwalk.elevator.config;
import cn.cloudwalk.elevator.integration.davinci.OpenFeignFileStorageManager;
import cn.cloudwalk.intelligent.davinci.storage.manager.FilePartManager;
import cn.cloudwalk.intelligent.davinci.storage.manager.FileStorageManager;
import cn.cloudwalk.intelligent.davinci.storage.manager.impl.FilePartManagerImpl;
import feign.Client;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.feign.FeignClientsConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(FeignClientsConfiguration.class)
public class DavinciStorageBeansConfiguration {
@Bean
public FileStorageManager fileStorageManager(
@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName,
Decoder decoder,
Encoder encoder,
Client client) {
return new OpenFeignFileStorageManager(serviceName, decoder, encoder, client);
}
@Bean
public FilePartManager filePartManager(
@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName,
Decoder decoder,
Encoder encoder,
Client client) {
return new FilePartManagerImpl(serviceName, decoder, encoder, client);
}
}
@@ -0,0 +1,228 @@
package cn.cloudwalk.elevator.integration.davinci;
import cn.cloudwalk.intelligent.davinci.common.exception.DavinciServiceException;
import cn.cloudwalk.intelligent.davinci.common.result.DavinciResult;
import cn.cloudwalk.intelligent.davinci.storage.bean.file.dto.FileRemoveDTO;
import cn.cloudwalk.intelligent.davinci.storage.feign.FileManagerFeign;
import cn.cloudwalk.intelligent.davinci.storage.feign.OuterCallFeignClient;
import cn.cloudwalk.intelligent.davinci.storage.manager.FileStorageManager;
import feign.Client;
import feign.Feign;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import feign.okhttp.OkHttpClient;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Locale;
import org.apache.commons.io.IOUtils;
import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/**
* 与 davinci-manager-storage 中逻辑一致,但固定使用 {@link SpringMvcContract}OpenFeign)。
* 避免依赖 Nexus 仍带 Netflix 引用的旧 {@code FileStorageManagerImpl} 字节码导致 NoClassDefFoundError。
*/
public class OpenFeignFileStorageManager implements FileStorageManager {
private final FileManagerFeign fileManagerFeign;
private final FileManagerFeign fileManagerRestFeign;
public OpenFeignFileStorageManager(String serviceName, Decoder decoder, Encoder encoder, Client client) {
String url = "http://" + serviceName + "/portal/fileManager";
this.fileManagerFeign = Feign.builder().client(client).decode404().encoder(new SpringFormEncoder())
.decoder(decoder).contract(new SpringMvcContract()).target(FileManagerFeign.class, url);
this.fileManagerRestFeign = Feign.builder().client(client).decode404().encoder(encoder).decoder(decoder)
.contract(new SpringMvcContract()).target(FileManagerFeign.class, url);
}
static void assertSafeHttpUrl(String urlString) throws DavinciServiceException {
if (StringUtils.isEmpty(urlString)) {
throw new DavinciServiceException("INVALID_URL", "URL 为空");
}
URI uri;
try {
uri = new URI(urlString);
} catch (URISyntaxException e) {
throw new DavinciServiceException("INVALID_URL", "URL 非法");
}
String scheme = uri.getScheme();
if (scheme == null || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
throw new DavinciServiceException("INVALID_URL", "仅允许 http 或 https 协议");
}
String host = uri.getHost();
if (StringUtils.isEmpty(host)) {
throw new DavinciServiceException("INVALID_URL", "缺少主机名");
}
String lowerHost = host.toLowerCase(Locale.ROOT);
if ("localhost".equals(lowerHost) || lowerHost.endsWith(".local")) {
throw new DavinciServiceException("INVALID_URL", "禁止访问该主机");
}
if ("metadata.google.internal".equalsIgnoreCase(host)) {
throw new DavinciServiceException("INVALID_URL", "禁止访问该主机");
}
try {
InetAddress[] all = InetAddress.getAllByName(host);
for (InetAddress addr : all) {
if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress()
|| addr.isSiteLocalAddress() || addr.isMulticastAddress()) {
throw new DavinciServiceException("INVALID_URL", "禁止访问内网或保留地址");
}
}
} catch (UnknownHostException e) {
throw new DavinciServiceException("INVALID_URL", "无法解析主机");
}
}
private static void requireDavinciResult(DavinciResult<?> result, String op) throws DavinciServiceException {
if (result == null) {
throw new DavinciServiceException("NULL_RESULT", "Davinci-portal 返回空结果: " + op);
}
}
private static InputStream attachResponseClose(InputStream bodyStream, Response response) {
return new FilterInputStream(bodyStream) {
@Override
public void close() throws IOException {
try {
super.close();
} finally {
response.close();
}
}
};
}
@Override
public String fileUpload(MultipartFile file) throws DavinciServiceException {
DavinciResult<String> result = this.fileManagerFeign.fileUpload(file);
requireDavinciResult(result, "fileUpload");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public String fileUpload(String moduleCategory, MultipartFile file) throws DavinciServiceException {
DavinciResult<String> result = this.fileManagerFeign.fileUpload(moduleCategory, file);
requireDavinciResult(result, "fileUpload(module)");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public String bigFileUpload(MultipartFile file) throws DavinciServiceException {
DavinciResult<String> result = this.fileManagerFeign.bigFileUpload(file);
requireDavinciResult(result, "bigFileUpload");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public String bigFileUpload(String moduleCategory, MultipartFile file) throws DavinciServiceException {
DavinciResult<String> result = this.fileManagerFeign.bigFileUpload(moduleCategory, file);
requireDavinciResult(result, "bigFileUpload(module)");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public byte[] fileDownload(String path) throws DavinciServiceException {
try (Response response = this.fileManagerFeign.fileDownload(path)) {
if (response == null) {
return null;
}
if (response.body() == null) {
return null;
}
try (InputStream inputStream = response.body().asInputStream()) {
return IOUtils.toByteArray(inputStream);
}
} catch (IOException e) {
throw new DavinciServiceException("FILE_DOWNLOAD_IO", "调用Davinci-portal服务,获取文件流接口异常");
}
}
@Override
public InputStream fileDownloadStream(String path) throws DavinciServiceException {
Response response = this.fileManagerFeign.fileDownload(path);
try {
if (response == null) {
return null;
}
if (response.body() == null) {
response.close();
return null;
}
return attachResponseClose(response.body().asInputStream(), response);
} catch (IOException e) {
if (response != null) {
response.close();
}
throw new DavinciServiceException("FILE_DOWNLOAD_IO", "调用Davinci-portal服务,获取文件流接口异常");
}
}
@Override
public String getFileBase64(String path) throws DavinciServiceException {
if (StringUtils.isEmpty(path)) {
return "";
}
DavinciResult<String> result = this.fileManagerFeign.getFileData(path);
requireDavinciResult(result, "getFileData");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public List<String> remove(FileRemoveDTO dto) throws DavinciServiceException {
DavinciResult<List<String>> result = this.fileManagerRestFeign.remove(dto);
requireDavinciResult(result, "remove");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public InputStream fileDownLoadWithAbsoluteUrl(String url) throws DavinciServiceException {
assertSafeHttpUrl(url);
OuterCallFeignClient feignClient = Feign.builder().client(new OkHttpClient()).target(OuterCallFeignClient.class,
url);
Response response;
try {
response = feignClient.downLoad();
} catch (RuntimeException e) {
throw new DavinciServiceException("OUTER_DOWNLOAD", "拉取远程文件失败");
}
try {
if (response.body() == null) {
response.close();
return null;
}
return attachResponseClose(response.body().asInputStream(), response);
} catch (IOException e) {
response.close();
throw new DavinciServiceException("OUTER_DOWNLOAD", "读取远程文件流失败");
}
}
}
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true">
<property name="CONTEXT_NAME" value="api.1.0.0"/>
<contextName>${CONTEXT_NAME}</contextName>
<springProperty scope="context" name="fileName" source="logging.file" defaultValue="default"/>
<!--myibatis log configure-->
<logger name="com.apache.ibatis" level="DEBUG"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 控制台输出 -->
<appender name="S" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{50}:%line - %msg%n</pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="R" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_PATH}/${fileName}.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_PATH}/${fileName}.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>7</MaxHistory>
<!--日志文件大小 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{50}:%line - %msg%n</pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
<!--日志文件最大的大小-->
<!-- <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> -->
<!-- <MaxFileSize>10MB</MaxFileSize> -->
<!-- </triggeringPolicy> -->
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="S"/>
<appender-ref ref="R"/>
</root>
</configuration>
@@ -20,7 +20,7 @@
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -0,0 +1,5 @@
# 体积大,由 sync-jars.sh 生成;属性文件可版本管理
*.jar
# 运行/调试产生的日志不纳入版本库
**/logs/*.log
@@ -0,0 +1,92 @@
# 电梯应用双版本同路径部署
每个发行目录内 **JAR**、**`application.properties`**、**`bootstrap.properties`**Consul)与 **`redis-override.properties`** 位于**同一目录**。`run.sh` 使用:
```bash
--spring.config.location=file:./application.properties,file:./redis-override.properties
```
(说明见下文:Boot 1.5 下 jar 内 `classpath:/application.properties` 往往最后生效,仅靠外置文件盖不住密码。)
## 目录
| 目录 | JAR | 端口(见配置首行) |
|------|-----|-------------------|
| `v1-legacy/` | `cw-elevator-application-V1.0.0.20211103.jar` | **18080** |
| `v2-maven/` | `cw-elevator-application-2.0.0.jar` | **18081** |
属性文件由历史包 `cw-elevator-application-V1.0.0.20211103/application.properties` 复制而来,仅在各自文件头部增加了 **注释****`server.port`**,便于双实例并行(对拍 / API 套件)。
## 一次性同步 JAR
**`deploy/`** 下执行(需已存在 V1 原始 JAR;V2 优先用 `releases/v2.0.0/`,否则用 `cw-elevator-application-starter/target/`):
```bash
./sync-jars.sh
```
## 启动(两个终端)
```bash
cd v1-legacy && ./run.sh
```
```bash
cd v2-maven && ./run.sh
```
探活示例:`curl -s http://127.0.0.1:18080/actuator/health``18081`
## Consul`192.168.3.12` Docker
本仓库在 **`v1-legacy/bootstrap.properties`**、**`v2-maven/bootstrap.properties`** 中写入:
- `spring.cloud.consul.host=192.168.3.12`
- `spring.cloud.consul.port=8500`
用于覆盖 fat-jar 内 **`10.128.161.95:8500`**,与 **`deploy/consul-docker`**`hashicorp/consul:1.22`)对齐。在同一目录执行 `./run.sh` 时,Spring Cloud 会加载上述 **`bootstrap.properties`**。
验证 Consul`curl -s http://192.168.3.12:8500/v1/status/leader`。浏览器打开 **`http://192.168.3.12:8500`** 可看 UI。
**说明**jar 内 **Dubbo / ZooKeeper** 仍可能指向旧 IP(如 `10.128.161.95:2181`);若启动报 ZK 连接失败,需在 **`application.properties`** 中另行改 Dubbo 注册中心(本次仅处理 Consul)。
### Feign `ninca-crk-std`Ribbon / Consul
若日志出现 **`Load balancer does not have available server for client: ninca-crk-std`**,表示 **Consul 里没有名为 `ninca-crk-std` 的注册实例**,而 Feign 默认用 **Consul 服务发现**拉实例列表。
**做法**:① 在目标环境启动 **`ninca-crk-std`(访客标准服务)** 并注册到同一 Consul;或 ② 在 **`application.properties`** 中使用 **Ribbon 静态列表**(已增加 `ninca-crk-std.ribbon.NIWSServerListClassName``listOfServers`,默认与 `ninca-crk-std.ip` 一致),把 **`listOfServers`** 改成你实际可访问的 **`主机:端口`**。
## Redis 与 `SPRING_APPLICATION_JSON`
fat JAR 的 `classpath:/application.properties` 会带内网旧 **host****password**;在 Spring Boot 1.5 下,同目录的 `application.properties` / `redis-override.properties` 往往**压不过** jar 里同文件(见上节说明)。
**当前做法**`deploy/merge-redis-json.sh` 读取各目录下的 **`redis-override.properties`**,生成一行 **`SPRING_APPLICATION_JSON`**(包含 `spring.redis.host` / `port` / `password`),由 `run.sh` **`export`** 后再启动 JVM,优先级高于打包配置。
- 默认已指向 **`redis-override.properties`** 中的地址与口令(若你的环境不一致,直接改该文件)。
- **临时覆盖口令**`SPRING_REDIS_PASSWORD='别的密码' ./run.sh`(若设为**空字符串**表示使用无密码 Redis)。
- 需要 **python3**
- **勿把生产口令提交到公开仓库**;团队协作时口令宜走密钥管理,`redis-override.properties` 仅作本机示例。
## 修改配置
直接编辑对应目录下的 **`application.properties`**(与 JAR 同路径),重启进程生效。
## JDK 版本(避免 `InaccessibleObjectException` / CGLIB
历史 JAR 面向 **JDK 8**。本仓库在 **`deploy/common-java.sh`** 顶部用变量 **`DEPLOY_JDK8`** 写死默认路径(当前为 **`/usr/lib/jvm/java-8-openjdk-amd64`**)。换机器请只改这一处,或通过环境变量临时覆盖:
`DEPLOY_JDK8=/你的/jdk8 ./run.sh`
**默认**`run.sh` 使用 **`DEPLOY_JDK8`****不跟随** Conda 的 `JAVA_HOME`
- 坚持用当前 Shell 里的 Java(如 Conda JDK17):
`ELEVATOR_USE_ENV_JAVA=1 ./run.sh`
非 JDK8 时会自动追加 `--add-opens=...`
- 额外 JVM
`ELEVATOR_JAVA_OPTS="-Xmx512m" ./run.sh`
## Shell 脚本换行(若出现 `bash\r`
在 Windows 或某些编辑器下保存成 **CRLF** 会导致 `#!/usr/bin/env bash\r`。仓库根已有 **`.editorconfig`** 约束 `*.sh` 使用 **LF**;若再出现可执行:
`find . -name '*.sh' -type f -exec sed -i 's/\r$//' {} +`(在 `反编译` 根目录)
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# shellcheck shell=bash
# 由 v1-legacy/run.sh、v2-maven/run.sh sourceJAVA_HOME;非 JDK8 时追加 --add-opens。
#
# === 本机 JDK 8 安装根目录(含 bin/java);换机器只需改下行默认路径或通过环境变量覆盖 ===
: "${DEPLOY_JDK8:=/usr/lib/jvm/java-8-openjdk-amd64}"
_pick_java_home() {
if [[ "${ELEVATOR_USE_ENV_JAVA:-0}" == "1" ]] && [[ -n "${JAVA_HOME:-}" && -x "${JAVA_HOME}/bin/java" ]]; then
return 0
fi
if [[ -x "${DEPLOY_JDK8}/bin/java" ]]; then
export JAVA_HOME="${DEPLOY_JDK8}"
return 0
fi
for d in /usr/lib/jvm/java-8-openjdk-amd64 /usr/lib/jvm/java-1.8.0-openjdk; do
if [[ -x "$d/bin/java" ]]; then
export JAVA_HOME="$d"
return 0
fi
done
if [[ -n "${JAVA_HOME:-}" && -x "${JAVA_HOME}/bin/java" ]]; then
return 0
fi
export JAVA_HOME="${JAVA_HOME:-${DEPLOY_JDK8}}"
}
_jdk8_open_flags() {
local java="$1"
if "$java" -version 2>&1 | grep -qE 'version "1\.8\.'; then
echo ""
return
fi
echo "--add-opens=java.base/java.lang=ALL-UNNAMED"
echo "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
echo "--add-opens=java.base/java.util=ALL-UNNAMED"
echo "--add-opens=java.base/java.io=ALL-UNNAMED"
}
@@ -0,0 +1,31 @@
# Consul 单机 ServerDocker),镜像 hashicorp/consul:1.22
# 在存放本文件的目录执行: docker compose up -d
# UI / HTTP API: http://<宿主机IP>:8500 (例: 192.168.3.12:8500
# 应用侧: spring.cloud.consul.host=<宿主机IP> spring.cloud.consul.port=8500
services:
consul:
image: hashicorp/consul:1.22
container_name: consul
restart: unless-stopped
ports:
- "8500:8500" # HTTP API / UI
- "8300:8300" # Server RPC
- "8301:8301" # LAN Serf
- "8302:8302/udp"
- "8302:8302/tcp"
- "8600:8600/tcp"
- "8600:8600/udp" # DNS
command: >
agent -server -bootstrap-expect=1 -ui
-client=0.0.0.0
-bind=0.0.0.0
-data-dir=/consul/data
volumes:
- consul-data:/consul/data
# 若夸主机访问 RPC 异常,可在宿主机上改为显式 advertise(示例,按实际 IP 修改):
# command: >
# agent -server -bootstrap-expect=1 -ui -client=0.0.0.0 -bind=0.0.0.0
# -advertise=192.168.3.12 -data-dir=/consul/data
volumes:
consul-data:
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# 读取 redis-override.properties,输出一行 SPRING_APPLICATION_JSON(紧凑 JSON)。
# 若环境变量 SPRING_REDIS_PASSWORD 已设置(含空字符串),则覆盖文件中的 spring.redis.password。
# 用法: export SPRING_APPLICATION_JSON="$(./merge-redis-json.sh /path/to/redis-override.properties)"
set -euo pipefail
PROP="${1:?用法: merge-redis-json.sh <redis-override.properties>}"
if [[ ! -f "$PROP" ]]; then
echo "找不到文件: $PROP" >&2
exit 1
fi
python3 - "$PROP" <<'PY'
import json, os, pathlib, re, sys
prop = pathlib.Path(sys.argv[1])
lines = prop.read_text(encoding="utf-8")
def get(key):
pat = r"^" + re.escape(key) + r"\s*=\s*(.*?)\s*$"
m = re.search(pat, lines, re.MULTILINE)
return m.group(1).strip() if m else ""
host = get("spring.redis.host") or "127.0.0.1"
port_raw = get("spring.redis.port") or "6379"
try:
port = int(port_raw)
except ValueError:
port = 6379
if "SPRING_REDIS_PASSWORD" in os.environ:
pwd = os.environ["SPRING_REDIS_PASSWORD"]
else:
pwd = get("spring.redis.password")
payload = {"spring": {"redis": {"host": host, "port": port, "password": pwd}}}
print(json.dumps(payload, separators=(",", ":")))
PY
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# 将 V1 / V2 JAR 复制到与各目录 application.properties 同路径,便于 java -jar 启动。
set -euo pipefail
DEPLOY="$(cd "$(dirname "$0")" && pwd)"
MAVEN="$(cd "$DEPLOY/.." && pwd)"
REPO="$(cd "$MAVEN/.." && pwd)"
V1_SRC="${REPO}/cw-elevator-application-V1.0.0.20211103/cw-elevator-application-V1.0.0.20211103.jar"
V2_REL="${MAVEN}/releases/v2.0.0/cw-elevator-application-2.0.0.jar"
V2_TGT="${MAVEN}/cw-elevator-application-starter/target/cw-elevator-application-2.0.0.jar"
if [[ ! -f "$V1_SRC" ]]; then
echo "ERROR: 未找到 V1 JAR: $V1_SRC" >&2
exit 1
fi
# 优先 target:本地 mvn package 后应与 deploy 同步,避免 releases 里旧包盖住新构建。
V2_SRC=""
if [[ -f "$V2_TGT" ]]; then
V2_SRC="$V2_TGT"
elif [[ -f "$V2_REL" ]]; then
V2_SRC="$V2_REL"
else
echo "ERROR: 未找到 V2 JAR(请先 mvn package 或放入 releases:" >&2
echo " $V2_TGT$V2_REL" >&2
exit 1
fi
install -m0644 "$V1_SRC" "${DEPLOY}/v1-legacy/cw-elevator-application-V1.0.0.20211103.jar"
install -m0644 "$V2_SRC" "${DEPLOY}/v2-maven/cw-elevator-application-2.0.0.jar"
echo "OK: V1 -> deploy/v1-legacy/"
echo "OK: V2 -> deploy/v2-maven/"
ls -la "${DEPLOY}/v1-legacy/"*.jar "${DEPLOY}/v2-maven/"*.jar
@@ -0,0 +1,120 @@
# deploy/v1-legacy \uFF1A\u5386\u53F2\u5305 cw-elevator-application-V1.0.0.20211103.jar\uFF08\u540C\u76EE\u5F55\u542F\u52A8\uFF09
server.port=18080
spring.application.name=elevator-app
# OpenFeign 2.1.x\uFF1A\u591A\u4E2A @FeignClient \u5171\u540C name \u5360\u4F4D\u7B26\u65F6\u91CD\u590D\u6CE8\u518C FeignClientSpecification\uFF0C\u4E0E Spring \u9519\u8BEF\u63D0\u793A\u4E00\u81F4
spring.main.allow-bean-definition-overriding=true
# spring\u914D\u7F6E
spring.mvc.throw-exception-if-no-handler-found=true
spring.mvc.locale=zh_CN
# \u8D44\u6E90\u6587\u4EF6\u914D\u7F6E
spring.messages.basename=access-control
spring.messages.always-use-message-format=true
spring.messages.encoding=utf-8
# http\u914D\u7F6E
spring.http.multipart.max-file-size=200MB
spring.http.multipart.max-request-size=200MB
spring.http.encoding.force=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
# \u65E5\u5FD7\u914D\u7F6E
logging.config=classpath:logs/logback.xml
logging.file=${spring.application.name}
logging.path=logs
logging.level.root=info
logging.level.cn.cloudwalk=info
# mybatis\u914D\u7F6E
mybatis.mapper-locations=classpath*:cn/cloudwalk/elevator/**/*.xml
mybatis.config-location=classpath:mapper/mybatis-config.xml
# \u5E8F\u5217\u53F7\u914D\u7F6E
cloudwalk.serial.enabled=true
cloudwalk.serial.serial-length=8
cloudwalk.serial.serial-type=redis
cloudwalk.serial.serial-redis-key=CLOUDWALK-ACS-SERIAL-KEY
# \u7F13\u5B58\u914D\u7F6E
cloudwalk.spring.cache.expires=CACHE_NAME_APPLICATIONIDS#21600,ACS_DeviceTypesCache#7200,ACS_DeviceTypeFeaturesCache#7200,ACS_DeviceAttrsCache#7200,ACS_RecordStatisticsCache#90000,ACS_AreaTreeCache#60
# \u5185\u90E8\u63A5\u53E3\u8C03\u7528\u5BA2\u6237\u7AEF\u53CA\u8D85\u65F6\u914D\u7F6E
feign.hystrix.enable=true
feign.httpclient.enable=false
feign.okhttp.enable=true
ribbon.http.client.enabled=false
ribbon.okhttp.enabled=true
ribbon.ReadTimeout=10000
ribbon.ConnectTimeout=10000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
# \u5065\u5EB7\u68C0\u67E5\u914D\u7F6E
management.health.redis.enabled=false
management.health.db.enabled=false
# \u6570\u636E\u8131\u654F\u914D\u7F6E
cloudwalk.datafield.enable=true
cloudwalk.datafield.securityKey=d4b2aabc97394a12a27fc3cca6cd9ba1
cloudwalk.datafield.encrypt=AES
# redis\u914D\u7F6E\uFF08\u672C\u673A Docker\uFF1Aybs-redis 6379->6379\uFF0C\u82E5\u7528 craftlabs-redis \u6539\u4E3A 6380\uFF09
spring.redis.host=127.0.0.1
spring.redis.port=6379
# \u672C\u673A Redis \u65E0\u5BC6\u7801\u65F6\u5FC5\u987B\u4FDD\u7559\u4E0B\u884C\u7A7A\u503C\uFF0C\u4EE5\u8986\u76D6 fat-jar \u5185\u5D4C\u65E7\u5BC6\u7801\uFF08\u5426\u5219 Redisson ERR AUTH\uFF09
spring.redis.password=
spring.redis.database=5
spring.redis.timeout=0
spring.redis.pool.max-active=10
spring.redis.pool.max-idle=1
spring.redis.pool.max-wait=10
spring.redis.pool.min-idle=0
# \u6570\u636E\u5E93sharding\u914D\u7F6E
spring.shardingsphere.datasource.names=ds0
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.3.12:3307/cw-elevator-application?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
spring.shardingsphere.datasource.ds0.connection-timeout=60000
spring.shardingsphere.datasource.ds0.maximum-pool-size=20
spring.shardingsphere.datasource.ds0.minimum-idle=5
spring.shardingsphere.datasource.ds0.max-lifetime=1765000
spring.shardingsphere.datasource.ds0.auto-commit=true
spring.shardingsphere.datasource.ds0.pool-name=ds0-pool
spring.shardingsphere.props.sql.show=false
spring.shardingsphere.sharding.default-data-source-name=ds0
# \u5FAE\u670D\u52A1\u670D\u52A1\u540D\u914D\u7F6E
feign.device.name=cwos-portal
feign.resource.name=cwos-portal
feign.cwos-portal.name=cwos-portal
feign.ninca-crk-std.name=ninca-crk-std
# Feign/Ribbon 默认从 Consul 按服务名发现 ninca-crk-stdConsul 无注册时出现「Load balancer does not have available server」。
# 以下两行改为静态 ServerList(与下行 ninca-crk-std.ip 同目标时保持一致;若访客服务只部署在其它 IP/端口请一起修改):
ninca-crk-std.ribbon.NIWSServerListClassName=com.netflix.loadbalancer.ConfigurationBasedServerList
ninca-crk-std.ribbon.listOfServers=10.128.161.95:16106
feign.davinci-portal.name=cwos-portal
feign.component-organization.name=ninca-common-component-organization
feign.ninca-common.name=ninca-common
feign.mqtt.name=cloudwalk-device-thirdparty
# CWOS\u4E8B\u4EF6\u914D\u7F6E
cloudwalk.event.bootstrap-servers=192.168.3.12:9092
cloudwalk.event.group-id=cw-elevator-application-1
cloudwalk.event.handler-executor-config.core-pool-size=10
cloudwalk.event.handler-executor-config.maximum-pool-size=30
# \u5206\u5E03\u5F0F\u9501\u914D\u7F6E
intelligent.lock.enable=true
intelligent.lock.config.default-wait-time=10000
lockWatchdogTimeout=21000
# PERSON_NAME_SPACE
person.name.space=recordEvent
elevator.application.key=xinghewan
elevator.application.time=600
elevator.application.keyA=5B7DEF88FF04
ninca-crk-std.ip=10.128.161.95:16106
#\u53D1\u9001\u7B2C\u4E09\u65B9\u6570\u636Eip
sendRecord.ip=hrec.star-river.com:32165
sendRecord.token.corpId=53db867a8bb747a1bd04dd1afcad8ca6
sendRecord.token.appKey=293e2d708f0143c2957b702cef44d951
sendRecord.token.appSecret=5f6995009b864669b52041b8f5dc4625
sendRecord.boolean=true
# \u8BBE\u5907\u5904\u7406\u7EBF\u7A0B\u6C60\u914D\u7F6E
ninca.update.floor.pool.corePoolSize=5
ninca.update.floor.pool.maxPoolSize=5
ninca.update.floor.pool.queueCapacity=100000
ninca.update.floor.pool.keepAliveSeconds=150
ninca.update.floor.pool.allowCoreThreadTimeOut=true
#\u697C\u680Bid
floor.building.id=605560539791228928
@@ -0,0 +1,4 @@
# 与 JAR 同目录,Spring Cloud 会加载本文件,覆盖 jar 内 bootstrap.properties 中的旧 Consul 地址。
# 对应 192.168.3.12 上 Docker: hashicorp/consul:1.228500
spring.cloud.consul.host=192.168.3.12
spring.cloud.consul.port=8500
@@ -0,0 +1,5 @@
# 必须在 spring.config.location 中排在 application.properties 之后加载。
# run.sh 用 merge-redis-json.sh 将本文件转为 SPRING_APPLICATION_JSON,以压过 fat-jar 内 classpath 里的 Redis 配置。
spring.redis.host=192.168.3.12
spring.redis.port=6379
spring.redis.password=1qaz!QAZ
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# 与当前目录下 application.properties 同路径启动 V1 历史包。
# 默认优先使用系统 JDK 8(避免 Conda 的 JDK17+ 触发 CGLIB 模块错误)。
# 若必须用当前环境的 JAVA_HOME ELEVATOR_USE_ENV_JAVA=1 ./run.sh
# 额外 JVM 参数: ELEVATOR_JAVA_OPTS="-Xmx512m" ./run.sh
set -euo pipefail
cd "$(dirname "$0")"
# shellcheck source=../common-java.sh
source "$(cd "$(dirname "$0")" && pwd)/../common-java.sh"
JAR="cw-elevator-application-V1.0.0.20211103.jar"
if [[ ! -f "$JAR" ]]; then
echo "缺少 $JAR,请在 deploy 目录执行: ./sync-jars.sh" >&2
exit 1
fi
_pick_java_home
if [[ ! -x "${JAVA_HOME}/bin/java" ]]; then
echo "ERROR: 未找到可执行的 JDK。请安装 openjdk-8-jdk,或设定 JAVA_HOME / ELEVATOR_USE_ENV_JAVA=1 ./run.sh(使用 Conda 等当前环境)。" >&2
exit 1
fi
JAVA="${JAVA_HOME}/bin/java"
OPEN_FLAGS=()
while IFS= read -r line; do
[[ -n "$line" ]] && OPEN_FLAGS+=("$line")
done < <(_jdk8_open_flags "$JAVA")
# classpath:/application.properties 最后加载会盖住外置 properties;用 merge-redis-json.sh 把 redis-override.properties
# 转成 SPRING_APPLICATION_JSON(含 host/port/password),优先级高于 jar。
# 临时改密码:SPRING_REDIS_PASSWORD='其它' ./run.sh(含空字符串表示无密码)
if ! command -v python3 >/dev/null 2>&1; then
echo "需要 python3deploy/merge-redis-json.sh)。请安装 python3。" >&2
exit 1
fi
MERGE="$(cd "$(dirname "$0")" && pwd)/../merge-redis-json.sh"
if [[ ! -x "$MERGE" ]]; then
chmod +x "$MERGE" 2>/dev/null || true
fi
export SPRING_APPLICATION_JSON="$("$MERGE" "$PWD/redis-override.properties")"
# shellcheck disable=SC2086
exec "$JAVA" "${OPEN_FLAGS[@]}" ${ELEVATOR_JAVA_OPTS:-} -jar "$JAR" \
--spring.config.location=file:./application.properties,file:./redis-override.properties
@@ -0,0 +1,119 @@
# deploy/v2-maven \uFF1Amaven \u6784\u5EFA cw-elevator-application-2.0.0.jar\uFF08\u540C\u76EE\u5F55\u542F\u52A8\uFF09
server.port=18081
spring.application.name=cw-elevator-application
# Boot 1.5 \u65E0 spring.main.allow-bean-definition-overriding\uFF1B\u82E5\u91CD\u590D Bean \u9700\u5728\u4EE3\u7801\u4FA7\u6D88\u6B67\u4E49\u6216\u5347\u7EA7 Spring Boot
# spring\u914D\u7F6E
spring.mvc.throw-exception-if-no-handler-found=true
spring.mvc.locale=zh_CN
# \u8D44\u6E90\u6587\u4EF6\u914D\u7F6E
spring.messages.basename=access-control
spring.messages.always-use-message-format=true
spring.messages.encoding=utf-8
# http\u914D\u7F6E
spring.http.multipart.max-file-size=200MB
spring.http.multipart.max-request-size=200MB
spring.http.encoding.force=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
# \u65E5\u5FD7\u914D\u7F6E
logging.config=classpath:logs/logback.xml
logging.file=${spring.application.name}
logging.path=logs
logging.level.root=info
logging.level.cn.cloudwalk=info
# mybatis\u914D\u7F6E
mybatis.mapper-locations=classpath*:cn/cloudwalk/elevator/**/*.xml
mybatis.config-location=classpath:mapper/mybatis-config.xml
# \u5E8F\u5217\u53F7\u914D\u7F6E
cloudwalk.serial.enabled=true
cloudwalk.serial.serial-length=8
cloudwalk.serial.serial-type=redis
cloudwalk.serial.serial-redis-key=CLOUDWALK-ACS-SERIAL-KEY
# \u7F13\u5B58\u914D\u7F6E
cloudwalk.spring.cache.expires=CACHE_NAME_APPLICATIONIDS#21600,ACS_DeviceTypesCache#7200,ACS_DeviceTypeFeaturesCache#7200,ACS_DeviceAttrsCache#7200,ACS_RecordStatisticsCache#90000,ACS_AreaTreeCache#60
# \u5185\u90E8\u63A5\u53E3\u8C03\u7528\u5BA2\u6237\u7AEF\u53CA\u8D85\u65F6\u914D\u7F6E
feign.hystrix.enable=true
feign.httpclient.enable=false
feign.okhttp.enable=true
ribbon.http.client.enabled=false
ribbon.okhttp.enabled=true
ribbon.ReadTimeout=10000
ribbon.ConnectTimeout=10000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
# \u5065\u5EB7\u68C0\u67E5\u914D\u7F6E
management.health.redis.enabled=false
management.health.db.enabled=false
# \u6570\u636E\u8131\u654F\u914D\u7F6E
cloudwalk.datafield.enable=true
cloudwalk.datafield.securityKey=d4b2aabc97394a12a27fc3cca6cd9ba1
cloudwalk.datafield.encrypt=AES
# redis\u914D\u7F6E\uFF08\u672C\u673A Docker\uFF1Aybs-redis 6379->6379\uFF0C\u82E5\u7528 craftlabs-redis \u6539\u4E3A 6380\uFF09
spring.redis.host=127.0.0.1
spring.redis.port=6379
# \u672C\u673A Redis \u65E0\u5BC6\u7801\u65F6\u5FC5\u987B\u4FDD\u7559\u4E0B\u884C\u7A7A\u503C\uFF0C\u4EE5\u8986\u76D6 fat-jar \u5185\u5D4C\u65E7\u5BC6\u7801\uFF08\u5426\u5219 Redisson ERR AUTH\uFF09
spring.redis.password=
spring.redis.database=5
spring.redis.timeout=0
spring.redis.pool.max-active=10
spring.redis.pool.max-idle=1
spring.redis.pool.max-wait=10
spring.redis.pool.min-idle=0
# \u6570\u636E\u5E93sharding\u914D\u7F6E
spring.shardingsphere.datasource.names=ds0
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.3.12:3307/cw-elevator-application?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
spring.shardingsphere.datasource.ds0.connection-timeout=60000
spring.shardingsphere.datasource.ds0.maximum-pool-size=20
spring.shardingsphere.datasource.ds0.minimum-idle=5
spring.shardingsphere.datasource.ds0.max-lifetime=1765000
spring.shardingsphere.datasource.ds0.auto-commit=true
spring.shardingsphere.datasource.ds0.pool-name=ds0-pool
spring.shardingsphere.props.sql.show=false
spring.shardingsphere.sharding.default-data-source-name=ds0
# \u5FAE\u670D\u52A1\u670D\u52A1\u540D\u914D\u7F6E
feign.device.name=cwos-portal
feign.resource.name=cwos-portal
feign.cwos-portal.name=cwos-portal
feign.ninca-crk-std.name=ninca-crk-std
# 见 v1-legacy 同段注释:Consul 无 ninca-crk-std 时用静态 Ribbon 列表。
ninca-crk-std.ribbon.NIWSServerListClassName=com.netflix.loadbalancer.ConfigurationBasedServerList
ninca-crk-std.ribbon.listOfServers=10.128.161.95:16106
feign.davinci-portal.name=cwos-portal
feign.component-organization.name=ninca-common-component-organization
feign.ninca-common.name=ninca-common
feign.mqtt.name=cloudwalk-device-thirdparty
# CWOS\u4E8B\u4EF6\u914D\u7F6E
cloudwalk.event.bootstrap-servers=192.168.3.12:9092
cloudwalk.event.group-id=cw-elevator-application-1
cloudwalk.event.handler-executor-config.core-pool-size=10
cloudwalk.event.handler-executor-config.maximum-pool-size=30
# \u5206\u5E03\u5F0F\u9501\u914D\u7F6E
intelligent.lock.enable=true
intelligent.lock.config.default-wait-time=10000
lockWatchdogTimeout=21000
# PERSON_NAME_SPACE
person.name.space=recordEvent
elevator.application.key=xinghewan
elevator.application.time=600
elevator.application.keyA=5B7DEF88FF04
ninca-crk-std.ip=10.128.161.95:16106
#\u53D1\u9001\u7B2C\u4E09\u65B9\u6570\u636Eip
sendRecord.ip=hrec.star-river.com:32165
sendRecord.token.corpId=53db867a8bb747a1bd04dd1afcad8ca6
sendRecord.token.appKey=293e2d708f0143c2957b702cef44d951
sendRecord.token.appSecret=5f6995009b864669b52041b8f5dc4625
sendRecord.boolean=true
# \u8BBE\u5907\u5904\u7406\u7EBF\u7A0B\u6C60\u914D\u7F6E
ninca.update.floor.pool.corePoolSize=5
ninca.update.floor.pool.maxPoolSize=5
ninca.update.floor.pool.queueCapacity=100000
ninca.update.floor.pool.keepAliveSeconds=150
ninca.update.floor.pool.allowCoreThreadTimeOut=true
#\u697C\u680Bid
floor.building.id=605560539791228928
@@ -0,0 +1,3 @@
# 覆盖 fat-jar 内嵌 Consul 地址,指向局域网 Docker Consul。
spring.cloud.consul.host=192.168.3.12
spring.cloud.consul.port=8500
@@ -0,0 +1,4 @@
# run.sh 将本文件合并为 SPRING_APPLICATION_JSON,覆盖 jar 内 Redis 配置。
spring.redis.host=192.168.3.12
spring.redis.port=6379
spring.redis.password=1qaz!QAZ
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# 与当前目录下 application.properties 同路径启动 V2maven 构建)包。
# 默认优先系统 JDK 8;若只有 JDK11+ 会自动附加 --add-opens。
# ELEVATOR_USE_ENV_JAVA=1 ./run.sh 使用当前 JAVA_HOME(如 Conda)。
set -euo pipefail
cd "$(dirname "$0")"
# shellcheck source=../common-java.sh
source "$(cd "$(dirname "$0")" && pwd)/../common-java.sh"
JAR="cw-elevator-application-2.0.0.jar"
if [[ ! -f "$JAR" ]]; then
echo "缺少 $JAR,请在 deploy 目录执行: ./sync-jars.sh" >&2
exit 1
fi
_pick_java_home
if [[ ! -x "${JAVA_HOME}/bin/java" ]]; then
echo "ERROR: 未找到可执行的 JDK。请安装 openjdk-8-jdk,或设定 JAVA_HOME / ELEVATOR_USE_ENV_JAVA=1 ./run.sh(使用 Conda 等当前环境)。" >&2
exit 1
fi
JAVA="${JAVA_HOME}/bin/java"
OPEN_FLAGS=()
while IFS= read -r line; do
[[ -n "$line" ]] && OPEN_FLAGS+=("$line")
done < <(_jdk8_open_flags "$JAVA")
# 同 v1:由 redis-override.properties 合并出 SPRING_APPLICATION_JSON。
if ! command -v python3 >/dev/null 2>&1; then
echo "需要 python3deploy/merge-redis-json.sh)。请安装 python3。" >&2
exit 1
fi
MERGE="$(cd "$(dirname "$0")" && pwd)/../merge-redis-json.sh"
if [[ ! -x "$MERGE" ]]; then
chmod +x "$MERGE" 2>/dev/null || true
fi
export SPRING_APPLICATION_JSON="$("$MERGE" "$PWD/redis-override.properties")"
# shellcheck disable=SC2086
exec "$JAVA" "${OPEN_FLAGS[@]}" ${ELEVATOR_JAVA_OPTS:-} -jar "$JAR" \
--spring.config.location=file:./application.properties,file:./redis-override.properties
+42 -15
View File
@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.18.RELEASE</version> <version>1.5.17.RELEASE</version>
<relativePath/> <relativePath/>
</parent> </parent>
@@ -15,7 +15,7 @@
<version>2.0-SNAPSHOT</version> <version>2.0-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>cw-elevator-application (Maven reactor)</name> <name>cw-elevator-application (Maven reactor)</name>
<description>聚合模块:common → data → service → web。release/cw-elevator-v1-lib-min-riskcloudwalk-common-service/event 与 V1 cw_lib 一致为 3.7.2部分第三方与 cw-elevator-application-V1.0.0.20211103/lib 文件名对齐。对照目录见 cw.elevator.v1.lib.dir。</description> <description>聚合模块:common → data → service → web。内部件版本与 ../cw-elevator-application-V1.0.0.20211103/cw_lib 对齐(含 intelligent-cwoscomponent 2.9.2-xinghewan,不使用 3.0.0);cloudwalk-common 3.7.2;第三方与 V1 运行包一致:Spring Boot 1.5.17、Spring Cloud Edgware、mybatis-spring-boot 1.3.1 等。可选对照 V1 展开包 lib 目录见 cw.elevator.v1.lib.dir。</description>
<modules> <modules>
<module>cw-elevator-application-common</module> <module>cw-elevator-application-common</module>
@@ -32,24 +32,31 @@
<!-- 与 V1.0.0.20211103/cw_lib 中 cloudwalk-common-* 一致,降低与历史运行包 API 差异 --> <!-- 与 V1.0.0.20211103/cw_lib 中 cloudwalk-common-* 一致,降低与历史运行包 API 差异 -->
<cloudwalk.internal.version>3.7.2-Brussels-SRX</cloudwalk.internal.version> <cloudwalk.internal.version>3.7.2-Brussels-SRX</cloudwalk.internal.version>
<cloudwalk.legacy.public.version>3.7.2-Brussels-SRX</cloudwalk.legacy.public.version> <cloudwalk.legacy.public.version>3.7.2-Brussels-SRX</cloudwalk.legacy.public.version>
<intelligent.cwoscomponent.version>3.0.0-xinghewan</intelligent.cwoscomponent.version> <!-- 与 cw_lib 完全一致;禁止改为 3.0.0-xinghewan(与本产品线口径冲突) -->
<!-- 以下与 V1.0.0.20211103/lib 中 *.pom 主版本对齐(保持 Spring Boot 2.1.18 以兼容当前源码) --> <intelligent.cwoscomponent.version>2.9.2-xinghewan</intelligent.cwoscomponent.version>
<!-- 以下与 V1 fat-jar lib 目录文件名对齐(与 cw-elevator-application-V1.0.0.20211103 一致) -->
<fastjson.version>1.2.73</fastjson.version> <fastjson.version>1.2.73</fastjson.version>
<guava.version>28.2-jre</guava.version> <guava.version>20.0</guava.version>
<poi.version>4.1.2</poi.version> <poi.version>3.15</poi.version>
<ant.version>1.10.12</ant.version> <ant.version>1.9.6</ant.version>
<thumbnailator.version>0.4.8</thumbnailator.version> <thumbnailator.version>0.4.8</thumbnailator.version>
<commons-io.version>2.5</commons-io.version> <commons-io.version>2.5</commons-io.version>
<!-- Boot 1.5 父 POM 不托管 commons-lang3,显式与 1.5.x 栈常用版本一致 -->
<commons-lang3.version>3.5</commons-lang3.version>
<zip4j.version>2.6.2</zip4j.version> <zip4j.version>2.6.2</zip4j.version>
<zxing.version>3.3.3</zxing.version> <zxing.version>3.3.3</zxing.version>
<pagehelper.version>5.1.2</pagehelper.version> <pagehelper.version>5.1.2</pagehelper.version>
<pagehelper-spring-boot.version>1.2.5</pagehelper-spring-boot.version> <pagehelper-spring-boot.version>1.2.5</pagehelper-spring-boot.version>
<shardingsphere.version>4.0.0</shardingsphere.version> <shardingsphere.version>4.0.0</shardingsphere.version>
<mybatis.version>3.5.6</mybatis.version> <mybatis.version>3.4.6</mybatis.version>
<mybatis-spring.version>2.0.6</mybatis-spring.version> <mybatis-spring.version>1.3.2</mybatis-spring.version>
<mybatis-spring-boot.version>2.0.1</mybatis-spring-boot.version> <mybatis-spring-boot.version>1.3.1</mybatis-spring-boot.version>
<servlet-api.version>2.5</servlet-api.version> <javax.servlet-api.version>3.1.0</javax.servlet-api.version>
<javax.annotation-api.version>1.3.2</javax.annotation-api.version>
<!-- javax.annotation.Nullable 来自 JSR-305,非 javax.annotation-api -->
<jsr305.version>3.0.2</jsr305.version>
<cwos.sdk.resource.version>1.0.0-SNAPSHOT</cwos.sdk.resource.version> <cwos.sdk.resource.version>1.0.0-SNAPSHOT</cwos.sdk.resource.version>
<cwos.sdk.event.version>1.5.0-SNAPSHOT</cwos.sdk.event.version>
<intelligent.lock.version>1.1.1-SNAPSHOT</intelligent.lock.version> <intelligent.lock.version>1.1.1-SNAPSHOT</intelligent.lock.version>
<davinci.manager.storage.version>1.1.7-SNAPSHOT</davinci.manager.storage.version> <davinci.manager.storage.version>1.1.7-SNAPSHOT</davinci.manager.storage.version>
<!-- Nexus UI: http://192.168.3.12/#browse/welcome --> <!-- Nexus UI: http://192.168.3.12/#browse/welcome -->
@@ -57,8 +64,8 @@
<nexus.public.repo>${nexus.baseUrl}/repository/maven-public/</nexus.public.repo> <nexus.public.repo>${nexus.baseUrl}/repository/maven-public/</nexus.public.repo>
<formatter.maven.plugin.version>2.24.1</formatter.maven.plugin.version> <formatter.maven.plugin.version>2.24.1</formatter.maven.plugin.version>
<alibaba.eclipse.codestyle.path>${project.basedir}/../docs/style/alibaba-eclipse-codestyle.xml</alibaba.eclipse.codestyle.path> <alibaba.eclipse.codestyle.path>${project.basedir}/../docs/style/alibaba-eclipse-codestyle.xml</alibaba.eclipse.codestyle.path>
<!-- 与 Spring Boot 2.1.18 对齐的 OpenFeign/Cloud 版本(发布包可执行 JAR 需要 --> <!-- 与 Spring Boot 1.5.x 配对的 Spring Cloud Edgwarespring-cloud-starter-openfeign;注解包为 netflix.feign -->
<spring-cloud.version>Greenwich.SR6</spring-cloud.version> <spring-cloud.version>Edgware.SR6</spring-cloud.version>
<!-- spring-boot-maven-plugin repackage 产出的可执行 JAR 文件名(不含 .jar) --> <!-- spring-boot-maven-plugin repackage 产出的可执行 JAR 文件名(不含 .jar) -->
<elevator.release.finalName>cw-elevator-application-2.0.0</elevator.release.finalName> <elevator.release.finalName>cw-elevator-application-2.0.0</elevator.release.finalName>
</properties> </properties>
@@ -107,6 +114,11 @@
<artifactId>cwos-java-sdk-resource</artifactId> <artifactId>cwos-java-sdk-resource</artifactId>
<version>${cwos.sdk.resource.version}</version> <version>${cwos.sdk.resource.version}</version>
</dependency> </dependency>
<dependency>
<groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cwos-sdk-event</artifactId>
<version>${cwos.sdk.event.version}</version>
</dependency>
<dependency> <dependency>
<groupId>cn.cloudwalk.intelligent</groupId> <groupId>cn.cloudwalk.intelligent</groupId>
<artifactId>cloudwalk-intelligent-component-lock</artifactId> <artifactId>cloudwalk-intelligent-component-lock</artifactId>
@@ -147,6 +159,11 @@
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
<version>${commons-io.version}</version> <version>${commons-io.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId> <artifactId>fastjson</artifactId>
@@ -194,8 +211,18 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
<version>${servlet-api.version}</version> <version>${javax.servlet-api.version}</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>${javax.annotation-api.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>${jsr305.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.el</groupId> <groupId>javax.el</groupId>
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env bash
# 仅编译本仓库(maven-cw-elevator-application),依赖从 Nexus 拉取,避免使用全局 ~/.m2 里
# 其它反编译工程 mvn install 产生的同坐标覆盖。
#
# 方案要点:
# - 使用独立 maven.repo.local(默认:仓库根目录下 .m2-elevator-nexus-only),与 ~/.m2 隔离。
# - Nexus 常缺聚合父 POM cloudwalk-cloud-common:脚本会(1)尝试从全局 ~/.m2 复制该目录;
# (2)若仍缺,且存在相邻仓库 maven-cloudwalk-legacy-public/cloudwalk-cloud-common,则自动
# mvn -N install 到隔离库(仅父 POM,不编其它反编译模块)。
# - 私服上 cloudwalk-common-web 等 POM 若传递依赖不完整,隔离构建会缺 spring-boot / commons-lang
# 等编译包。可选(3)从全局 ~/.m2 复制 cloudwalk-common-* 同版本目录(与 Nexus 二选一:先复制则优先用本地该目录)。
# - Nexus 常缺 SNAPSHOT 父 POMcwos / davinci)及 cloudwalk-device-sdk 父 POM:若相邻仓库存在对应 pom.xml,则自动 mvn -N install
# 到隔离库:maven-cwos-resource 的 cwos-component-resource、cwos-portalmaven-cloudwalk-intelligent-davinci-manager 根 POM
# maven-cloudwalk-device-sdk 根 POM(供 intelligent-cwoscomponent-interface 解析 protocol-entity)。
# ELEVATOR_AUTO_INSTALL_LEGACY_SNAPSHOT_PARENTS=0 跳过上述「相邻父 POM install」整段。
# - intelligent-cwoscomponent 固定为 2.9.2-xinghewan(与 cw_lib 一致,禁止 3.0.0):可先 ELEVATOR_BOOTSTRAP_INTELLIGENT_CWOSCOMPONENT_FROM_GLOBAL
# 从 ~/.m2 复制 parentartifactId intelligent-cwoscomponent/ interface / rest;若仍缺 JAR,则对
# cw-elevator-application-V1.0.0.20211103/cw_lib 内同名 jar+pom 执行 install-file,并先安装 scripts/legacy-poms 下父 POM 桩。
# ELEVATOR_AUTO_INSTALL_INTELLIGENT_CWOSCOMPONENT=0 跳过 installELEVATOR_CW_LIB_DIR 覆盖 cw_lib 路径。
# - cwos-sdk-event(默认 1.5.0-SNAPSHOT):ELEVATOR_BOOTSTRAP_CWOS_SDK_EVENT_FROM_GLOBAL 从 ~/.m2 复制;缺 JAR 时对
# maven-cloudwalk-legacy-public/cwos-sdk-event mvn install。ELEVATOR_AUTO_INSTALL_CWOS_SDK_EVENT=0 跳过 install。
# - ELEVATOR_BOOTSTRAP_FROM_GLOBAL_M2=0 跳过(1);ELEVATOR_AUTO_INSTALL_LEGACY_PARENT=0 跳过(2);
# ELEVATOR_BOOTSTRAP_CLOUDWALK_MODULES_FROM_GLOBAL=0 跳过(3)(坚持纯 Nexus 时用)。
#
# 用法:
# ./scripts/build_nexus_only.sh
# ELEVATOR_M2_REPO=/path/to/custom-repo ./scripts/build_nexus_only.sh
# ELEVATOR_SYNC_DEPLOY=1 ./scripts/build_nexus_only.sh # 另同步 V1/V2 到 deploy/v1-legacy 与 deploy/v2-maven
# - 编译成功后默认将 starter fat jar 安装到 deploy/v2-maven/(覆盖 cw-elevator-application-2.0.0.jar)。
# ELEVATOR_DEPLOY_V2_MAVEN=0 跳过;ELEVATOR_DEPLOY_V2_DIR=/path 覆盖目标目录(默认同 deploy/v2-maven)。
set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO"
if [[ -z "${JAVA_8:-}" ]]; then
for d in /usr/lib/jvm/java-8-openjdk-amd64 /usr/lib/jvm/java-1.8.0-openjdk; do
if [[ -x "$d/bin/java" ]]; then
export JAVA_8="$d"
break
fi
done
fi
: "${JAVA_8:=/usr/lib/jvm/java-8-openjdk-amd64}"
export JAVA_HOME="$JAVA_8"
export PATH="$JAVA_HOME/bin:$PATH"
if [[ ! -x "${JAVA_HOME}/bin/java" ]]; then
echo "ERROR: 未找到 JDK 8,请设置 JAVA_8 或安装 openjdk-8-jdk。" >&2
exit 1
fi
M2_LOCAL="${ELEVATOR_M2_REPO:-$REPO/.m2-elevator-nexus-only}"
mkdir -p "$M2_LOCAL"
LEGACY_VER="${CLOUDWALK_LEGACY_VERSION:-3.7.2-Brussels-SRX}"
ICOMP_VER="${ELEVATOR_INTELLIGENT_CWOSCOMPONENT_VERSION:-2.9.2-xinghewan}"
CWOS_EVENT_VER="${ELEVATOR_CWOS_SDK_EVENT_VERSION:-1.5.0-SNAPSHOT}"
GLOBAL_M2="${ELEVATOR_GLOBAL_M2:-$HOME/.m2/repository}"
parent_marker_file() {
echo "$M2_LOCAL/cn/cloudwalk/cloud/cloudwalk-cloud-common/${LEGACY_VER}/cloudwalk-cloud-common-${LEGACY_VER}.pom"
}
bootstrap_from_global_m2() {
local rel="cn/cloudwalk/cloud/cloudwalk-cloud-common/${LEGACY_VER}"
local src="$GLOBAL_M2/$rel"
if [[ ! -d "$src" ]]; then
echo "WARN: 全局仓库中无父 POM 目录: $src(将尝试相邻 legacy 自动 install" >&2
return 0
fi
mkdir -p "$M2_LOCAL/$rel"
cp -a "$src/." "$M2_LOCAL/$rel/"
echo "==> 已从全局 ~/.m2 预置父 POM(仅该目录): $rel"
}
purge_last_updated_under() {
local base="$1"
[[ -d "$base" ]] || return 0
find "$base" -name '*.lastUpdated' -type f -print -delete 2>/dev/null || true
}
# 参数: marker_pom_path pom_file human_desc
install_one_snapshot_parent_to_local_repo() {
local marker="$1"
local pom="$2"
local desc="$3"
if [[ -f "$marker" ]]; then
echo "==> 隔离库已有: $desc"
return 0
fi
if [[ ! -f "$pom" ]]; then
echo "ERROR: 隔离库缺少 $desc,且未找到 POM: $pom" >&2
echo " 请将对应父 POM 发布到 Nexus,或设置 ELEVATOR_LEGACY_CWOS_RESOURCE_DIR / ELEVATOR_LEGACY_DAVINCI_MANAGER_POM。" >&2
exit 1
fi
echo "==> 向隔离库安装(mvn -N install: $desc -> $pom"
mvn -Dmaven.repo.local="$M2_LOCAL" -f "$pom" -N install -DskipTests
if [[ ! -f "$marker" ]]; then
echo "ERROR: install 后仍缺少: $marker" >&2
exit 1
fi
}
install_legacy_cloudwalk_parent_to_local_repo() {
local marker
marker="$(parent_marker_file)"
if [[ -f "$marker" ]]; then
echo "==> 隔离库已有父 POM: $marker"
return 0
fi
if [[ "${ELEVATOR_AUTO_INSTALL_LEGACY_PARENT:-1}" != "1" ]]; then
echo "ERROR: 隔离库缺少父 POM 且 ELEVATOR_AUTO_INSTALL_LEGACY_PARENT=0$marker" >&2
exit 1
fi
local def_pom
def_pom="$(cd "$REPO/.." && pwd)/maven-cloudwalk-legacy-public/cloudwalk-cloud-common/pom.xml"
local legacy_pom="${ELEVATOR_LEGACY_CLOUDWALK_COMMON_POM:-$def_pom}"
if [[ ! -f "$legacy_pom" ]]; then
echo "ERROR: 隔离库无 cloudwalk-cloud-common POM,且未找到:$legacy_pom" >&2
echo " 请设置 ELEVATOR_LEGACY_CLOUDWALK_COMMON_POM,或将该父 POM 发布到 Nexus。" >&2
exit 1
fi
echo "==> 向隔离库安装父 POMmvn -N install: $legacy_pom"
mvn -Dmaven.repo.local="$M2_LOCAL" -f "$legacy_pom" -N install -DskipTests
if [[ ! -f "$marker" ]]; then
echo "ERROR: install 后仍缺少: $marker" >&2
exit 1
fi
}
if [[ "${ELEVATOR_BOOTSTRAP_FROM_GLOBAL_M2:-1}" == "1" ]]; then
echo "==> 尝试从全局 ~/.m2 预置 cloudwalk-cloud-common${LEGACY_VER}"
bootstrap_from_global_m2
fi
install_legacy_cloudwalk_parent_to_local_repo
# 私服缺 cwos-portal / cwos-component-resource / cloudwalk-intelligent-davinci-manager 等父 POM 时,用相邻反应堆根 POM 闭合描述符。
install_legacy_snapshot_parents_to_local_repo() {
if [[ "${ELEVATOR_AUTO_INSTALL_LEGACY_SNAPSHOT_PARENTS:-1}" != "1" ]]; then
return 0
fi
local root
root="$(cd "$REPO/.." && pwd)"
local def_cwos="$root/maven-cwos-resource"
local def_davinci="$root/maven-cloudwalk-intelligent-davinci-manager/pom.xml"
local cwos_base="${ELEVATOR_LEGACY_CWOS_RESOURCE_DIR:-$def_cwos}"
install_one_snapshot_parent_to_local_repo \
"$M2_LOCAL/cn/cloudwalk/cloud/cwos-component-resource/1.0.0-SNAPSHOT/cwos-component-resource-1.0.0-SNAPSHOT.pom" \
"$cwos_base/cwos-component-resource/pom.xml" \
"cwos-component-resource 1.0.0-SNAPSHOT"
install_one_snapshot_parent_to_local_repo \
"$M2_LOCAL/cn/cloudwalk/cwos-portal/1.0.0-SNAPSHOT/cwos-portal-1.0.0-SNAPSHOT.pom" \
"$cwos_base/cwos-portal/pom.xml" \
"cwos-portal 1.0.0-SNAPSHOT"
install_one_snapshot_parent_to_local_repo \
"$M2_LOCAL/cn/cloudwalk/intelligent/cloudwalk-intelligent-davinci-manager/1.1.7-SNAPSHOT/cloudwalk-intelligent-davinci-manager-1.1.7-SNAPSHOT.pom" \
"${ELEVATOR_LEGACY_DAVINCI_MANAGER_POM:-$def_davinci}" \
"cloudwalk-intelligent-davinci-manager 1.1.7-SNAPSHOT"
install_one_snapshot_parent_to_local_repo \
"$M2_LOCAL/cn/cloudwalk/cloudwalk-device-sdk/2.2.0/cloudwalk-device-sdk-2.2.0.pom" \
"${ELEVATOR_LEGACY_DEVICE_SDK_POM:-$root/maven-cloudwalk-device-sdk/pom.xml}" \
"cloudwalk-device-sdk 2.2.0"
}
install_legacy_snapshot_parents_to_local_repo
bootstrap_intelligent_cwoscomponent_from_global() {
if [[ "${ELEVATOR_BOOTSTRAP_INTELLIGENT_CWOSCOMPONENT_FROM_GLOBAL:-1}" != "1" ]]; then
return 0
fi
local copied=0
# 2.9.2 线:父 artifactId 为 intelligent-cwoscomponent(非 reactor/parent 3.x 命名)
for art in intelligent-cwoscomponent intelligent-cwoscomponent-interface intelligent-cwoscomponent-rest; do
local rel="cn/cloudwalk/intelligent/${art}/${ICOMP_VER}"
if [[ -d "$GLOBAL_M2/$rel" ]]; then
mkdir -p "$M2_LOCAL/$rel"
cp -a "$GLOBAL_M2/$rel/." "$M2_LOCAL/$rel/"
echo "==> 已从 ~/.m2 预置 intelligent-cwoscomponent: $rel"
copied=1
fi
done
if [[ "$copied" -eq 0 ]]; then
echo "WARN: ~/.m2 中未找到 intelligent-cwoscomponent-*${ICOMP_VER}),将视情况从 cw_lib install-file。" >&2
fi
}
install_intelligent_cwoscomponent_from_cw_lib_if_missing() {
if [[ "${ELEVATOR_AUTO_INSTALL_INTELLIGENT_CWOSCOMPONENT:-1}" != "1" ]]; then
return 0
fi
if [[ "$ICOMP_VER" == "3.0.0-xinghewan" ]]; then
echo "ERROR: 本产品线禁止使用 intelligent-cwoscomponent 3.0.0-xinghewan;请使用 2.9.2-xinghewancw_lib)。" >&2
exit 1
fi
local marker="$M2_LOCAL/cn/cloudwalk/intelligent/intelligent-cwoscomponent-rest/${ICOMP_VER}/intelligent-cwoscomponent-rest-${ICOMP_VER}.jar"
if [[ -f "$marker" ]]; then
echo "==> 隔离库已有 intelligent-cwoscomponent-rest${ICOMP_VER}"
return 0
fi
local root cw_lib
root="$(cd "$REPO/.." && pwd)"
cw_lib="${ELEVATOR_CW_LIB_DIR:-$root/cw-elevator-application-V1.0.0.20211103/cw_lib}"
local stub="$REPO/scripts/legacy-poms/intelligent-cwoscomponent-2.9.2-xinghewan-parent.pom"
local ij="$cw_lib/intelligent-cwoscomponent-interface-${ICOMP_VER}.jar"
local ip="$cw_lib/intelligent-cwoscomponent-interface-${ICOMP_VER}.pom"
local rj="$cw_lib/intelligent-cwoscomponent-rest-${ICOMP_VER}.jar"
local rp="$cw_lib/intelligent-cwoscomponent-rest-${ICOMP_VER}.pom"
if [[ ! -f "$stub" ]]; then
echo "ERROR: 缺少父 POM 桩: $stub" >&2
exit 1
fi
if [[ ! -f "$ij" || ! -f "$ip" || ! -f "$rj" || ! -f "$rp" ]]; then
echo "ERROR: cw_lib 缺少 intelligent-cwoscomponent 2.9.2 构件,无法安装到隔离库。" >&2
echo " 期望目录: $cw_lib(设 ELEVATOR_CW_LIB_DIR 可覆盖)" >&2
echo " 需要: intelligent-cwoscomponent-interface/rest 的 .jar 与 .pom" >&2
exit 1
fi
echo "==> 向隔离库安装 intelligent-cwoscomponent 父 POM(桩): $stub"
mvn -Dmaven.repo.local="$M2_LOCAL" -f "$stub" -N install -DskipTests
echo "==> install-file intelligent-cwoscomponent-interface${ICOMP_VER}"
mvn -Dmaven.repo.local="$M2_LOCAL" org.apache.maven.plugins:maven-install-plugin:3.1.1:install-file \
-DpomFile="$ip" -Dfile="$ij" -Dpackaging=jar
echo "==> install-file intelligent-cwoscomponent-rest${ICOMP_VER}"
mvn -Dmaven.repo.local="$M2_LOCAL" org.apache.maven.plugins:maven-install-plugin:3.1.1:install-file \
-DpomFile="$rp" -Dfile="$rj" -Dpackaging=jar
if [[ ! -f "$marker" ]]; then
echo "ERROR: install 后仍缺少: $marker" >&2
exit 1
fi
}
bootstrap_intelligent_cwoscomponent_from_global
install_intelligent_cwoscomponent_from_cw_lib_if_missing
bootstrap_cwos_sdk_event_from_global() {
if [[ "${ELEVATOR_BOOTSTRAP_CWOS_SDK_EVENT_FROM_GLOBAL:-1}" != "1" ]]; then
return 0
fi
local rel="cn/cloudwalk/cloud/cwos-sdk-event/${CWOS_EVENT_VER}"
if [[ -d "$GLOBAL_M2/$rel" ]]; then
mkdir -p "$M2_LOCAL/$rel"
cp -a "$GLOBAL_M2/$rel/." "$M2_LOCAL/$rel/"
echo "==> 已从 ~/.m2 预置 cwos-sdk-event: $rel"
else
echo "WARN: ~/.m2 中无 cwos-sdk-event${CWOS_EVENT_VER}),将视情况 mvn install legacy 模块。" >&2
fi
}
install_cwos_sdk_event_if_missing() {
if [[ "${ELEVATOR_AUTO_INSTALL_CWOS_SDK_EVENT:-1}" != "1" ]]; then
return 0
fi
local marker="$M2_LOCAL/cn/cloudwalk/cloud/cwos-sdk-event/${CWOS_EVENT_VER}/cwos-sdk-event-${CWOS_EVENT_VER}.jar"
if [[ -f "$marker" ]]; then
echo "==> 隔离库已有 cwos-sdk-event${CWOS_EVENT_VER}"
return 0
fi
local root def_pom
root="$(cd "$REPO/.." && pwd)"
def_pom="$root/maven-cloudwalk-legacy-public/cwos-sdk-event/pom.xml"
local ev_pom="${ELEVATOR_LEGACY_CWOS_SDK_EVENT_POM:-$def_pom}"
if [[ ! -f "$ev_pom" ]]; then
echo "ERROR: 私服缺少 cwos-sdk-event:${CWOS_EVENT_VER},且未找到: $ev_pom" >&2
exit 1
fi
echo "==> 向隔离库 install cwos-sdk-event: $ev_pom"
mvn -Dmaven.repo.local="$M2_LOCAL" -f "$ev_pom" install -DskipTests
if [[ ! -f "$marker" ]]; then
echo "ERROR: install 后仍缺少: $marker" >&2
exit 1
fi
}
bootstrap_cwos_sdk_event_from_global
install_cwos_sdk_event_if_missing
bootstrap_cloudwalk_modules_from_global() {
if [[ "${ELEVATOR_BOOTSTRAP_CLOUDWALK_MODULES_FROM_GLOBAL:-1}" != "1" ]]; then
return 0
fi
local _arts="${ELEVATOR_CLOUDWALK_BOOTSTRAP_ARTIFACTS:-cloudwalk-common-result cloudwalk-common-web cloudwalk-common-serial cloudwalk-common-service}"
local copied=0
for a in $_arts; do
[[ -n "$a" ]] || continue
local rel="cn/cloudwalk/cloud/${a}/${LEGACY_VER}"
if [[ -d "$GLOBAL_M2/$rel" ]]; then
mkdir -p "$M2_LOCAL/$rel"
cp -a "$GLOBAL_M2/$rel/." "$M2_LOCAL/$rel/"
echo "==> 已从 ~/.m2 预置 cloudwalk 构件目录: $rel"
copied=1
fi
done
if [[ "$copied" -eq 0 ]]; then
echo "WARN: ~/.m2 中未找到上述 cloudwalk-common-* 目录(${LEGACY_VER}),将完全依赖 Nexus 传递依赖。" >&2
fi
}
bootstrap_cloudwalk_modules_from_global
echo "==> 清理 cn/cloudwalk 下失败缓存 (*.lastUpdated)"
purge_last_updated_under "$M2_LOCAL/cn/cloudwalk"
MVN_GOALS="${ELEVATOR_MVN_GOALS:-clean package}"
echo "==> 使用隔离本地仓库: $M2_LOCAL"
echo "==> mvn -Dmaven.repo.local=... $MVN_GOALS -DskipTests -U"
mvn -Dmaven.repo.local="$M2_LOCAL" -U $MVN_GOALS -DskipTests
JAR="$REPO/cw-elevator-application-starter/target/cw-elevator-application-2.0.0.jar"
echo "==> 产物: $JAR"
test -f "$JAR" && ls -la "$JAR"
if [[ -f "$JAR" && "${ELEVATOR_DEPLOY_V2_MAVEN:-1}" == "1" ]]; then
V2_DEPLOY_DIR="${ELEVATOR_DEPLOY_V2_DIR:-$REPO/deploy/v2-maven}"
mkdir -p "$V2_DEPLOY_DIR"
install -m0644 "$JAR" "$V2_DEPLOY_DIR/cw-elevator-application-2.0.0.jar"
echo "==> 已发布到 deploy/v2-maven(替换 JAR: $V2_DEPLOY_DIR/cw-elevator-application-2.0.0.jar"
ls -la "$V2_DEPLOY_DIR/cw-elevator-application-2.0.0.jar"
fi
if [[ "${ELEVATOR_SYNC_DEPLOY:-0}" == "1" ]]; then
SYNC="$REPO/deploy/sync-jars.sh"
if [[ -x "$SYNC" ]] || chmod +x "$SYNC" 2>/dev/null; then
echo "==> ELEVATOR_SYNC_DEPLOY=1 -> $SYNC"
bash "$SYNC"
else
echo "WARN: 未找到可执行的 deploy/sync-jars.sh,跳过同步。" >&2
fi
fi
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# 将 cw_lib 中的 intelligent-cwoscomponent(父 POM 桩 + interface + rest)部署到 Nexus
# 与 docs/operations/deploy_cw_elevator_v1_lib_to_nexus.py 使用同一 server id / 仓库 URL 约定。
#
# 需要 ~/.m2/settings.xml 中配置 server idnexus-releases(密码等)。
#
# 环境变量(可选):
# NEXUS_RELEASES_URL 默认 http://192.168.3.12:8081/repository/maven-releases/
# NEXUS_RELEASES_ID 默认 nexus-releases
# ELEVATOR_CW_LIB_DIR 默认 反编译根下 cw-elevator-application-V1.0.0.20211103/cw_lib
# ELEVATOR_ICOMP_VER 默认 2.9.2-xinghewan
# DRY_RUN=1 仅打印命令
# ELEVATOR_INSTALL_LOCAL_M2=1(默认)部署后再把 cw_lib 安装到 ~/.m2(与 build_nexus_only
# 一致;因私服上子 POM 缺版本段时 dependency:get 可能无效模型,本地以桩父 POM+install-file 为准)
# ELEVATOR_INSTALL_LOCAL_M2=0 跳过本机安装
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
REPO_ROOT="${ELEVATOR_REPO_ROOT_OVERRIDE:-$REPO_ROOT}"
NEXUS_RELEASES_URL="${NEXUS_RELEASES_URL:-http://192.168.3.12:8081/repository/maven-releases/}"
NEXUS_RELEASES_ID="${NEXUS_RELEASES_ID:-nexus-releases}"
ICOMP_VER="${ELEVATOR_ICOMP_VER:-2.9.2-xinghewan}"
CW_LIB="${ELEVATOR_CW_LIB_DIR:-$REPO_ROOT/cw-elevator-application-V1.0.0.20211103/cw_lib}"
PARENT_STUB="$SCRIPT_DIR/legacy-poms/intelligent-cwoscomponent-2.9.2-xinghewan-parent.pom"
die() { echo "ERROR: $*" >&2; exit 1; }
[[ -f "$PARENT_STUB" ]] || die "缺少父 POM 桩: $PARENT_STUB"
[[ -d "$CW_LIB" ]] || die "cw_lib 目录不存在: $CW_LIB"
IJ="$CW_LIB/intelligent-cwoscomponent-interface-${ICOMP_VER}.jar"
IP="$CW_LIB/intelligent-cwoscomponent-interface-${ICOMP_VER}.pom"
RJ="$CW_LIB/intelligent-cwoscomponent-rest-${ICOMP_VER}.jar"
RP="$CW_LIB/intelligent-cwoscomponent-rest-${ICOMP_VER}.pom"
for f in "$IJ" "$IP" "$RJ" "$RP"; do
[[ -f "$f" ]] || die "缺少文件: $f"
done
# 在无 reactor pom 的目录执行 deploy:deploy-file,避免误挂到 cw-elevator-application-reactor。
# Release 仓库已存在同版本时 Nexus 返回 400 cannot be updated — 视为已发布,跳过。
deploy_file_or_skip() {
local label="$1"
shift
if [[ "${DRY_RUN:-}" == "1" ]]; then
echo "DRY-RUN [$label]:" "$@"
return 0
fi
local out rc
set +e
out="$(cd "$CW_LIB" && mvn -B -q deploy:deploy-file "$@" 2>&1)"
rc=$?
set -e
if [[ "$rc" -eq 0 ]]; then
echo "OK [$label]"
return 0
fi
if echo "$out" | grep -qE 'cannot be updated|status code: 400'; then
echo "SKIP [$label]Nexus 已存在该 release 坐标,不可覆盖)"
return 0
fi
echo "$out" >&2
return "$rc"
}
echo "==> Nexus releases: $NEXUS_RELEASES_URL (id=$NEXUS_RELEASES_ID)"
echo "==> cw_lib: $CW_LIB"
echo "==> [1/3] deploy 父 POM cn.cloudwalk.intelligent:intelligent-cwoscomponent:${ICOMP_VER}"
deploy_file_or_skip "parent pom" \
-DrepositoryId="$NEXUS_RELEASES_ID" \
-Durl="$NEXUS_RELEASES_URL" \
-Dfile="$PARENT_STUB" \
-DpomFile="$PARENT_STUB" \
-DgroupId=cn.cloudwalk.intelligent \
-DartifactId=intelligent-cwoscomponent \
-Dversion="$ICOMP_VER" \
-Dpackaging=pom
echo "==> [2/3] deploy intelligent-cwoscomponent-interface"
deploy_file_or_skip "interface" \
-DrepositoryId="$NEXUS_RELEASES_ID" \
-Durl="$NEXUS_RELEASES_URL" \
-Dfile="$IJ" \
-DpomFile="$IP" \
-Dpackaging=jar
echo "==> [3/3] deploy intelligent-cwoscomponent-rest"
deploy_file_or_skip "rest" \
-DrepositoryId="$NEXUS_RELEASES_ID" \
-Durl="$NEXUS_RELEASES_URL" \
-Dfile="$RJ" \
-DpomFile="$RP" \
-Dpackaging=jar
if [[ "${DRY_RUN:-}" == "1" ]]; then
echo "DRY-RUN 结束。"
exit 0
fi
if [[ "${ELEVATOR_INSTALL_LOCAL_M2:-1}" == "1" ]]; then
echo "==> 本机 ~/.m2:父 POM 桩 + install-file(与 build_nexus_only 闭包一致)"
mvn -B -q -N -f "$PARENT_STUB" install -DskipTests
mvn -B -q org.apache.maven.plugins:maven-install-plugin:3.1.1:install-file \
-DpomFile="$IP" -Dfile="$IJ" -Dpackaging=jar
mvn -B -q org.apache.maven.plugins:maven-install-plugin:3.1.1:install-file \
-DpomFile="$RP" -Dfile="$RJ" -Dpackaging=jar
else
echo "==> 跳过本机 ~/.m2ELEVATOR_INSTALL_LOCAL_M2=0"
fi
echo "完成。"
echo " ~/.m2: ls \"\$HOME/.m2/repository/cn/cloudwalk/intelligent/intelligent-cwoscomponent-rest/$ICOMP_VER/\""
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.18.RELEASE</version>
<relativePath/>
</parent>
<groupId>cn.cloudwalk.intelligent</groupId>
<artifactId>intelligent-cwoscomponent</artifactId>
<version>2.9.2-xinghewan</version>
<packaging>pom</packaging>
<name>intelligent-cwoscomponent (2.9.2 parent stub)</name>
<description>与 V1 cw_lib 内 intelligent-cwoscomponent-*.pom 中 parent 坐标一致;DM 与电梯 reactor 3.7.2 / Greenwich.SR6 对齐,供 install-file 安装到本机库或隔离库。</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR6</spring-cloud.version>
<cloudwalk.legacy.public.version>3.7.2-Brussels-SRX</cloudwalk.legacy.public.version>
<cloudwalk.internal.version>3.7.2-Brussels-SRX</cloudwalk.internal.version>
<fastjson.version>1.2.73</fastjson.version>
<cloudwalk.device.sdk.version>2.2.0</cloudwalk.device.sdk.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cloudwalk-common-result</artifactId>
<version>${cloudwalk.legacy.public.version}</version>
</dependency>
<dependency>
<groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cloudwalk-common-service</artifactId>
<version>${cloudwalk.internal.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>cn.cloudwalk</groupId>
<artifactId>cloudwalk-device-sdk-protocol-entity</artifactId>
<version>${cloudwalk.device.sdk.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
@@ -29,6 +29,6 @@ echo " (对拍为 HTTP; 需另开终端分别 java -jar --server.port=18080/
echo "==> 单元(无联调): test_unit_compare" echo "==> 单元(无联调): test_unit_compare"
export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
(cd "$TOOL" && python3 -m pytest tests/test_unit_compare.py -q --tb=short) (cd "$TOOL" && python3 -m pytest tests/test_unit_compare.py -q --tb=short)
echo "==> 对拍(无两实例时跳过 test_parity_endpoints): 全部" echo "==> 对拍(无两实例时跳过 test_parity_endpoints);不含单机 smoke"
(cd "$TOOL" && python3 -m pytest tests/ -q --tb=line) (cd "$TOOL" && python3 -m pytest tests/test_unit_compare.py tests/test_parity_endpoints.py -q --tb=line)
echo "报告: $TOOL/report/ (对拍有执行且成功时由 pytest 会话写出 parity-*.md)" echo "报告: $TOOL/report/ (对拍有执行且成功时由 pytest 会话写出 parity-*.md)"
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# 完整接口测试:V1/V2 单机冒烟 + 双端对拍 + 套件总览 Markdown。
# 前置:两 JAR 已分别启动(默认 http://127.0.0.1:18080 为 V1、18081 为 V2),且配置同一数据源/Redis。
# 用法:在 maven-cw-elevator-application 目录执行 ./scripts/run_full_elevator_api_suite.sh
set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
TOOL="${REPO}/tools/elevator_api_parity"
MARKER="${TOOL}/.suite_run_marker"
export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
export PIP_DISABLE_PIP_VERSION_CHECK=1
export ELEVATOR_BASE_OLD="${ELEVATOR_BASE_OLD:-http://127.0.0.1:18080}"
export ELEVATOR_BASE_NEW="${ELEVATOR_BASE_NEW:-http://127.0.0.1:18081}"
cd "$TOOL"
python3 -m pip install -q -r requirements.txt 2>/dev/null || true
touch "$MARKER"
# -rs 在结束时打印 skip 原因;s=skip 通常表示目标端口无服务或 /actuator/health 无 200
PTF="-q -rs --tb=line"
echo "==> 单元(compare 逻辑)"
python3 -m pytest tests/test_unit_compare.py -q --tb=short
echo "==> 冒烟 V1 ($ELEVATOR_BASE_OLD) [需该地址可访问 /actuator/health 等健康端点]"
python3 -m pytest tests/test_smoke_catalog.py -m smoke \
--smoke-base="$ELEVATOR_BASE_OLD" --smoke-label=v1_legacy $PTF || true
echo "==> 冒烟 V2 ($ELEVATOR_BASE_NEW) [需该地址可访问 /actuator/health 等健康端点]"
python3 -m pytest tests/test_smoke_catalog.py -m smoke \
--smoke-base="$ELEVATOR_BASE_NEW" --smoke-label=v2_build $PTF || true
echo "==> 横向对拍(两实例均需通过健康探针)"
python3 -m pytest tests/test_parity_endpoints.py -m live $PTF \
--base-old="$ELEVATOR_BASE_OLD" --base-new="$ELEVATOR_BASE_NEW" || true
SUITE_TS="$(date +%Y%m%d-%H%M%S)"
_pick_newer() {
local pattern="$1"
local f=""
for f in $(ls -t "${TOOL}/report"/${pattern} 2>/dev/null); do
if [[ -f "$f" && "$f" -nt "$MARKER" ]]; then echo "$f"; return; fi
done
}
SMOKE_V1="$(_pick_newer 'smoke-v1_legacy-*.md')"
SMOKE_V2="$(_pick_newer 'smoke-v2_build-*.md')"
PARITY="$(_pick_newer 'parity-*.md')"
OUT="${TOOL}/report/SUITE-${SUITE_TS}.md"
python3 report/generate_suite_summary.py --out "$OUT" \
--catalog "${TOOL}/api_catalog.json" \
${PARITY:+--parity "$PARITY"} \
${SMOKE_V1:+--smoke-v1 "$SMOKE_V1"} \
${SMOKE_V2:+--smoke-v2 "$SMOKE_V2"}
echo "==> 套件总览: $OUT"
if [[ -z "$SMOKE_V1" && -z "$SMOKE_V2" && -z "$PARITY" ]]; then
echo "==> 提示: 未生成 smoke/parity 子报告(多为本机未起 JAR 或健康检查未通过)。"
echo " 先起两进程: java -jar V1.jar --server.port=18080 --spring.config.location=... 与 V2 用 18081"
echo " 快速探活: curl -s -o /dev/null -w '%{http_code}' $ELEVATOR_BASE_OLD/actuator/health"
fi
rm -f "$MARKER"
@@ -2,3 +2,7 @@ __pycache__/
*.pyc *.pyc
.pytest_cache/ .pytest_cache/
report/parity-*.md report/parity-*.md
report/smoke-*.md
report/SUITE-*.md
report/*.json
.suite_run_marker
@@ -1,44 +1,92 @@
# elevator_api_parity — 新旧 JAR 接口对拍 # elevator_api_parity — V1/V2 接口冒烟与对拍
## 功能概览
| 能力 | 说明 |
|------|------|
| **单机冒烟** | 对单个 Base URL 遍历 `api_catalog.json` 中的接口(`include_in_smoke=true`),记录 HTTP 状态、耗时、业务 `code`、响应摘要 → `report/smoke-{label}-*.md` |
| **横向对拍** | 旧 JAR`--base-old`)与新 JAR`--base-new`)并行调用;仅 **`include_in_parity=true`** 的条目参与 **HTTP 状态 + 业务 code** 一致性断言 → `report/parity-*.md` |
| **套件总览** | 合并本次产生的冒烟×2 + 对拍 → `report/SUITE-*.md` |
接口清单与请求体见 **`api_catalog.json`**(支持 `fixture` 文件或内联 `body`)。
## 环境 ## 环境
- Python 3.8+ - Python 3.8+`pip install -r requirements.txt`
- 安装:`pip install -r requirements.txt`(在**本目录**下执行) - 若遇全局 pytest 插件冲突:`export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`
## 环境说明 ## 启动两实例(示例)
本机若安装过 `allure_pytest` 等全局 pytest 插件且与 Python 版本冲突,请执行 同一套 `application.properties`(或外部配置),仅端口不同
`export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1``run_elevator_parity.sh` 已自动设置)后再跑 `pytest`
## 快速使用
1. 构建新 JAR(在 reactor 根目录、JDK8
`mvn -DskipTests clean package`
得到:`../cw-elevator-application-starter/target/cw-elevator-application-2.0.0.jar`
2. 将历史 JAR 放到 `cw-elevator-application-V1.0.0.20211103/cw-elevator-application-V1.0.0.20211103.jar` 或设 `ELEVATOR_JAR_LEGACY`
3. 准备**与现网相同**的 `application.yml`(或目录),如 `ELEVATOR_SPRING_CONFIG=file:/path/to/application-elevator.yml`
4. 从仓库**反编译**根或本目录执行:
`../../scripts/run_elevator_parity.sh`(见该脚本内环境变量说明)
或手动启动两实例后:
```bash ```bash
export ELEVATOR_BASE_OLD=http://127.0.0.1:18080 export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
export ELEVATOR_BASE_NEW=http://127.0.0.1:18081 CFG="file:/path/to/application.properties"
export ELEVATOR_HEADER_BUSINESSID=... # 与现网/联调一致 java -jar cw-elevator-application-V1.0.0.20211103.jar --server.port=18080 --spring.config.location=$CFG
# ... java -jar cw-elevator-application-2.0.0.jar --server.port=18081 --spring.config.location=$CFG
cd tools/elevator_api_parity
python -m pytest tests/ -v --tb=short
``` ```
## 配置 ## 一键完整套件(推荐)
- `api_catalog.json`:对拍端点、方法、fixture 文件名、说明。 **`maven-cw-elevator-application`** 目录:
- `fixtures/*.json`:各接口请求体。
- 归一化忽略键可扩展 `parity/compare.py``DEFAULT_STRIP_PATHS`(如 `data.rows` 内动态字段,谨慎)。
## 报告 ```bash
./scripts/run_full_elevator_api_suite.sh
```
- `report/parity-YYYYMMDD-HHMMSS.md`:实跑时由 `conftest` 的 session hook 与 `report/generate_report.py` 组合生成。 环境变量(可选):
- JUnit(可选):`--junitxml=report/junit.xml`
- `ELEVATOR_BASE_OLD` — 默认 `http://127.0.0.1:18080`V1
- `ELEVATOR_BASE_NEW` — 默认 `http://127.0.0.1:18081`V2
- `ELEVATOR_HEADER_BUSINESSID``ELEVATOR_HEADER_LOGINID``ELEVATOR_HEADER_AUTHORIZATION` 等 — 与现网一致时业务码更有可比性
## 分步执行
```bash
cd tools/elevator_api_parity
export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
# 仅逻辑单测
python3 -m pytest tests/test_unit_compare.py -q
# V1 单机冒烟
python3 -m pytest tests/test_smoke_catalog.py -m smoke \
--smoke-base=http://127.0.0.1:18080 --smoke-label=v1_legacy -q
# V2 单机冒烟
python3 -m pytest tests/test_smoke_catalog.py -m smoke \
--smoke-base=http://127.0.0.1:18081 --smoke-label=v2_build -q
# 双端对拍(双端 /actuator/health 等可达时执行,否则跳过)
python3 -m pytest tests/test_parity_endpoints.py -m live -q \
--base-old=http://127.0.0.1:18080 --base-new=http://127.0.0.1:18081
```
### 强制要求联调(失败即中断)
```bash
export ELEVATOR_PARITY_REQUIRE_LIVE=1 # 对拍
export ELEVATOR_SMOKE_REQUIRE=1 # 冒烟
```
## 仅对拍(不含冒烟)
```bash
./scripts/run_elevator_parity.sh
```
(脚本内会先 `mvn package`,再跑单测 + 对拍。)
## 报告位置
- `report/smoke-v1_legacy-*.md` + **同名 `.json`**(结构化结果,供套件矩阵消费)
- `report/smoke-v2_build-*.md` + **同名 `.json`**
- `report/parity-*.md` + **同名 `.json`**
- **`report/SUITE-*.md`**:始终包含 **第二节「全量接口清单」**(来源于 `api_catalog.json`);**第三节「测试结果矩阵」** 在有上述 JSON 时填入 V1/V2 HTTP、业务 code、对拍 Y/N;若本次因未起服务而 **skip**,矩阵中为 **—**(参见第三节说明)。
## 扩展接口
编辑 **`api_catalog.json`**
- `include_in_parity: false` — 只做冒烟,不参与 V1/V2 等值断言(避免依赖不一致导致误报)。
- `include_in_parity: true` — 纳入横向对拍(当前默认对 **访客/人员/规则分页/记录分页** 四个核心场景开启)。
@@ -1,21 +1,26 @@
{ {
"version": 1, "version": 2,
"description": "电梯应用 HTTP 接口清单:冒烟与对拍共用。include_in_parity=false 的项仅做单机路由/业务响应探测,不参与 V1/V2 等值断言。",
"endpoints": [ "endpoints": [
{ {
"id": "person_add_visitor_min", "id": "person_add_visitor_min",
"name": "访客派梯-最小体(多依赖业务失败,仅比对 HTTP+业务code)", "name": "访客派梯-最小体",
"method": "POST", "method": "POST",
"path": "/elevator/person/add/visitor", "path": "/elevator/person/add/visitor",
"fixture": "person_add_visitor_min.json", "fixture": "person_add_visitor_min.json",
"compare_mode": "code_only" "compare_mode": "code_only",
"include_in_parity": true,
"include_in_smoke": true
}, },
{ {
"id": "person_detail", "id": "person_detail",
"name": "人员详情-最小分页", "name": "人员详情-分页",
"method": "POST", "method": "POST",
"path": "/elevator/person/detail", "path": "/elevator/person/detail",
"fixture": "person_detail_min.json", "fixture": "person_detail_min.json",
"compare_mode": "code_only" "compare_mode": "code_only",
"include_in_parity": true,
"include_in_smoke": true
}, },
{ {
"id": "passrule_page", "id": "passrule_page",
@@ -23,7 +28,9 @@
"method": "POST", "method": "POST",
"path": "/elevator/passRule/page", "path": "/elevator/passRule/page",
"fixture": "passrule_page.json", "fixture": "passrule_page.json",
"compare_mode": "code_only" "compare_mode": "code_only",
"include_in_parity": true,
"include_in_smoke": true
}, },
{ {
"id": "record_page", "id": "record_page",
@@ -31,7 +38,399 @@
"method": "POST", "method": "POST",
"path": "/intelligent/acs/elevator/record/page", "path": "/intelligent/acs/elevator/record/page",
"fixture": "record_page.json", "fixture": "record_page.json",
"compare_mode": "code_only" "compare_mode": "code_only",
"include_in_parity": true,
"include_in_smoke": true
},
{
"id": "person_add",
"name": "人员-从现有人员添加",
"method": "POST",
"path": "/elevator/person/add",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "person_edit",
"name": "人员-编辑",
"method": "POST",
"path": "/elevator/person/edit",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "person_delete",
"name": "人员-删除",
"method": "POST",
"path": "/elevator/person/delete",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "person_page",
"name": "人员-分页",
"method": "POST",
"path": "/elevator/person/page",
"body": { "pageNo": 1, "pageSize": 1 },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "person_time_detail",
"name": "人员-时间详情",
"method": "POST",
"path": "/elevator/person/timeDetail",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "person_page_by_app",
"name": "人员-应用分页",
"method": "POST",
"path": "/elevator/person/pageByApp",
"body": { "pageNo": 1, "pageSize": 1 },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "passrule_floor",
"name": "通行规则-楼层列表",
"method": "POST",
"path": "/elevator/passRule/floor",
"body": { "pageNo": 1, "pageSize": 1, "zoneId": "" },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "passrule_add",
"name": "通行规则-新增",
"method": "POST",
"path": "/elevator/passRule/add",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "passrule_edit",
"name": "通行规则-修改",
"method": "POST",
"path": "/elevator/passRule/edit",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "passrule_delete",
"name": "通行规则-删除",
"method": "POST",
"path": "/elevator/passRule/delete",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "passrule_detail",
"name": "通行规则-详情",
"method": "POST",
"path": "/elevator/passRule/detail",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "passrule_image",
"name": "通行规则-按人像查楼层权限",
"method": "POST",
"path": "/elevator/passRule/image",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "passrule_image_list",
"name": "通行规则-批量人像",
"method": "POST",
"path": "/elevator/passRule/image/list",
"body": { "personList": [] },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "record_analyse_cycle",
"name": "记录-周期统计",
"method": "POST",
"path": "/intelligent/acs/elevator/record/analyse/cycle",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "record_analyse_count",
"name": "记录-次数统计",
"method": "POST",
"path": "/intelligent/acs/elevator/record/analyse/count",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "record_page_request",
"name": "记录-请求分页",
"method": "POST",
"path": "/intelligent/acs/elevator/record/page/request",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "record_device_list",
"name": "记录-设备列表",
"method": "POST",
"path": "/intelligent/acs/elevator/record/device/list",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "record_zone_tree",
"name": "记录-区域树",
"method": "POST",
"path": "/intelligent/acs/elevator/record/zone/tree",
"body": { "parentId": "", "businessId": "" },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "device_v2_39201",
"name": "设备网关-39201 设备列表",
"method": "POST",
"path": "/device/v2/39201",
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "device_v2_39202",
"name": "设备网关-39202 区域电梯码",
"method": "POST",
"path": "/device/v2/39202",
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "device_v2_39203",
"name": "设备网关-39203 添加记录",
"method": "POST",
"path": "/device/v2/39203",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "device_v2_39204",
"name": "设备网关-39204 密钥时间戳(已废弃)",
"method": "POST",
"path": "/device/v2/39204",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_add",
"name": "设备-新增",
"method": "POST",
"path": "/elevator/device/add",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_edit",
"name": "设备-编辑",
"method": "POST",
"path": "/elevator/device/edit",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_get_by_id",
"name": "设备-按ID查询",
"method": "POST",
"path": "/elevator/device/getById",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_delete",
"name": "设备-删除",
"method": "POST",
"path": "/elevator/device/delete",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_edit_code",
"name": "设备-改码",
"method": "POST",
"path": "/elevator/device/editCode",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_get",
"name": "设备-查询列表",
"method": "POST",
"path": "/elevator/device/get",
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_export",
"name": "设备-导出",
"method": "POST",
"path": "/elevator/device/export",
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_zone_tree_code",
"name": "设备-区域树编码",
"method": "POST",
"path": "/elevator/device/zone/treeCode",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "elevator_device_page",
"name": "设备-分页",
"method": "POST",
"path": "/elevator/device/devicePage",
"body": { "deviceName": "_api_probe", "currentPage": 1, "rowsOfPage": 10 },
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "restructure_unbind_floors",
"name": "改造-未绑定楼层",
"method": "POST",
"path": "/elevator/restructure/unbind/floors",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "restructure_floors",
"name": "改造-楼层列表",
"method": "POST",
"path": "/elevator/restructure/floors",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "restructure_condition",
"name": "改造-条件",
"method": "POST",
"path": "/elevator/restructure/condition",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "restructure_condition_labels",
"name": "改造-条件标签",
"method": "POST",
"path": "/elevator/restructure/condition/labels",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "restructure_binding",
"name": "改造-绑定",
"method": "POST",
"path": "/elevator/restructure/binding",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "restructure_binding_person",
"name": "改造-绑定人员",
"method": "POST",
"path": "/elevator/restructure/binding/person",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "restructure_task_progress",
"name": "改造-任务进度",
"method": "POST",
"path": "/elevator/restructure/task/progress",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
},
{
"id": "restructure_task_stop",
"name": "改造-停止任务",
"method": "POST",
"path": "/elevator/restructure/task/stop",
"body": {},
"compare_mode": "code_only",
"include_in_parity": false,
"include_in_smoke": true
} }
] ]
} }
@@ -1,18 +1,20 @@
from __future__ import annotations from __future__ import annotations
import os import os
from datetime import datetime
from pathlib import Path from pathlib import Path
import pytest import pytest
import requests import requests
from parity.client import can_reach_both, default_headers from parity.client import can_reach_both, can_reach_one, default_headers
_DIR = Path(__file__).resolve().parent _DIR = Path(__file__).resolve().parent
def pytest_configure(config): def pytest_configure(config):
config._parity_rows = [] # type: ignore[attr-defined] config._parity_rows = [] # type: ignore[attr-defined]
config._smoke_rows = [] # type: ignore[attr-defined]
def pytest_addoption(parser): def pytest_addoption(parser):
@@ -24,6 +26,14 @@ def pytest_addoption(parser):
"--base-new", "--base-new",
default=os.environ.get("ELEVATOR_BASE_NEW", "http://127.0.0.1:18081"), default=os.environ.get("ELEVATOR_BASE_NEW", "http://127.0.0.1:18081"),
) )
parser.addoption(
"--smoke-base",
default=os.environ.get("ELEVATOR_SMOKE_BASE", "http://127.0.0.1:18080"),
)
parser.addoption(
"--smoke-label",
default=os.environ.get("ELEVATOR_SMOKE_LABEL", "v1_legacy"),
)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@@ -36,6 +46,16 @@ def base_new(request):
return str(request.config.getoption("--base-new")).rstrip("/") return str(request.config.getoption("--base-new")).rstrip("/")
@pytest.fixture(scope="session")
def smoke_base(request):
return str(request.config.getoption("--smoke-base")).rstrip("/")
@pytest.fixture(scope="session")
def smoke_label(request):
return str(request.config.getoption("--smoke-label"))
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def session_http(): def session_http():
s = requests.Session() s = requests.Session()
@@ -55,27 +75,56 @@ def two_instances_ready(base_old, base_new, session_http, request):
return True return True
@pytest.fixture(scope="session")
def smoke_instance_ready(smoke_base, session_http, request):
ok, _ = can_reach_one(smoke_base, session_http)
require = os.environ.get("ELEVATOR_SMOKE_REQUIRE", "")
if not ok and not require:
pytest.skip(f"单机 {smoke_base} 健康检查不通过(跳过 smoke)")
if not ok and require:
pytest.fail(f"ELEVATOR_SMOKE_REQUIRE=1 且 {smoke_base} 不可达")
return True
def _write_smoke_report(config, srows: list, report_dir: Path) -> None:
from report import generate_smoke_report
label = str(config.getoption("--smoke-label", default="smoke"))
p2 = report_dir / f"smoke-{label}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.md"
generate_smoke_report.write_file(
p2,
str(config.getoption("--smoke-base", default="")),
label,
srows,
)
print(f"\n[smoke] 报告: {p2}")
def pytest_sessionfinish(session, exitstatus): def pytest_sessionfinish(session, exitstatus):
rows = getattr(session.config, "_parity_rows", None) import importlib
if not rows:
return
try:
from report import generate_report
p = _DIR / "report" / generate_report.timestamped_name("parity") config = session.config
p.parent.mkdir(parents=True, exist_ok=True) report_dir = _DIR / "report"
generate_report.write_file( report_dir.mkdir(parents=True, exist_ok=True)
p,
str(session.config.getoption("--base-old", default="")),
str(session.config.getoption("--base-new", default="")),
rows,
)
print(f"\n[parity] 报告: {p}")
except Exception as e:
print(f"\n[parity] 报告未生成: {e}")
rows = getattr(config, "_parity_rows", None) or []
if rows:
try:
gen = importlib.import_module("report.generate_report")
p = report_dir / gen.timestamped_name("parity")
gen.write_file(
p,
str(config.getoption("--base-old", default="")),
str(config.getoption("--base-new", default="")),
rows,
)
print(f"\n[parity] 对拍报告: {p}")
except Exception as e:
print(f"\n[parity] 对拍报告未生成: {e}")
def load_catalog() -> dict: srows = getattr(config, "_smoke_rows", None) or []
from parity import catalog_loader if srows:
try:
return catalog_loader.load() _write_smoke_report(config, srows, report_dir)
except Exception as e:
print(f"\n[smoke] 报告未生成: {e}")
@@ -1,8 +1,42 @@
import json import json
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional
_ROOT = Path(__file__).resolve().parent.parent _ROOT = Path(__file__).resolve().parent.parent
def load() -> dict: def load() -> dict:
return json.loads((_ROOT / "api_catalog.json").read_text(encoding="utf-8")) return json.loads((_ROOT / "api_catalog.json").read_text(encoding="utf-8"))
def endpoint_body(ep: Dict[str, Any]) -> Dict[str, Any]:
"""Resolve request JSON: inline ``body`` wins, else load ``fixture`` file."""
if ep.get("body") is not None:
return dict(ep["body"])
fix = ep.get("fixture")
if fix:
p = _ROOT / "fixtures" / fix
return json.loads(p.read_text(encoding="utf-8"))
return {}
def iter_endpoints(catalog: dict, *, tag: Optional[str] = None) -> list[dict]:
"""If ``tag`` is set, only endpoints with ``tags`` containing it (or legacy entries with no tags = all)."""
out: list[dict] = []
for ep in catalog.get("endpoints", []):
tags = ep.get("tags") or []
if tag is None:
out.append(ep)
elif not tags:
out.append(ep)
elif tag in tags:
out.append(ep)
return out
def include_in_parity(ep: dict) -> bool:
return bool(ep.get("include_in_parity", True))
def include_in_smoke(ep: dict) -> bool:
return bool(ep.get("include_in_smoke", True))
@@ -2,6 +2,7 @@ from __future__ import annotations
import json import json
import os import os
import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional from typing import Any, Optional
@@ -124,6 +125,52 @@ def _safe_json(text: str) -> Any:
return None return None
def call_single(
name: str,
method: str,
path: str,
body: Any,
base_url: str,
session: requests.Session | None = None,
) -> dict[str, Any]:
"""One HTTP call for smoke coverage; returns a flat dict for reporting."""
s = session or requests.Session()
h = default_headers()
url = base_url.rstrip("/") + path
data = (
None
if body is None
else (body if isinstance(body, str) else json.dumps(body, ensure_ascii=False))
)
m = method.upper()
t0 = time.perf_counter()
if m == "GET":
r = s.get(url, headers=h, timeout=120)
else:
r = s.post(url, headers=h, data=data, timeout=120)
elapsed_ms = int((time.perf_counter() - t0) * 1000)
txt = r.text or ""
oj = _safe_json(txt)
bc = compare.business_code(oj) if oj is not None else None
head = txt[:400].replace("\n", " ")
return {
"name": name,
"method": m,
"path": path,
"http_status": r.status_code,
"elapsed_ms": elapsed_ms,
"business_code": bc,
"response_head": head,
"reachable": True,
}
def can_reach_one(base_url: str, s: requests.Session | None = None) -> tuple[bool, str]:
s = s or requests.Session()
_, ok, _ = probe_healthy(base_url, s)
return ok, base_url
def can_reach_both( def can_reach_both(
base_old: str, base_new: str, s: requests.Session | None = None base_old: str, base_new: str, s: requests.Session | None = None
) -> tuple[bool, str]: ) -> tuple[bool, str]:
@@ -4,5 +4,6 @@ addopts = -q --strict-markers
testpaths = tests testpaths = tests
markers = markers =
live: 需要两实例可访问 live: 需要两实例可访问
smoke: 单机全量 HTTP 探测
unit: 纯逻辑单测 unit: 纯逻辑单测
pythonpath = . pythonpath = .
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import json
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List from typing import List
@@ -40,3 +41,16 @@ def write_file(
f"\n## 汇总\n- 通过: {ok},不一致: {bad}\n- **上线前请人工与联调/业务确认**。\n" f"\n## 汇总\n- 通过: {ok},不一致: {bad}\n- **上线前请人工与联调/业务确认**。\n"
) )
out_path.write_text("".join(lines), encoding="utf-8") out_path.write_text("".join(lines), encoding="utf-8")
payload = {
"meta": {
"generated_at": datetime.now().isoformat(),
"base_old": base_old,
"base_new": base_new,
"markdown": str(out_path.resolve()),
},
"rows": rows,
"summary": {"match_ok": ok, "match_bad": bad},
}
json_path = out_path.with_suffix(".json")
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
@@ -0,0 +1,53 @@
from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from typing import List
def write_file(
out_path: Path,
base_url: str,
label: str,
rows: List[dict],
) -> None:
lines = [
f"# 电梯应用 API 单机冒烟报告 — {label}\n",
f"- **时间**: {datetime.now().isoformat()}\n",
f"- **Base URL**: {base_url}\n",
"\n## 接口探测\n\n",
"| 用例 | 方法+路径 | HTTP | ms | 业务code | 响应摘要 |\n",
"| ---- | -------- | ---- | -- | -------- | -------- |\n",
]
ok_http = 0
for r in rows:
name = r.get("name", "")
pth = f"{r.get('method', '')} {r.get('path', '')}"
hs = r.get("http_status", "")
ms = r.get("elapsed_ms", "")
bc = r.get("business_code", "") or ""
head = (r.get("response_head", "") or "").replace("|", "\\|")[:180]
if isinstance(hs, int) and 200 <= hs < 300:
ok_http += 1
lines.append(f"| {name} | `{pth}` | {hs} | {ms} | {bc} | {head} |\n")
lines.append(
f"\n## 汇总\n"
f"- 用例数: {len(rows)}HTTP 2xx 数量: {ok_http}\n"
f"- 业务失败(非 0 code)仍可能为**预期**(缺数据/缺 token);本报告仅证明路由可达且返回 Cloudwalk 风格 JSON。\n"
)
out_path.write_text("".join(lines), encoding="utf-8")
payload = {
"meta": {
"generated_at": datetime.now().isoformat(),
"base_url": base_url,
"label": label,
"markdown": str(out_path.resolve()),
},
"rows": rows,
"summary": {"total": len(rows), "http_2xx": ok_http},
}
out_path.with_suffix(".json").write_text(
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
)
@@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""合并电梯 API 测试套件总览:全量清单(catalog)+ 测试结果矩阵(JSON)+ 可选附录(子报告 MD)。"""
from __future__ import annotations
import argparse
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
def _tool_root() -> Path:
return Path(__file__).resolve().parent.parent
def _load_catalog(path: Path) -> List[dict]:
data = json.loads(path.read_text(encoding="utf-8"))
return list(data.get("endpoints") or [])
def _safe_json_load(p: Optional[Path]) -> Optional[dict]:
if not p or not p.is_file():
return None
try:
return json.loads(p.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
def _rows_by_id(rows: Any) -> Dict[str, dict]:
out: Dict[str, dict] = {}
if not isinstance(rows, list):
return out
for r in rows:
if not isinstance(r, dict):
continue
eid = r.get("id")
if not eid:
# 对拍行可能在 id 字段,否则用 path 不可靠
continue
out[str(eid)] = r
return out
def _parity_by_id(rows: Any) -> Dict[str, dict]:
out: Dict[str, dict] = {}
if not isinstance(rows, list):
return out
for r in rows:
if isinstance(r, dict) and r.get("id"):
out[str(r["id"])] = r
return out
def _resolve_json_arg(md_or_json: Optional[str]) -> Optional[Path]:
if not md_or_json:
return None
p = Path(md_or_json)
if p.suffix.lower() == ".json" and p.is_file():
return p
if p.suffix.lower() == ".md":
cand = p.with_suffix(".json")
if cand.is_file():
return cand
return None
def _markdown_table_catalog(endpoints: List[dict]) -> str:
lines = [
"| ID | 名称 | 方法 | Path | 冒烟 | 横向对拍 |\n",
"| ---- | ---- | ---- | ---- | ---- | -------- |\n",
]
for ep in endpoints:
eid = ep.get("id", "")
name = (ep.get("name") or "").replace("|", "\\|")
method = ep.get("method") or "POST"
path = (ep.get("path") or "").replace("|", "\\|")
sm = "" if ep.get("include_in_smoke", True) else ""
py = "" if ep.get("include_in_parity", True) else ""
lines.append(f"| `{eid}` | {name} | {method} | `{path}` | {sm} | {py} |\n")
return "".join(lines)
def _markdown_matrix(
endpoints: List[dict],
smoke_v1: Optional[dict],
smoke_v2: Optional[dict],
parity: Optional[dict],
) -> str:
m1 = _rows_by_id((smoke_v1 or {}).get("rows"))
m2 = _rows_by_id((smoke_v2 or {}).get("rows"))
mp = _parity_by_id((parity or {}).get("rows"))
meta_v1 = (smoke_v1 or {}).get("meta") or {}
meta_v2 = (smoke_v2 or {}).get("meta") or {}
meta_pr = (parity or {}).get("meta") or {}
head = ""
head += (
f"- **V1 冒烟数据来源**: {meta_v1.get('markdown') or meta_v1.get('base_url') or '(无 JSON,未执行或已跳过)'}\n"
)
head += (
f"- **V2 冒烟数据来源**: {meta_v2.get('markdown') or meta_v2.get('base_url') or '(无)'}\n"
)
head += f"- **对拍数据来源**: {meta_pr.get('markdown') or '(无)'}\n\n"
lines = [
head,
"### 测试结果矩阵(按 catalog `id` 对齐)\n\n",
"| catalog id | V1 HTTP | V1 code | V2 HTTP | V2 code | 对拍一致(仅参与对拍的条目) | 备注 |\n",
"| ---------- | ------- | ------- | ------- | ------- | ---------------------------- | ---- |\n",
]
for ep in endpoints:
eid = str(ep.get("id", ""))
include_p = ep.get("include_in_parity", True)
v1 = m1.get(eid)
v2 = m2.get(eid)
pr_row = mp.get(eid) if include_p else None
def cell_smoke(row: Optional[dict]) -> tuple[str, str]:
if not row:
return "", ""
return str(row.get("http_status", "")), str(row.get("business_code") or "")
h1, c1 = cell_smoke(v1)
h2, c2 = cell_smoke(v2)
if not include_p:
par_c = "(不参与)"
elif not parity:
par_c = "—(未执行)"
elif pr_row:
par_c = "**Y**" if pr_row.get("match") else "**N**"
if not pr_row.get("match"):
par_c += " " + (pr_row.get("message") or "")[:60].replace("|", "\\|")
else:
par_c = "—(本次对拍清单无此项)"
remark = ""
if v1 is None and v2 is None:
remark = "冒烟未执行或无该 id 结果"
lines.append(
f"| `{eid}` | {h1} | {c1} | {h2} | {c2} | {par_c} | {remark} |\n"
)
lines.append(
"\n**说明**`code` 为 CloudwalkResult 顶层业务码;HTTP 为传输层状态。"
"对拍列为 **Y** 表示旧/新 HTTP 状态一致且业务 code 一致(`code_only` 模式)。\n"
)
return "".join(lines)
def main() -> None:
ap = argparse.ArgumentParser(description="Generate SUITE markdown with catalog + matrix")
ap.add_argument("--out", required=True, help="Output SUITE-*.md path")
ap.add_argument("--catalog", help="api_catalog.json path")
ap.add_argument("--smoke-v1", help="smoke-v1_*.md 或同名 .json")
ap.add_argument("--smoke-v2", help="smoke-v2_*.md 或同名 .json")
ap.add_argument("--parity", help="parity-*.md 或同名 .json")
ap.add_argument(
"--embed-full",
action="store_true",
help="附录中嵌入子报告 Markdown 全文(较长)",
)
args = ap.parse_args()
root = _tool_root()
catalog_path = Path(args.catalog or (root / "api_catalog.json"))
endpoints = _load_catalog(catalog_path)
js_v1 = _resolve_json_arg(args.smoke_v1)
js_v2 = _resolve_json_arg(args.smoke_v2)
js_pr = _resolve_json_arg(args.parity)
doc_v1 = _safe_json_load(js_v1)
doc_v2 = _safe_json_load(js_v2)
doc_pr = _safe_json_load(js_pr)
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
lines: List[str] = [
"# 电梯应用 API 测试套件总览\n\n",
f"- **生成时间**: {datetime.now().isoformat()}\n",
f"- **清单来源**: `{catalog_path.resolve()}`(共 **{len(endpoints)}** 条接口定义)\n\n",
"## 1. 说明\n\n",
"- **全量清单**:第二节,来自 `api_catalog.json`,含是否参与冒烟/对拍。\n",
"- **测试结果矩阵**:第三节,与本次运行生成的 **JSON** 侧车文件对齐(与 `.md` 同名的 `.json`)。"
"若应用未启动导致 pytest **skip**,则无 JSON,矩阵中表现为 **—**。\n",
"- **横向对拍**:仅 `include_in_parity=true` 的条目会写入对拍 JSON 并参与对比。\n\n",
"## 2. 全量接口测试清单(catalog)\n\n",
_markdown_table_catalog(endpoints),
"\n",
"## 3. 测试结果矩阵\n\n",
_markdown_matrix(endpoints, doc_v1, doc_v2, doc_pr),
]
sec = 4
if args.embed_full:
for title, path_s in (
("V1 单机冒烟 Markdown", args.smoke_v1),
("V2 单机冒烟 Markdown", args.smoke_v2),
("横向对拍 Markdown", args.parity),
):
if not path_s:
continue
pp = Path(path_s)
if pp.suffix.lower() != ".md":
pp = pp.with_suffix(".md") if pp.with_suffix(".md").is_file() else pp
if not pp.is_file():
continue
lines.append(f"## {sec}. {title}\n\n")
lines.append(f"源文件: `{pp.resolve()}`\n\n---\n\n")
lines.append(pp.read_text(encoding="utf-8"))
lines.append("\n\n")
sec += 1
lines.append(
f"\n## {sec}. 原始报告路径(便于回放)\n\n"
f"- V1 冒烟: `{args.smoke_v1 or '(未生成)'}`\n"
f"- V2 冒烟: `{args.smoke_v2 or '(未生成)'}`\n"
f"- 对拍: `{args.parity or '(未生成)'}`\n"
f"- 同名 **`.json`** 与 `.md` 一并生成时可自动填充第三节矩阵。\n"
)
out.write_text("".join(lines), encoding="utf-8")
print(out.resolve())
if __name__ == "__main__":
main()
@@ -1,15 +1,11 @@
from __future__ import annotations from __future__ import annotations
import json
from pathlib import Path
import pytest import pytest
from parity.catalog_loader import endpoint_body as cb_body
from parity.catalog_loader import include_in_parity as cb_parity
from parity.catalog_loader import load as load_catalog from parity.catalog_loader import load as load_catalog
from parity.client import call_both from parity.client import call_both
_DIR = Path(__file__).resolve().parent.parent
_FIX = _DIR / "fixtures"
@pytest.mark.usefixtures("two_instances_ready") @pytest.mark.usefixtures("two_instances_ready")
@pytest.mark.live @pytest.mark.live
@@ -19,16 +15,13 @@ def test_parity_from_catalog(
base_new, base_new,
session_http, session_http,
): ):
"""按 api_catalog 双端对拍。compare_mode: deep | code_only | status_only""" """按 api_catalog 双端对拍(仅 ``include_in_parity`` 为 true 的条目)。compare_mode: deep | code_only | status_only"""
cat = load_catalog()["endpoints"] # type: ignore[operator] cat = load_catalog()["endpoints"] # type: ignore[operator]
for ep in cat: for ep in cat:
if not cb_parity(ep):
continue
name = ep["id"] name = ep["id"]
fixture = ep.get("fixture") body = cb_body(ep)
body: dict
if fixture:
body = json.loads((_FIX / fixture).read_text(encoding="utf-8"))
else:
body = {}
pr = call_both( pr = call_both(
name=ep.get("name", name), name=ep.get("name", name),
method=ep.get("method", "POST"), method=ep.get("method", "POST"),
@@ -0,0 +1,31 @@
from __future__ import annotations
import pytest
from parity.catalog_loader import endpoint_body as cb_body
from parity.catalog_loader import include_in_smoke as cb_smoke
from parity.catalog_loader import load as load_catalog
from parity.client import call_single
"""单机按 catalog 逐接口 POST/GET,写入 _smoke_rowssessionfinish 落盘)。"""
@pytest.mark.usefixtures("smoke_instance_ready")
@pytest.mark.smoke
def test_smoke_from_catalog(request, smoke_base, smoke_label, session_http):
cat = load_catalog()["endpoints"]
for ep in cat:
if not cb_smoke(ep):
continue
name = ep["id"]
body = cb_body(ep)
row = call_single(
name=ep.get("name", name),
method=ep.get("method", "POST"),
path=ep["path"],
body=body,
base_url=smoke_base,
session=session_http,
)
row["id"] = name
row["label"] = smoke_label
request.config._smoke_rows.append(row) # type: ignore
@@ -30,6 +30,19 @@
<groupId>cn.cloudwalk</groupId> <groupId>cn.cloudwalk</groupId>
<artifactId>cloudwalk-device-sdk-protocol-entity</artifactId> <artifactId>cloudwalk-device-sdk-protocol-entity</artifactId>
</dependency> </dependency>
<!-- 私服上 cloudwalk-common-result POM 常缺 dependencyManagement,传递依赖不生效;编译 interface 需下列包 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
+3 -2
View File
@@ -15,7 +15,7 @@
<version>3.0.0-xinghewan</version> <version>3.0.0-xinghewan</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>intelligent-cwoscomponent (Maven reactor)</name> <name>intelligent-cwoscomponent (Maven reactor)</name>
<description>聚合模块:interface → rest。原父 intelligent-cwoscomponent 缺失</description> <description>聚合模块:interface → rest(反应堆版本 3.0.0-xinghewan)。注:maven-cw-elevator-application 上线口径固定依赖 cw_lib 的 2.9.2-xinghewan,不以此反应堆产物替代;见电梯 scripts/build_nexus_only.sh</description>
<modules> <modules>
<module>intelligent-cwoscomponent-parent</module> <module>intelligent-cwoscomponent-parent</module>
@@ -26,7 +26,8 @@
<properties> <properties>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR6</spring-cloud.version> <spring-cloud.version>Greenwich.SR6</spring-cloud.version>
<cloudwalk.internal.version>4.0.0-Brussels-SRX</cloudwalk.internal.version> <!-- 与 maven-cw-elevator-application / V1 cw_lib 对齐;4.0.0 常不在私服,会导致 intelligent-cwoscomponent-rest 无法解析 -->
<cloudwalk.internal.version>3.7.2-Brussels-SRX</cloudwalk.internal.version>
<cloudwalk.legacy.public.version>3.7.2-Brussels-SRX</cloudwalk.legacy.public.version> <cloudwalk.legacy.public.version>3.7.2-Brussels-SRX</cloudwalk.legacy.public.version>
<fastjson.version>1.2.83</fastjson.version> <fastjson.version>1.2.83</fastjson.version>
<!-- 与 V1 运行包 lib 内 cloudwalk-device-sdk-protocol-entity-2.2.0.jar 一致 --> <!-- 与 V1 运行包 lib 内 cloudwalk-device-sdk-protocol-entity-2.2.0.jar 一致 -->
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
systemctl restart ninca-qk-alarm-app.service systemctl restart ninca-qk-alarm-app.service
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# 转发至 maven-cw-elevator-application 内的一键 API 套件脚本。
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec bash "${ROOT}/maven-cw-elevator-application/scripts/run_full_elevator_api_suite.sh" "$@"