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
systemctl restart ninca-qk-alarm-app.service
#!/bin/bash
systemctl restart ninca-qk-alarm-app.service
@@ -43,10 +43,11 @@ management.health.db.enabled=false
cloudwalk.datafield.enable=true
cloudwalk.datafield.securityKey=d4b2aabc97394a12a27fc3cca6cd9ba1
cloudwalk.datafield.encrypt=AES
# redis\u914D\u7F6E
spring.redis.host=10.128.161.95
# 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
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.timeout=0
spring.redis.pool.max-active=10
@@ -57,9 +58,9 @@ spring.redis.pool.min-idle=0
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://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.password=1qaz!QAZ
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
@@ -3,7 +3,7 @@
> **范围**`/media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/星河湾星中星/反编译` 下 `maven-*` 目录内全部 `pom.xml`(**主聚合工程 5 个 + 补充反应堆 7 个**,合计 **44** 个 `pom.xml` 文件)。
> **说明**:子模块未单独声明 `<version>` 时,与**反应堆(reactor)父 POM** 的 `<version>` 一致。
> **生成方式**:走查各 `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-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` |
| intelligent 组件线 | **3.0.0-xinghewan** | intelligent 反应堆;电梯 `intelligent.cwoscomponent.version`ninca-crk `intelligent.cwoscomponent.rest.version` |
| 云从内部线版本 | **3.7.2**(电梯常用) / **4.0.0**(部分工程) | 电梯、ninca-qk-alarm `cloudwalk.internal.version` 多为 **3.7.2****cloudwalk-cloud / ninca-crk** 等仍可能 **4.0.0** |
| 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`** 反应堆版本 |
| 设备管理 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** |
@@ -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-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-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 优先级)。
> **走查代码根**`maven-cw-elevator-application/cw-elevator-application-service`2026-04-24 静态走查)。
> **说明**:下表「子任务数」指**与约定相关的 RPC/可优化循环次数上界**(随运行时数据规模变化);**状态**表示在**不扩展 Feign/HTTP 契约**前提下是否建议动代码。
> **排期(2026-04**`frontend/` 暂无可用前端工程;**凡需改前端、登记页、物业管理端 UI 的迭代,暂跳过**(详见 [docs/README 当前排期与范围](../README.md#当前排期与范围2026-04))。本文档仅跟踪**电梯等服务端**在约定下的走查与修正。
---
## 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.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.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** |
---
## 2. 数量小结(便于排期)
| 类型 | 计数符号 | 含义 |
|------|----------|------|
| **§3.1 可合并 RPC** | 由 N 降为 **1**(在语义确认后) | 规则批量删除场景收益最大 |
| **§3.2 并行度** | P | 人员多 ID 删除 |
| **§3.3 并行度** | F | 楼层列表人数统计 |
| **§3.4 并行度** | D | 设备绑图库 |
| **§3.5** | A + D | 异步任务按层调用 |
---
## 3. 迭代规划与下一迭代范围(不改 HTTP/Feign 签名)
### 冻结范围(2026-04-25
| 项 | 说明 |
|----|------|
| **约定 §3.1 全范围** | 未取得图库/通行对 `updateGroupPersonRef` 语义确认前,**不进行**与该约定相关的**任何**代码修正与优化(含 **合并 N→1** 及 §6.4 所述**仅 `isSuccess` 校验**小步),避免在无确认期分散实现与回滚成本。 |
| **恢复条件** | 图库书面或接口说明确认 + 在 [约定文档 §3.1](对外接口不变-远程调用与性能优化约定.md) 文末回填对接人、日期;再按 §1 表拆分 PR(合并与返回值校验可分步)。 |
| **前端相关** | 仓库**暂无可用前端**;所有需改**前端 / 登记页 / 物业端页面**的迭代**暂不排期、不执行**(与本文服务端走查无冲突;产品方案中 UI 类阶段见 [docs/README](../README.md#当前排期与范围2026-04))。 |
### 迭代 3**§3.5 `updateFloors`**
| 字段 | 内容 |
|------|------|
| **状态** | **走查与首轮修正已完成**(见 **§7**):`getById` 空防护、步骤级 `CloudwalkResult` 校验、`keepAliveSeconds` 绑定线程池。 |
| **约定锚点** | **§3.5** `AcsDeviceTaskServiceImpl#updateFloors` |
| **暂缓项** | 楼层有界并行**已**见 **§9**`AbortPolicy``catch` 语义、其它见 **§7.3**。 |
### 迭代 4**P1:§3.2 / §3.3 / §3.4 + 统一有界池**
| 字段 | 内容 |
|------|------|
| **状态** | **已实施**(实现说明与语义边界见 **§8**)。 |
| **线程池 Bean** | `elevatorRemoteBoundedExecutor``ElevatorRemoteIoExecutorConfig`),配置前缀 **`ninca.elevator.remote-io.pool`**(默认 core=max=**6**queue=512`CallerRunsPolicy`)。**未**与 `updateFloorsExecutor` 合并,避免异步任务与同步 RPC 抢同池。 |
| **公共能力** | `FeignThreadLocalUtil.callWithContext``cw-elevator-application-common`):子线程执行 Feign 前绑定/恢复 ThreadLocal 请求头。 |
### 迭代 5**§3.5 `updateFloors` 楼层有界并行**
| 字段 | 内容 |
|------|------|
| **状态** | **已实施**(见 **§9**):增楼列表、删楼列表在各自阶段内以最多 **6** 路**并发**执行 `add` / `addOnlyRule` / `delete` 等;**`BIND_DEVICES` 仍按原列表顺序**、每层成功**仍 +1** 重读任务行,与纯串行「进度语义」一致;子线程 Feign 经 `FeignThreadLocalUtil``ruleMap``zoneId` 时用 **`getOrDefault(…, "")`** 防 NPE。 |
| **不变更** | **§3.1 全冻结** 期间未改 `ImageRuleRefServiceImpl#delete``updateGroupPersonRef` 次数。 |
### 迭代 6 及以后(可选)
- **调参 / 观测**`ninca.elevator.remote-io.pool` 按环境压测;必要时为 `updateFloors` 批处理增加**指标/耗时日志**(不扩展 HTTP 响应)。
- **§3.1**:图库确认后再评估合并 N→1 与(若允许)`updateGroupPersonRef``isSuccess` 小步。
**已完成回顾**:迭代 1 — **§5**;迭代 2 — **§6**(§3.1 冻结);迭代 3 — **§7**;迭代 4 — **§8**;迭代 5 — **§9**。
---
## 4. 文档维护
| 项目 | 内容 |
|------|------|
| 更新触发 | `ImageRuleRefServiceImpl#delete``AcsPersonServiceImpl#delete``AcsPassRuleServiceImpl#listFloor` / `#addImageStore``AcsDeviceTaskServiceImpl#updateFloors` 任一处重构或签契约变更 |
| 结论回填 | 图库对 §3.1 的确认结论请写回 [约定文档 §3.1](对外接口不变-远程调用与性能优化约定.md) 文末建议行(对接人 + 日期) |
---
## 5. 迭代 1`AcsPassRuleServiceImpl#listFloor` 走查结论(§3.3 / §2.2
**走查日期**2026-04-25
**代码位置**`maven-cw-elevator-application/.../passrule/impl/AcsPassRuleServiceImpl.java` 方法 `listFloor`
| 检查项 | 现状 | 结论 |
|--------|------|------|
| `zoneService.tree` 返回值 | 已校验 `zoneTree.isSuccess()`,失败抛 `ServiceException` | **通过** |
| `acsPersonService.page` 返回值 | 循环内**未**校验 `page.isSuccess()`,直接 `page.getData()``totalRows` | **不通过**:违反约定 **§2.2**Feign 失败时 `getData()` 可能为 null,存在 **NPE** 风险,且可能把失败误当「0 人」 |
| `page.getData()` 空指针 | 未防护 | **不通过**:与上项合并修正 |
| `rowsOfPage` | 当前为 `10`,仅使用 `totalRows` | **建议**:改为 **`1`**(约定 §3.3:仅取总数,略减负载),**不改变** HTTP 响应字段 |
**评估结论(是否允许进入代码修正)****通过进入修正** — 仅补充与 `zoneTree` 分支一致的失败处理及空数据防护,**不**改变对外 JSON 字段语义;`rowsOfPage=1` 与现逻辑(只读 `totalRows`)等价。
**代码修正(已应用,2026-04-25**`AcsPassRuleServiceImpl#listFloor` — 增加 `page.isSuccess()` 失败抛错、`page.getData()` 为空时 `personNumber=0``CloudwalkPageInfo(1,1)` 仅取总数。
**修正实施后**:提交 **`e652eb3`**(分支 `v0.11`)。
---
## 6. 迭代 2`ImageRuleRefServiceImpl#delete` 与 §3.1 方案走查(仅评估,未改代码)
**走查日期**2026-04-25
**目标**:在全工作区定位**图库(intelligent 图库人员服务)**与**通行规则(电梯应用本地)**相关代码,审核「合并 `updateGroupPersonRef`」方案是否需图库侧语义确认后方可实施。
### 6.1 图库 / 通行相关代码位置(工作区)
| 层级 | 路径/符号 | 职责 |
|------|-----------|------|
| **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`(可同时只填部分字段) |
| **客户端封装** | `.../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) |
| **同文件其它 `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** 为主不同) |
| **HTTP 入口** | `cw-elevator-application-web/.../AcsPassRuleController.java` | `imageRuleRefService.delete` |
| **异步任务调用** | `AcsDeviceTaskServiceImpl#updateFloors` | 删楼层时多构造 `deleteParam.setIds(Collections.singletonList(ruleId))` **逐层**调 `imageRuleRefService.delete`;单次 `delete` 内仍可能 1 次或多次 `updateGroupPersonRef`(视该层 `ids` 数量) |
**说明**:本仓库**无** intelligent 图库服务端的 `updateGroupPersonRef` 业务实现源码,仅能依据 DTO 与调用方推断语义;**与图库/通行团队确认**仍属 §3.1 前置条件。
### 6.2 当前 `delete` 行为摘要(与合并相关)
- `imageStoreId` 来自 **`deviceImageStoreDao.getByBuildingId(param.getParentId())`**,整次批量删除共用同一图库。
- 对每个待删父规则 `id`:先读**该父规则下子规则**的 label/org(子行若带 label 则 `continue`,**同一子行不会同时写入 org**,与数据模型一致),再删父规则,再带着**本轮** `includeLabels` / `includeOrganizations` 调图库刷新。
- **`updateGroupPersonRef` 的返回值未做 `isSuccess` 校验**(违反约定 §2.2;与 `getImageStorePerson` 等分支不一致)。
### 6.3 约定中的「合并」方案审核
| 维度 | 结论 |
|------|------|
| **与现网 N 次调用的等价性** | 若图库侧语义为:在**给定 `imageStoreId`** 下,对传入的 **labelIds / organizationIds 集合**做**增量刷新或按维度重算引用**(各维度独立、与顺序无关),则「删库前汇总所有待删父规则的子维度 → **去重并集** → 删库完成后 **1 次** `updateGroupPersonRef`」与「每删一条父规则刷新其维度子集」在**最终一致**上通常等价。 |
| **必须向图库确认的风险** | 若远端实现为「以本次入参**覆盖/裁剪**图库可见范围」或依赖**调用顺序**产生副作用,则合并后的**单次并集**与 N 次**子集递进**可能不等价。约定文档 §3.1 所述「非破坏性刷新」即针对此。 |
| **`personIds``labelIds`/`organizationIds` 混用** | `PersonRuleServiceImpl``personIds` 路径;`ImageRuleRefServiceImpl#delete` 走 label/org。合并方案**不改变** `delete` 仅设 label/org 的现状;但若图库服务在**未传 `personIds`** 时对空列表有特殊含义,仍须一并确认。 |
| **空列表仍 RPC** | 当前循环在子规则为空时仍调用 `updateGroupPersonRef`(两列表皆空)。合并后是否**跳过空并集**可减少无效 RPC,但属于**行为微调**,若图库依赖「空刷」触发全量重算,须图库确认后再定。 |
| **不合并时的安全增量** | 在未获图库书面确认前,**仅**可为每次 `updateGroupPersonRef` 增加 **`isSuccess` 校验 + 失败抛 `ServiceException`**(及可选日志),**不改变** RPC 次数;与约定「退化为循环调用 + 返回值校验」一致。 |
### 6.4 评估结论(是否允许进入 §3.1「合并」类代码修正)
- **合并 N→1**:**不允许在图库/通行确认前实施** — 与 §1 表及约定 §3.1 前置条件一致。
- **仅返回值校验(及可选空并集跳过,若产品同意)**:**允许作为独立小步** — 不依赖远端语义新假设,符合 §2.2。
**图库确认建议提问(可复制)**:「对同一 `imageStoreId``updateGroupPersonRef` 在仅设置 `labelIds``organizationIds``personIds` 为空)时,是否为**按这些维度刷新人员引用**且**不会**将图库维度裁剪为仅等于本次入参?多次调用子集与单次调用**并集**是否在业务上等价?」
**实施后回填**:确认结论、对接人、日期写入 [约定文档 §3.1 文末](对外接口不变-远程调用与性能优化约定.md)(见约定 §5)。
**排期决策(2026-04-25**:在取得图库确认前,**冻结**约定 **§3.1** 相关全部代码变更;下一迭代转 **§3.5**(见上文 **「迭代 3」**)。
---
## 7. 迭代 3`AcsDeviceTaskServiceImpl#updateFloors` 走查结论(§3.5
**走查日期**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`)。
### 7.1 调用链与 RPC 上界(与 §1 表对齐)
| 分支 | 行为 | 上界 |
|------|------|------|
| 增楼层 | `personRuleService.add` **或** `imageRuleRefService.addOnlyRule`,成功后 `updateBingDevices` | `addFloors.size()` |
| 删楼层 | `personRuleService.delete` **或** `imageRuleRefService.delete`(单 id**或** 仅 DAO `deleteByOrgAndLabel`,成功后 `updateBingDevices` | `delFloorIds.size()` |
| 内层放大 | `imageRuleRefService.delete` 仍受约定 **§3.1** 冻结影响(`updateGroupPersonRef` 多次);本迭代**未**改该内层。 | 不变 |
**入口**`AcsElevatorDeviceServiceImpl#bindingFloors` / `#bindingPerson` 在插入任务行后**同步**调用 `updateFloors`;方法带 `@Async`,实际在 **`updateFloorsExecutor`** 线程执行;HTTP 已返回 `taskId` 后,**异步内失败不会回写该 HTTP 响应**(现网行为保持;运维依赖任务进度与日志)。
### 7.2 检查项与结论
| 检查项 | 现状(走查时) | 结论 |
|--------|----------------|------|
| `acsDeviceTaskDao.getById` | 未判空即 `task.getIsStop()`,存在 **NPE** 风险(数据异常或竞态) | **不通过** → 已修正:空则记录并 `ServiceException` |
| `personRuleService.add/delete``imageRuleRefService.addOnlyRule/delete` 返回值 | 未校验 `CloudwalkResult.isSuccess()`,失败时仍 **`updateBingDevices`**,进度与真实绑定不一致 | **不通过**(违反 §2.2)→ 已修正:统一 `requireTaskStepSuccess`,失败抛错且**不**递增 |
| `catch``ServiceException(e.getMessage())` | 丢失根因类型与栈信息到调用方;异步场景仅日志含 `{}` 与异常 | **记录**:是否改为 `ServiceException(code, msg)``initCause` 属产品/运维范围,**本轮不改** |
| 线程池 `keepAliveSeconds` | `UpdateFloorsPoolProperties` 有字段,**Bean 未 `setKeepAliveSeconds`**,配置项无效 | **缺陷** → 已在 `UpdateFloorsTaskExecutor` 绑定 |
| `RejectedExecutionHandler` | `AbortPolicy`,队列满时拒绝提交 | **记录**:与背压策略相关,**本轮不改**(须与运维对齐) |
| 删楼 `ruleMap.get(delFloorId)` | 若 `listZoneInfoByIds` 未覆盖某 `delFloorId` 可能 **null** 拼接 `ruleName` | **记录**:数据正常时风险低;**本轮未改**(可后续与 DAO 对齐) |
### 7.3 评估结论(是否允许进入代码修正)
- **允许并已实施(本轮)**`task` 空指针防护;对 **`personRuleService.add` / `delete`**、**`imageRuleRefService.addOnlyRule` / `delete`** 的 **`CloudwalkResult` 成功校验**(约定 §2.2);`updateFloorsExecutor` 绑定 **`keepAliveSeconds`**。
- **暂缓(须单独评审)**:拒绝策略、`catch` 异常语义增强;**楼层有界并行**与 `ruleMap` 缺键防护**已**见 **§9**。
**修正实施后**:提交 **`0ddeedc`**(分支 `v0.11`)。
---
## 8. 迭代 4P1 有界并行(§3.2 / §3.3 / §3.4
**实施日期**2026-04-25
### 8.1 行为与约定对齐说明
| 项 | 说明 |
|----|------|
| **并发度** | 代码常量与默认池 **`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.3 `listFloor`** | 设备数仍顺序 DAO`acsPersonService.page` 按批并行,结果写入 `personTotals[idx]` 后顺序 `setPersonNumber`**响应楼层顺序不变**。 |
| **§3.4 `addImageStore`** | `bindAppImageStoreDevice` 仍顺序执行;仅 **`bindDeviceAndImageStore`** 按批并行;任一批次中失败则 **`rollbackImageStoreAfterBindFailure`**(抽方法)后抛 `ServiceException`,与原先 try/catch 回滚路径一致。 |
| **Feign ThreadLocal** | 所有子线程 RPC 经 **`FeignThreadLocalUtil.callWithContext`**,避免池化线程串请求头。 |
**实施后提交****`fe571aa`**(分支 `v0.11`)。
---
## 9. 迭代 5`updateFloors` 楼层有界并行(§3.5
**实施日期**2026-04-24(工作区)
### 9.1 设计要点
| 项 | 说明 |
|----|------|
| **并发度** | 与 §8 一致使用 **`elevatorRemoteBoundedExecutor`** 的 **`ThreadPoolExecutor#invokeAll`**,批大小 **`UPDATE_FLOORS_FLOOR_PARALLEL = 6`**(与 `AcsPassRuleServiceImpl` 等处一致)。 |
| **bind 推进** | 同批内各楼层 RPC **可并发**`Future#get()` **按列表下标顺序** 等待;每遇返回值 **1** 则主线程**再** `getById`**`BIND_DEVICES`+1**,与旧实现「每层成功后立即 +1」的**终态**一致。 |
| **停任务** | 每层子步骤开头仍 `getById``isStop != 0` 时该层贡献 **0**、不推进 bind。 |
| **DAO 异常** | 删楼分支中 `getByRuleName` / `deleteByOrgAndLabel`**`DataAccessException`** 在子步内转 **`ServiceException`**,以配合 `Callable``invokeAll` 的异常链。 |
| **§3.1** | 未改 `imageRuleRefService.delete` 实现,内层 `updateGroupPersonRef` 行为与冻结前一致。 |
### 9.2 同批多楼层与「遇错即停」
**§8.1** 类似:同批中若一 Floor 的 RPC 失败,同批**其它在途** Floor 的 RPC 可能已执行完毕;`get()` 按顺序在**首败**时抛出。与严格串行「前一层失败则后层不再发起」在**已发出请求**上可存在差异,属有界并行常见取舍。
### 9.3 代码位置
- `AcsDeviceTaskServiceImpl#updateFloors``#runAddFloorsInBoundedParallel` / `#runDelFloorsInBoundedParallel``#addOneFloorStep` / `#delOneFloorStep``#advanceBindProgressOne`
# 对外接口不变:走查任务与状态
> **依据**:[对外接口不变-远程调用与性能优化约定](对外接口不变-远程调用与性能优化约定.md)(§2 总原则、§3 场景、§4 优先级)。
> **走查代码根**`maven-cw-elevator-application/cw-elevator-application-service`2026-04-24 静态走查)。
> **说明**:下表「子任务数」指**与约定相关的 RPC/可优化循环次数上界**(随运行时数据规模变化);**状态**表示在**不扩展 Feign/HTTP 契约**前提下是否建议动代码。
> **排期(2026-04**`frontend/` 暂无可用前端工程;**凡需改前端、登记页、物业管理端 UI 的迭代,暂跳过**(详见 [docs/README 当前排期与范围](../README.md#当前排期与范围2026-04))。本文档仅跟踪**电梯等服务端**在约定下的走查与修正。
---
## 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.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.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** |
---
## 2. 数量小结(便于排期)
| 类型 | 计数符号 | 含义 |
|------|----------|------|
| **§3.1 可合并 RPC** | 由 N 降为 **1**(在语义确认后) | 规则批量删除场景收益最大 |
| **§3.2 并行度** | P | 人员多 ID 删除 |
| **§3.3 并行度** | F | 楼层列表人数统计 |
| **§3.4 并行度** | D | 设备绑图库 |
| **§3.5** | A + D | 异步任务按层调用 |
---
## 3. 迭代规划与下一迭代范围(不改 HTTP/Feign 签名)
### 冻结范围(2026-04-25
| 项 | 说明 |
|----|------|
| **约定 §3.1 全范围** | 未取得图库/通行对 `updateGroupPersonRef` 语义确认前,**不进行**与该约定相关的**任何**代码修正与优化(含 **合并 N→1** 及 §6.4 所述**仅 `isSuccess` 校验**小步),避免在无确认期分散实现与回滚成本。 |
| **恢复条件** | 图库书面或接口说明确认 + 在 [约定文档 §3.1](对外接口不变-远程调用与性能优化约定.md) 文末回填对接人、日期;再按 §1 表拆分 PR(合并与返回值校验可分步)。 |
| **前端相关** | 仓库**暂无可用前端**;所有需改**前端 / 登记页 / 物业端页面**的迭代**暂不排期、不执行**(与本文服务端走查无冲突;产品方案中 UI 类阶段见 [docs/README](../README.md#当前排期与范围2026-04))。 |
### 迭代 3**§3.5 `updateFloors`**
| 字段 | 内容 |
|------|------|
| **状态** | **走查与首轮修正已完成**(见 **§7**):`getById` 空防护、步骤级 `CloudwalkResult` 校验、`keepAliveSeconds` 绑定线程池。 |
| **约定锚点** | **§3.5** `AcsDeviceTaskServiceImpl#updateFloors` |
| **暂缓项** | 楼层有界并行**已**见 **§9**`AbortPolicy``catch` 语义、其它见 **§7.3**。 |
### 迭代 4**P1:§3.2 / §3.3 / §3.4 + 统一有界池**
| 字段 | 内容 |
|------|------|
| **状态** | **已实施**(实现说明与语义边界见 **§8**)。 |
| **线程池 Bean** | `elevatorRemoteBoundedExecutor``ElevatorRemoteIoExecutorConfig`),配置前缀 **`ninca.elevator.remote-io.pool`**(默认 core=max=**6**queue=512`CallerRunsPolicy`)。**未**与 `updateFloorsExecutor` 合并,避免异步任务与同步 RPC 抢同池。 |
| **公共能力** | `FeignThreadLocalUtil.callWithContext``cw-elevator-application-common`):子线程执行 Feign 前绑定/恢复 ThreadLocal 请求头。 |
### 迭代 5**§3.5 `updateFloors` 楼层有界并行**
| 字段 | 内容 |
|------|------|
| **状态** | **已实施**(见 **§9**):增楼列表、删楼列表在各自阶段内以最多 **6** 路**并发**执行 `add` / `addOnlyRule` / `delete` 等;**`BIND_DEVICES` 仍按原列表顺序**、每层成功**仍 +1** 重读任务行,与纯串行「进度语义」一致;子线程 Feign 经 `FeignThreadLocalUtil``ruleMap``zoneId` 时用 **`getOrDefault(…, "")`** 防 NPE。 |
| **不变更** | **§3.1 全冻结** 期间未改 `ImageRuleRefServiceImpl#delete``updateGroupPersonRef` 次数。 |
### 迭代 6 及以后(可选)
- **调参 / 观测**`ninca.elevator.remote-io.pool` 按环境压测;必要时为 `updateFloors` 批处理增加**指标/耗时日志**(不扩展 HTTP 响应)。
- **§3.1**:图库确认后再评估合并 N→1 与(若允许)`updateGroupPersonRef``isSuccess` 小步。
**已完成回顾**:迭代 1 — **§5**;迭代 2 — **§6**(§3.1 冻结);迭代 3 — **§7**;迭代 4 — **§8**;迭代 5 — **§9**。
---
## 4. 文档维护
| 项目 | 内容 |
|------|------|
| 更新触发 | `ImageRuleRefServiceImpl#delete``AcsPersonServiceImpl#delete``AcsPassRuleServiceImpl#listFloor` / `#addImageStore``AcsDeviceTaskServiceImpl#updateFloors` 任一处重构或签契约变更 |
| 结论回填 | 图库对 §3.1 的确认结论请写回 [约定文档 §3.1](对外接口不变-远程调用与性能优化约定.md) 文末建议行(对接人 + 日期) |
---
## 5. 迭代 1`AcsPassRuleServiceImpl#listFloor` 走查结论(§3.3 / §2.2
**走查日期**2026-04-25
**代码位置**`maven-cw-elevator-application/.../passrule/impl/AcsPassRuleServiceImpl.java` 方法 `listFloor`
| 检查项 | 现状 | 结论 |
|--------|------|------|
| `zoneService.tree` 返回值 | 已校验 `zoneTree.isSuccess()`,失败抛 `ServiceException` | **通过** |
| `acsPersonService.page` 返回值 | 循环内**未**校验 `page.isSuccess()`,直接 `page.getData()``totalRows` | **不通过**:违反约定 **§2.2**Feign 失败时 `getData()` 可能为 null,存在 **NPE** 风险,且可能把失败误当「0 人」 |
| `page.getData()` 空指针 | 未防护 | **不通过**:与上项合并修正 |
| `rowsOfPage` | 当前为 `10`,仅使用 `totalRows` | **建议**:改为 **`1`**(约定 §3.3:仅取总数,略减负载),**不改变** HTTP 响应字段 |
**评估结论(是否允许进入代码修正)****通过进入修正** — 仅补充与 `zoneTree` 分支一致的失败处理及空数据防护,**不**改变对外 JSON 字段语义;`rowsOfPage=1` 与现逻辑(只读 `totalRows`)等价。
**代码修正(已应用,2026-04-25**`AcsPassRuleServiceImpl#listFloor` — 增加 `page.isSuccess()` 失败抛错、`page.getData()` 为空时 `personNumber=0``CloudwalkPageInfo(1,1)` 仅取总数。
**修正实施后**:提交 **`e652eb3`**(分支 `v0.11`)。
---
## 6. 迭代 2`ImageRuleRefServiceImpl#delete` 与 §3.1 方案走查(仅评估,未改代码)
**走查日期**2026-04-25
**目标**:在全工作区定位**图库(intelligent 图库人员服务)**与**通行规则(电梯应用本地)**相关代码,审核「合并 `updateGroupPersonRef`」方案是否需图库侧语义确认后方可实施。
### 6.1 图库 / 通行相关代码位置(工作区)
| 层级 | 路径/符号 | 职责 |
|------|-----------|------|
| **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`(可同时只填部分字段) |
| **客户端封装** | `.../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) |
| **同文件其它 `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** 为主不同) |
| **HTTP 入口** | `cw-elevator-application-web/.../AcsPassRuleController.java` | `imageRuleRefService.delete` |
| **异步任务调用** | `AcsDeviceTaskServiceImpl#updateFloors` | 删楼层时多构造 `deleteParam.setIds(Collections.singletonList(ruleId))` **逐层**调 `imageRuleRefService.delete`;单次 `delete` 内仍可能 1 次或多次 `updateGroupPersonRef`(视该层 `ids` 数量) |
**说明**:本仓库**无** intelligent 图库服务端的 `updateGroupPersonRef` 业务实现源码,仅能依据 DTO 与调用方推断语义;**与图库/通行团队确认**仍属 §3.1 前置条件。
### 6.2 当前 `delete` 行为摘要(与合并相关)
- `imageStoreId` 来自 **`deviceImageStoreDao.getByBuildingId(param.getParentId())`**,整次批量删除共用同一图库。
- 对每个待删父规则 `id`:先读**该父规则下子规则**的 label/org(子行若带 label 则 `continue`,**同一子行不会同时写入 org**,与数据模型一致),再删父规则,再带着**本轮** `includeLabels` / `includeOrganizations` 调图库刷新。
- **`updateGroupPersonRef` 的返回值未做 `isSuccess` 校验**(违反约定 §2.2;与 `getImageStorePerson` 等分支不一致)。
### 6.3 约定中的「合并」方案审核
| 维度 | 结论 |
|------|------|
| **与现网 N 次调用的等价性** | 若图库侧语义为:在**给定 `imageStoreId`** 下,对传入的 **labelIds / organizationIds 集合**做**增量刷新或按维度重算引用**(各维度独立、与顺序无关),则「删库前汇总所有待删父规则的子维度 → **去重并集** → 删库完成后 **1 次** `updateGroupPersonRef`」与「每删一条父规则刷新其维度子集」在**最终一致**上通常等价。 |
| **必须向图库确认的风险** | 若远端实现为「以本次入参**覆盖/裁剪**图库可见范围」或依赖**调用顺序**产生副作用,则合并后的**单次并集**与 N 次**子集递进**可能不等价。约定文档 §3.1 所述「非破坏性刷新」即针对此。 |
| **`personIds``labelIds`/`organizationIds` 混用** | `PersonRuleServiceImpl``personIds` 路径;`ImageRuleRefServiceImpl#delete` 走 label/org。合并方案**不改变** `delete` 仅设 label/org 的现状;但若图库服务在**未传 `personIds`** 时对空列表有特殊含义,仍须一并确认。 |
| **空列表仍 RPC** | 当前循环在子规则为空时仍调用 `updateGroupPersonRef`(两列表皆空)。合并后是否**跳过空并集**可减少无效 RPC,但属于**行为微调**,若图库依赖「空刷」触发全量重算,须图库确认后再定。 |
| **不合并时的安全增量** | 在未获图库书面确认前,**仅**可为每次 `updateGroupPersonRef` 增加 **`isSuccess` 校验 + 失败抛 `ServiceException`**(及可选日志),**不改变** RPC 次数;与约定「退化为循环调用 + 返回值校验」一致。 |
### 6.4 评估结论(是否允许进入 §3.1「合并」类代码修正)
- **合并 N→1**:**不允许在图库/通行确认前实施** — 与 §1 表及约定 §3.1 前置条件一致。
- **仅返回值校验(及可选空并集跳过,若产品同意)**:**允许作为独立小步** — 不依赖远端语义新假设,符合 §2.2。
**图库确认建议提问(可复制)**:「对同一 `imageStoreId``updateGroupPersonRef` 在仅设置 `labelIds``organizationIds``personIds` 为空)时,是否为**按这些维度刷新人员引用**且**不会**将图库维度裁剪为仅等于本次入参?多次调用子集与单次调用**并集**是否在业务上等价?」
**实施后回填**:确认结论、对接人、日期写入 [约定文档 §3.1 文末](对外接口不变-远程调用与性能优化约定.md)(见约定 §5)。
**排期决策(2026-04-25**:在取得图库确认前,**冻结**约定 **§3.1** 相关全部代码变更;下一迭代转 **§3.5**(见上文 **「迭代 3」**)。
---
## 7. 迭代 3`AcsDeviceTaskServiceImpl#updateFloors` 走查结论(§3.5
**走查日期**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`)。
### 7.1 调用链与 RPC 上界(与 §1 表对齐)
| 分支 | 行为 | 上界 |
|------|------|------|
| 增楼层 | `personRuleService.add` **或** `imageRuleRefService.addOnlyRule`,成功后 `updateBingDevices` | `addFloors.size()` |
| 删楼层 | `personRuleService.delete` **或** `imageRuleRefService.delete`(单 id**或** 仅 DAO `deleteByOrgAndLabel`,成功后 `updateBingDevices` | `delFloorIds.size()` |
| 内层放大 | `imageRuleRefService.delete` 仍受约定 **§3.1** 冻结影响(`updateGroupPersonRef` 多次);本迭代**未**改该内层。 | 不变 |
**入口**`AcsElevatorDeviceServiceImpl#bindingFloors` / `#bindingPerson` 在插入任务行后**同步**调用 `updateFloors`;方法带 `@Async`,实际在 **`updateFloorsExecutor`** 线程执行;HTTP 已返回 `taskId` 后,**异步内失败不会回写该 HTTP 响应**(现网行为保持;运维依赖任务进度与日志)。
### 7.2 检查项与结论
| 检查项 | 现状(走查时) | 结论 |
|--------|----------------|------|
| `acsDeviceTaskDao.getById` | 未判空即 `task.getIsStop()`,存在 **NPE** 风险(数据异常或竞态) | **不通过** → 已修正:空则记录并 `ServiceException` |
| `personRuleService.add/delete``imageRuleRefService.addOnlyRule/delete` 返回值 | 未校验 `CloudwalkResult.isSuccess()`,失败时仍 **`updateBingDevices`**,进度与真实绑定不一致 | **不通过**(违反 §2.2)→ 已修正:统一 `requireTaskStepSuccess`,失败抛错且**不**递增 |
| `catch``ServiceException(e.getMessage())` | 丢失根因类型与栈信息到调用方;异步场景仅日志含 `{}` 与异常 | **记录**:是否改为 `ServiceException(code, msg)``initCause` 属产品/运维范围,**本轮不改** |
| 线程池 `keepAliveSeconds` | `UpdateFloorsPoolProperties` 有字段,**Bean 未 `setKeepAliveSeconds`**,配置项无效 | **缺陷** → 已在 `UpdateFloorsTaskExecutor` 绑定 |
| `RejectedExecutionHandler` | `AbortPolicy`,队列满时拒绝提交 | **记录**:与背压策略相关,**本轮不改**(须与运维对齐) |
| 删楼 `ruleMap.get(delFloorId)` | 若 `listZoneInfoByIds` 未覆盖某 `delFloorId` 可能 **null** 拼接 `ruleName` | **记录**:数据正常时风险低;**本轮未改**(可后续与 DAO 对齐) |
### 7.3 评估结论(是否允许进入代码修正)
- **允许并已实施(本轮)**`task` 空指针防护;对 **`personRuleService.add` / `delete`**、**`imageRuleRefService.addOnlyRule` / `delete`** 的 **`CloudwalkResult` 成功校验**(约定 §2.2);`updateFloorsExecutor` 绑定 **`keepAliveSeconds`**。
- **暂缓(须单独评审)**:拒绝策略、`catch` 异常语义增强;**楼层有界并行**与 `ruleMap` 缺键防护**已**见 **§9**。
**修正实施后**:提交 **`0ddeedc`**(分支 `v0.11`)。
---
## 8. 迭代 4P1 有界并行(§3.2 / §3.3 / §3.4
**实施日期**2026-04-25
### 8.1 行为与约定对齐说明
| 项 | 说明 |
|----|------|
| **并发度** | 代码常量与默认池 **`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.3 `listFloor`** | 设备数仍顺序 DAO`acsPersonService.page` 按批并行,结果写入 `personTotals[idx]` 后顺序 `setPersonNumber`**响应楼层顺序不变**。 |
| **§3.4 `addImageStore`** | `bindAppImageStoreDevice` 仍顺序执行;仅 **`bindDeviceAndImageStore`** 按批并行;任一批次中失败则 **`rollbackImageStoreAfterBindFailure`**(抽方法)后抛 `ServiceException`,与原先 try/catch 回滚路径一致。 |
| **Feign ThreadLocal** | 所有子线程 RPC 经 **`FeignThreadLocalUtil.callWithContext`**,避免池化线程串请求头。 |
**实施后提交****`fe571aa`**(分支 `v0.11`)。
---
## 9. 迭代 5`updateFloors` 楼层有界并行(§3.5
**实施日期**2026-04-24(工作区)
### 9.1 设计要点
| 项 | 说明 |
|----|------|
| **并发度** | 与 §8 一致使用 **`elevatorRemoteBoundedExecutor`** 的 **`ThreadPoolExecutor#invokeAll`**,批大小 **`UPDATE_FLOORS_FLOOR_PARALLEL = 6`**(与 `AcsPassRuleServiceImpl` 等处一致)。 |
| **bind 推进** | 同批内各楼层 RPC **可并发**`Future#get()` **按列表下标顺序** 等待;每遇返回值 **1** 则主线程**再** `getById`**`BIND_DEVICES`+1**,与旧实现「每层成功后立即 +1」的**终态**一致。 |
| **停任务** | 每层子步骤开头仍 `getById``isStop != 0` 时该层贡献 **0**、不推进 bind。 |
| **DAO 异常** | 删楼分支中 `getByRuleName` / `deleteByOrgAndLabel`**`DataAccessException`** 在子步内转 **`ServiceException`**,以配合 `Callable``invokeAll` 的异常链。 |
| **§3.1** | 未改 `imageRuleRefService.delete` 实现,内层 `updateGroupPersonRef` 行为与冻结前一致。 |
### 9.2 同批多楼层与「遇错即停」
**§8.1** 类似:同批中若一 Floor 的 RPC 失败,同批**其它在途** Floor 的 RPC 可能已执行完毕;`get()` 按顺序在**首败**时抛出。与严格串行「前一层失败则后层不再发起」在**已发出请求**上可存在差异,属有界并行常见取舍。
### 9.3 代码位置
- `AcsDeviceTaskServiceImpl#updateFloors``#runAddFloorsInBoundedParallel` / `#runDelFloorsInBoundedParallel``#addOneFloorStep` / `#delOneFloorStep``#advanceBindProgressOne`
@@ -1,96 +1,96 @@
# 对外接口不变前提下的远程调用与性能优化约定
**适用范围**:本仓库电梯应用(`maven-cw-elevator-application`)及与之集成的 intelligent / 图库等 Feign 调用路径。
**订立日期**2026-04-24
**状态**:团队约定(实施前对「远端语义」类条目需与图库/通行服务二次确认)。
**走查落地**[对外接口不变-走查任务与状态](对外接口不变-走查任务与状态.md)(子任务计数、可修正 / 不可修正与下一步)。
---
## 1. 「对外接口不变」的可操作定义
| 层级 | 是否允许变更 | 说明 |
|------|----------------|------|
| **对客户端 HTTP** | **否** | URL、Method、请求/响应 JSON 字段与语义、成功/失败码含义与现网一致。 |
| **本模块内部实现** | **是** | `ServiceImpl`、DAO、私有方法、远程调用次数、并行策略等,只要最终 HTTP 行为与业务语义一致。 |
| **intelligent / 图库等 Feign 契约** | **默认否** | 不新增 `batchDelete`、多 zone 一次统计等接口时,仅允许在**现有方法**上组合、批处理逻辑或**有界并行**。若单独立项扩展 Feign,再按新版本评审。 |
下文默认 **HTTP 与 Feign 契约均不扩展**
---
## 2. 总原则
1. **等价优先**:任何减少 RPC 或改并行的地方,须保证与改造前**同一业务语义**(尤其 `totalRows`、删除范围、刷新范围);不得用「近似」本地统计替代远端分页结果,除非完成**对账验收**并文档留痕。
2. **返回值必检**:对 `CloudwalkResult` / Feign 封装结果须校验 `isSuccess`(或项目统一规范);禁止依赖「失败静默」作为常态路径。
3. **合并优于 N 次**:若远端操作对同一维度**幂等重算**(如按 label/org 刷新引用),优先在本地删库结束后**去重并集单次调用**,而非循环内重复调用。
4. **无法合并时的默认手段**:在契约无批量 API 时,采用**有界并行**(固定线程池或 `Semaphore`,建议并发度 4~8,可按环境调参),并明确**失败聚合策略**与现网「遇错即停」等行为一致。
5. **事务边界**:涉及多 RPC + 本地 DB 时,在约定中明确是否 `@Transactional`、失败是否需补偿;禁止在未定义产品语义时擅自「部分成功」。
---
## 3. 按场景的具体约定
### 3.1 `ImageRuleRefServiceImpl.delete`(循环内 `updateGroupPersonRef`
- **目标**:降低重复刷新带来的 RPC 与负载。
- **约定**:在**所有** `deleteById`(或等价删库步骤)完成后,对本轮涉及的 `labelIds``organizationIds` **去重并集**,再调用**一次** `updateGroupPersonRef`
- **前置条件**:与图库/通行侧确认「仅传 label/org 列表」为**刷新引用**语义,而非会把图库裁剪为仅含这些维度的破坏性语义。若不能确认,**退化为**保持循环调用 + **仍须做返回值校验**
- **HTTP**:不变。
### 3.2 `AcsPersonServiceImpl.delete`(循环内 `imageStorePersonService.delete`
- **约束**`ImageStorePersonDelParam` 仅支持单 `personId`,无批量 delete 时不扩展契约。
- **约定**:采用**有界并行** `delete`;失败策略与现实现**一致**(例如任一失败则整体失败);若需与现网「严格顺序失败」完全一致,须在实现中定义完成顺序与异常聚合方式。
- **HTTP**:不变。
### 3.3 `AcsPassRuleServiceImpl.listFloor`(每层 `acsPersonService.page` 仅取 `totalRows`
- **约束**`AcsPersonQueryParam` 单 zone;图库侧无「多楼层一次返回人数」API 时,不能靠单次 RPC 消除 N。
- **禁止**:用本地 `countPersonIdByZoneId` 等 SQL 人数**直接替代** `imageStorePersonService.page``totalRows`(与 `PersonRuleServiceImpl.page` 路径下的 label/org/del 等过滤**不等价**),除非完成**逐层对账**并书面验收。
- **约定**:在契约不扩展时,**最优为按楼层有界并行** `page`(如 `rowsOfPage=1` 仅取总数),按树顺序**合并结果**,保证响应列表顺序与字段不变。
- **HTTP**:不变。
### 3.4 `AcsPassRuleServiceImpl.addImageStore`(循环 `bindDeviceAndImageStore`
- **约定**:无批量 bind 时,**有界并行 bind**;异常时的回滚(如删除已建图库)须与现逻辑一致,并注意并行下与顺序相关的竞态。
- **HTTP**:不变。
### 3.5 `AcsDeviceTaskServiceImpl` 等按楼层调用 `personRuleService.delete` / `imageRuleRefService.delete`
- **约定**:优先受益于 **3.1** 的合并刷新;若仍为每层 `delete`,可叠加**楼层级有界并行**,前提是错误语义与资源侧限流可接受。
---
## 4. 实施优先级(契约不扩展时的 ROI)
| 优先级 | 项 | 说明 |
|--------|-----|------|
| P0 | 3.1 合并 `updateGroupPersonRef` + 返回值校验 | 收益大、变更面相对集中;依赖远端语义确认。 |
| P1 | 3.3 / 3.2 / 3.4 / 3.5 有界并行 | 不改 DTO/URL,主要降低墙钟时间;注意线程池生命周期与超时。 |
| 远期 | Feign 批量/聚合接口 | 契约可扩展时,再评估批量 delete、多 zone 统计等结构性优化。 |
---
## 5. 变更与评审
- 任何偏离本约定(例如采用本地 count 替代 `totalRows`)须在 PR/变更说明中**单列风险**并附对账或测试证据。
- 与 intelligent 团队对齐「`updateGroupPersonRef` 语义」后,建议在本文 **3.1** 节追加**结论日期与对接人**一行,便于审计。
---
## 6. 相关代码锚点(便于检索)
| 场景 | 典型类/方法 |
|------|-------------|
| 人员分页与规则 | `AcsPersonServiceImpl#page``getRuleListByZoneId` |
| 楼层人数 | `AcsPassRuleServiceImpl#listFloor` |
| 规则与图库口径对照 | `PersonRuleServiceImpl#page` |
| 规则引用删除 | `ImageRuleRefServiceImpl#delete` |
| 人员删除与图库 | `AcsPersonServiceImpl#delete` |
---
## 7. 走查任务索引(子任务与状态)
**[对外接口不变-走查任务与状态.md](对外接口不变-走查任务与状态.md)**:按 §3.1~§3.5 与当前代码对齐,列出 **RPC 次数上界**、**下一可实施修正**、**在契约不扩展前提下不可做或须先确认** 的项。
# 对外接口不变前提下的远程调用与性能优化约定
**适用范围**:本仓库电梯应用(`maven-cw-elevator-application`)及与之集成的 intelligent / 图库等 Feign 调用路径。
**订立日期**2026-04-24
**状态**:团队约定(实施前对「远端语义」类条目需与图库/通行服务二次确认)。
**走查落地**[对外接口不变-走查任务与状态](对外接口不变-走查任务与状态.md)(子任务计数、可修正 / 不可修正与下一步)。
---
## 1. 「对外接口不变」的可操作定义
| 层级 | 是否允许变更 | 说明 |
|------|----------------|------|
| **对客户端 HTTP** | **否** | URL、Method、请求/响应 JSON 字段与语义、成功/失败码含义与现网一致。 |
| **本模块内部实现** | **是** | `ServiceImpl`、DAO、私有方法、远程调用次数、并行策略等,只要最终 HTTP 行为与业务语义一致。 |
| **intelligent / 图库等 Feign 契约** | **默认否** | 不新增 `batchDelete`、多 zone 一次统计等接口时,仅允许在**现有方法**上组合、批处理逻辑或**有界并行**。若单独立项扩展 Feign,再按新版本评审。 |
下文默认 **HTTP 与 Feign 契约均不扩展**
---
## 2. 总原则
1. **等价优先**:任何减少 RPC 或改并行的地方,须保证与改造前**同一业务语义**(尤其 `totalRows`、删除范围、刷新范围);不得用「近似」本地统计替代远端分页结果,除非完成**对账验收**并文档留痕。
2. **返回值必检**:对 `CloudwalkResult` / Feign 封装结果须校验 `isSuccess`(或项目统一规范);禁止依赖「失败静默」作为常态路径。
3. **合并优于 N 次**:若远端操作对同一维度**幂等重算**(如按 label/org 刷新引用),优先在本地删库结束后**去重并集单次调用**,而非循环内重复调用。
4. **无法合并时的默认手段**:在契约无批量 API 时,采用**有界并行**(固定线程池或 `Semaphore`,建议并发度 4~8,可按环境调参),并明确**失败聚合策略**与现网「遇错即停」等行为一致。
5. **事务边界**:涉及多 RPC + 本地 DB 时,在约定中明确是否 `@Transactional`、失败是否需补偿;禁止在未定义产品语义时擅自「部分成功」。
---
## 3. 按场景的具体约定
### 3.1 `ImageRuleRefServiceImpl.delete`(循环内 `updateGroupPersonRef`
- **目标**:降低重复刷新带来的 RPC 与负载。
- **约定**:在**所有** `deleteById`(或等价删库步骤)完成后,对本轮涉及的 `labelIds``organizationIds` **去重并集**,再调用**一次** `updateGroupPersonRef`
- **前置条件**:与图库/通行侧确认「仅传 label/org 列表」为**刷新引用**语义,而非会把图库裁剪为仅含这些维度的破坏性语义。若不能确认,**退化为**保持循环调用 + **仍须做返回值校验**
- **HTTP**:不变。
### 3.2 `AcsPersonServiceImpl.delete`(循环内 `imageStorePersonService.delete`
- **约束**`ImageStorePersonDelParam` 仅支持单 `personId`,无批量 delete 时不扩展契约。
- **约定**:采用**有界并行** `delete`;失败策略与现实现**一致**(例如任一失败则整体失败);若需与现网「严格顺序失败」完全一致,须在实现中定义完成顺序与异常聚合方式。
- **HTTP**:不变。
### 3.3 `AcsPassRuleServiceImpl.listFloor`(每层 `acsPersonService.page` 仅取 `totalRows`
- **约束**`AcsPersonQueryParam` 单 zone;图库侧无「多楼层一次返回人数」API 时,不能靠单次 RPC 消除 N。
- **禁止**:用本地 `countPersonIdByZoneId` 等 SQL 人数**直接替代** `imageStorePersonService.page``totalRows`(与 `PersonRuleServiceImpl.page` 路径下的 label/org/del 等过滤**不等价**),除非完成**逐层对账**并书面验收。
- **约定**:在契约不扩展时,**最优为按楼层有界并行** `page`(如 `rowsOfPage=1` 仅取总数),按树顺序**合并结果**,保证响应列表顺序与字段不变。
- **HTTP**:不变。
### 3.4 `AcsPassRuleServiceImpl.addImageStore`(循环 `bindDeviceAndImageStore`
- **约定**:无批量 bind 时,**有界并行 bind**;异常时的回滚(如删除已建图库)须与现逻辑一致,并注意并行下与顺序相关的竞态。
- **HTTP**:不变。
### 3.5 `AcsDeviceTaskServiceImpl` 等按楼层调用 `personRuleService.delete` / `imageRuleRefService.delete`
- **约定**:优先受益于 **3.1** 的合并刷新;若仍为每层 `delete`,可叠加**楼层级有界并行**,前提是错误语义与资源侧限流可接受。
---
## 4. 实施优先级(契约不扩展时的 ROI)
| 优先级 | 项 | 说明 |
|--------|-----|------|
| P0 | 3.1 合并 `updateGroupPersonRef` + 返回值校验 | 收益大、变更面相对集中;依赖远端语义确认。 |
| P1 | 3.3 / 3.2 / 3.4 / 3.5 有界并行 | 不改 DTO/URL,主要降低墙钟时间;注意线程池生命周期与超时。 |
| 远期 | Feign 批量/聚合接口 | 契约可扩展时,再评估批量 delete、多 zone 统计等结构性优化。 |
---
## 5. 变更与评审
- 任何偏离本约定(例如采用本地 count 替代 `totalRows`)须在 PR/变更说明中**单列风险**并附对账或测试证据。
- 与 intelligent 团队对齐「`updateGroupPersonRef` 语义」后,建议在本文 **3.1** 节追加**结论日期与对接人**一行,便于审计。
---
## 6. 相关代码锚点(便于检索)
| 场景 | 典型类/方法 |
|------|-------------|
| 人员分页与规则 | `AcsPersonServiceImpl#page``getRuleListByZoneId` |
| 楼层人数 | `AcsPassRuleServiceImpl#listFloor` |
| 规则与图库口径对照 | `PersonRuleServiceImpl#page` |
| 规则引用删除 | `ImageRuleRefServiceImpl#delete` |
| 人员删除与图库 | `AcsPersonServiceImpl#delete` |
---
## 7. 走查任务索引(子任务与状态)
**[对外接口不变-走查任务与状态.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 须排在本步之后**)。
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`**)。
9. **`maven-intelligent-cwoscomponent`**`mvn -DskipTests clean install`(依赖第 1 步的 **protocol-entity** 与 cloudwalk 模块)
10. **`maven-cw-elevator-application`**`mvn -DskipTests clean install``service` 若仍失败,多为其它私服构件或业务源码问题,见下文)
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`或用 **`build_nexus_only.sh`** 隔离本地库 + Nexus`service` 若仍失败,多为其它私服构件或业务源码问题,见下文)
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-portal:pom:1.0.0-SNAPSHOT` absent | 同上,**`cwos-portal-interface`** 的父工程未发布。 |
| `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 未成功)。
@@ -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.PartInitDTO;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -13,14 +14,17 @@ import org.springframework.web.multipart.MultipartFile;
public interface FilePartFeign {
@RequestMapping(value = { "/init" }, method = { RequestMethod.POST })
@RequestMapping(value = { "/part/init" }, method = { RequestMethod.POST })
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,
@RequestParam("partNumber") Integer paramInteger, @RequestParam("uploadId") String paramString2,
@RequestPart("file") MultipartFile paramMultipartFile);
@RequestMapping(value = { "/finish" }, method = { RequestMethod.POST })
@RequestMapping(value = { "/part/finish" }, method = { RequestMethod.POST })
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.PartInitDTO;
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitResultDTO;
import java.io.InputStream;
import org.springframework.web.multipart.MultipartFile;
public interface FilePartManager {
@@ -14,4 +15,6 @@ public interface FilePartManager {
MultipartFile paramMultipartFile) 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 feign.Client;
import feign.Feign;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@Component
@Import({ FeignClientsConfiguration.class })
public class FilePartManagerImpl implements FilePartManager {
private FilePartFeign filePartFeign;
private FilePartFeign filePartRestFeign;
@Autowired
public FilePartManagerImpl(@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName,
Decoder decoder, Encoder encoder, Client client) {
String url = "http://" + serviceName + "/portal/file/part";
public FilePartManagerImpl(String serviceName, Decoder decoder, Encoder encoder, Client client) {
String url = "http://" + serviceName + "/portal/file";
this.filePartFeign = Feign.builder().client(client).decode404().encoder(new SpringFormEncoder())
.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());
}
@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.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@Component
@Import({ FeignClientsConfiguration.class })
public class FileStorageManagerImpl implements FileStorageManager {
private FileManagerFeign fileManagerFeign;
@@ -6,17 +6,20 @@ import cn.cloudwalk.serial.code.GeneralSerialCode;
import cn.cloudwalk.serial.code.MacGeneralSerial;
import cn.cloudwalk.serial.code.RedisGeneralCode;
import cn.cloudwalk.serial.redis.CloudwalkRedisService;
import cn.cloudwalk.serial.strategy.ServerIdStrategyBeanConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
@EnableConfigurationProperties({ CloudwalkSerialProperties.class })
@ConditionalOnProperty(prefix = "cloudwalk.serial", value = { "enabled" }, havingValue = "true", matchIfMissing = true)
@Import({ServerIdStrategyBeanConfig.class, CloudwalkSnowflakeConfiguration.class})
@EnableConfigurationProperties({CloudwalkSerialProperties.class})
@ConditionalOnProperty(prefix = "cloudwalk.serial", value = {"enabled"}, havingValue = "true", matchIfMissing = true)
public class CloudwalkSerialAutoConfiguration {
@Autowired(required = false)
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;说明与清单可单独跟踪。
releases/**/*.jar
# scripts/build_nexus_only.sh 使用的隔离 Maven 本地仓库(仅 Nexus 依赖缓存)
.m2-elevator-nexus-only/
@@ -58,6 +58,15 @@
<groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cloudwalk-common-web</artifactId>
</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>
<groupId>com.google.zxing</groupId>
<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>
<artifactId>cloudwalk-common-event</artifactId>
</dependency>
<dependency>
<groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cwos-sdk-event</artifactId>
</dependency>
<dependency>
<groupId>cn.cloudwalk.elevator</groupId>
<artifactId>cw-elevator-application-data</artifactId>
@@ -73,6 +77,14 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</dependency>
</dependencies>
<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.elevator.mqtt.fallback.MqttFeignClientFallback;
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.RequestMethod;
@@ -5,7 +5,7 @@ import cn.cloudwalk.cloud.result.CloudwalkResult;
import cn.cloudwalk.elevator.visitor.fallback.VisitorFeignClientFallback;
import cn.cloudwalk.elevator.visitor.param.VisitorRecordQueryParam;
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.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.ZoneTreeResult;
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.RequestMethod;
@@ -6,7 +6,7 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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.scheduling.annotation.EnableAsync;
@@ -14,12 +14,29 @@ import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
@EnableCaching
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableFeignClients(basePackages = "cn.cloudwalk.elevator")
@MapperScan("cn.cloudwalk.elevator")
@SpringBootApplication(exclude = {PageHelperAutoConfiguration.class})
@EnableFeignClients(basePackages = {
"cn.cloudwalk.elevator",
"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 static void main(String[] 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>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.18.RELEASE</version>
<version>1.5.17.RELEASE</version>
<relativePath/>
</parent>
@@ -15,7 +15,7 @@
<version>2.0-SNAPSHOT</version>
<packaging>pom</packaging>
<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>
<module>cw-elevator-application-common</module>
@@ -32,24 +32,31 @@
<!-- 与 V1.0.0.20211103/cw_lib 中 cloudwalk-common-* 一致,降低与历史运行包 API 差异 -->
<cloudwalk.internal.version>3.7.2-Brussels-SRX</cloudwalk.internal.version>
<cloudwalk.legacy.public.version>3.7.2-Brussels-SRX</cloudwalk.legacy.public.version>
<intelligent.cwoscomponent.version>3.0.0-xinghewan</intelligent.cwoscomponent.version>
<!-- 以下与 V1.0.0.20211103/lib 中 *.pom 主版本对齐(保持 Spring Boot 2.1.18 以兼容当前源码) -->
<!-- 与 cw_lib 完全一致;禁止改为 3.0.0-xinghewan(与本产品线口径冲突) -->
<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>
<guava.version>28.2-jre</guava.version>
<poi.version>4.1.2</poi.version>
<ant.version>1.10.12</ant.version>
<guava.version>20.0</guava.version>
<poi.version>3.15</poi.version>
<ant.version>1.9.6</ant.version>
<thumbnailator.version>0.4.8</thumbnailator.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>
<zxing.version>3.3.3</zxing.version>
<pagehelper.version>5.1.2</pagehelper.version>
<pagehelper-spring-boot.version>1.2.5</pagehelper-spring-boot.version>
<shardingsphere.version>4.0.0</shardingsphere.version>
<mybatis.version>3.5.6</mybatis.version>
<mybatis-spring.version>2.0.6</mybatis-spring.version>
<mybatis-spring-boot.version>2.0.1</mybatis-spring-boot.version>
<servlet-api.version>2.5</servlet-api.version>
<mybatis.version>3.4.6</mybatis.version>
<mybatis-spring.version>1.3.2</mybatis-spring.version>
<mybatis-spring-boot.version>1.3.1</mybatis-spring-boot.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.event.version>1.5.0-SNAPSHOT</cwos.sdk.event.version>
<intelligent.lock.version>1.1.1-SNAPSHOT</intelligent.lock.version>
<davinci.manager.storage.version>1.1.7-SNAPSHOT</davinci.manager.storage.version>
<!-- Nexus UI: http://192.168.3.12/#browse/welcome -->
@@ -57,8 +64,8 @@
<nexus.public.repo>${nexus.baseUrl}/repository/maven-public/</nexus.public.repo>
<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>
<!-- 与 Spring Boot 2.1.18 对齐的 OpenFeign/Cloud 版本(发布包可执行 JAR 需要 -->
<spring-cloud.version>Greenwich.SR6</spring-cloud.version>
<!-- 与 Spring Boot 1.5.x 配对的 Spring Cloud Edgwarespring-cloud-starter-openfeign;注解包为 netflix.feign -->
<spring-cloud.version>Edgware.SR6</spring-cloud.version>
<!-- spring-boot-maven-plugin repackage 产出的可执行 JAR 文件名(不含 .jar) -->
<elevator.release.finalName>cw-elevator-application-2.0.0</elevator.release.finalName>
</properties>
@@ -107,6 +114,11 @@
<artifactId>cwos-java-sdk-resource</artifactId>
<version>${cwos.sdk.resource.version}</version>
</dependency>
<dependency>
<groupId>cn.cloudwalk.cloud</groupId>
<artifactId>cwos-sdk-event</artifactId>
<version>${cwos.sdk.event.version}</version>
</dependency>
<dependency>
<groupId>cn.cloudwalk.intelligent</groupId>
<artifactId>cloudwalk-intelligent-component-lock</artifactId>
@@ -147,6 +159,11 @@
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
@@ -194,8 +211,18 @@
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet-api.version}</version>
<artifactId>javax.servlet-api</artifactId>
<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>
<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"
export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
(cd "$TOOL" && python3 -m pytest tests/test_unit_compare.py -q --tb=short)
echo "==> 对拍(无两实例时跳过 test_parity_endpoints): 全部"
(cd "$TOOL" && python3 -m pytest tests/ -q --tb=line)
echo "==> 对拍(无两实例时跳过 test_parity_endpoints);不含单机 smoke"
(cd "$TOOL" && python3 -m pytest tests/test_unit_compare.py tests/test_parity_endpoints.py -q --tb=line)
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
.pytest_cache/
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+
- 安装:`pip install -r requirements.txt`(在**本目录**下执行)
- Python 3.8+`pip install -r requirements.txt`
- 若遇全局 pytest 插件冲突:`export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`
## 环境说明
## 启动两实例(示例)
本机若安装过 `allure_pytest` 等全局 pytest 插件且与 Python 版本冲突,请执行
`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`(见该脚本内环境变量说明)
或手动启动两实例后:
同一套 `application.properties`(或外部配置),仅端口不同
```bash
export ELEVATOR_BASE_OLD=http://127.0.0.1:18080
export ELEVATOR_BASE_NEW=http://127.0.0.1:18081
export ELEVATOR_HEADER_BUSINESSID=... # 与现网/联调一致
# ...
cd tools/elevator_api_parity
python -m pytest tests/ -v --tb=short
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
CFG="file:/path/to/application.properties"
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
```
## 配置
## 一键完整套件(推荐)
- `api_catalog.json`:对拍端点、方法、fixture 文件名、说明。
- `fixtures/*.json`:各接口请求体。
- 归一化忽略键可扩展 `parity/compare.py``DEFAULT_STRIP_PATHS`(如 `data.rows` 内动态字段,谨慎)。
**`maven-cw-elevator-application`** 目录:
## 报告
```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": [
{
"id": "person_add_visitor_min",
"name": "访客派梯-最小体(多依赖业务失败,仅比对 HTTP+业务code)",
"name": "访客派梯-最小体",
"method": "POST",
"path": "/elevator/person/add/visitor",
"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",
"name": "人员详情-最小分页",
"name": "人员详情-分页",
"method": "POST",
"path": "/elevator/person/detail",
"fixture": "person_detail_min.json",
"compare_mode": "code_only"
"compare_mode": "code_only",
"include_in_parity": true,
"include_in_smoke": true
},
{
"id": "passrule_page",
@@ -23,7 +28,9 @@
"method": "POST",
"path": "/elevator/passRule/page",
"fixture": "passrule_page.json",
"compare_mode": "code_only"
"compare_mode": "code_only",
"include_in_parity": true,
"include_in_smoke": true
},
{
"id": "record_page",
@@ -31,7 +38,399 @@
"method": "POST",
"path": "/intelligent/acs/elevator/record/page",
"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
import os
from datetime import datetime
from pathlib import Path
import pytest
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
def pytest_configure(config):
config._parity_rows = [] # type: ignore[attr-defined]
config._smoke_rows = [] # type: ignore[attr-defined]
def pytest_addoption(parser):
@@ -24,6 +26,14 @@ def pytest_addoption(parser):
"--base-new",
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")
@@ -36,6 +46,16 @@ def base_new(request):
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")
def session_http():
s = requests.Session()
@@ -55,27 +75,56 @@ def two_instances_ready(base_old, base_new, session_http, request):
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):
rows = getattr(session.config, "_parity_rows", None)
if not rows:
return
try:
from report import generate_report
import importlib
p = _DIR / "report" / generate_report.timestamped_name("parity")
p.parent.mkdir(parents=True, exist_ok=True)
generate_report.write_file(
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}")
config = session.config
report_dir = _DIR / "report"
report_dir.mkdir(parents=True, exist_ok=True)
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:
from parity import catalog_loader
return catalog_loader.load()
srows = getattr(config, "_smoke_rows", None) or []
if srows:
try:
_write_smoke_report(config, srows, report_dir)
except Exception as e:
print(f"\n[smoke] 报告未生成: {e}")
@@ -1,8 +1,42 @@
import json
from pathlib import Path
from typing import Any, Dict, Optional
_ROOT = Path(__file__).resolve().parent.parent
def load() -> dict:
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 os
import time
from dataclasses import dataclass
from typing import Any, Optional
@@ -124,6 +125,52 @@ def _safe_json(text: str) -> Any:
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(
base_old: str, base_new: str, s: requests.Session | None = None
) -> tuple[bool, str]:
@@ -4,5 +4,6 @@ addopts = -q --strict-markers
testpaths = tests
markers =
live: 需要两实例可访问
smoke: 单机全量 HTTP 探测
unit: 纯逻辑单测
pythonpath = .
@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from typing import List
@@ -40,3 +41,16 @@ def write_file(
f"\n## 汇总\n- 通过: {ok},不一致: {bad}\n- **上线前请人工与联调/业务确认**。\n"
)
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
import json
from pathlib import Path
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.client import call_both
_DIR = Path(__file__).resolve().parent.parent
_FIX = _DIR / "fixtures"
@pytest.mark.usefixtures("two_instances_ready")
@pytest.mark.live
@@ -19,16 +15,13 @@ def test_parity_from_catalog(
base_new,
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]
for ep in cat:
if not cb_parity(ep):
continue
name = ep["id"]
fixture = ep.get("fixture")
body: dict
if fixture:
body = json.loads((_FIX / fixture).read_text(encoding="utf-8"))
else:
body = {}
body = cb_body(ep)
pr = call_both(
name=ep.get("name", name),
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>
<artifactId>cloudwalk-device-sdk-protocol-entity</artifactId>
</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>
<build>
+3 -2
View File
@@ -15,7 +15,7 @@
<version>3.0.0-xinghewan</version>
<packaging>pom</packaging>
<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>
<module>intelligent-cwoscomponent-parent</module>
@@ -26,7 +26,8 @@
<properties>
<java.version>1.8</java.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>
<fastjson.version>1.2.83</fastjson.version>
<!-- 与 V1 运行包 lib 内 cloudwalk-device-sdk-protocol-entity-2.2.0.jar 一致 -->
@@ -1,3 +1,3 @@
#!/bin/bash
systemctl restart ninca-qk-alarm-app.service
#!/bin/bash
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" "$@"