diff --git a/artifacts/decompiled/trees/ninca-qk-alarm-app-V2.9.2_20210730.jar.src/BOOT-INF/classes/sh/check-restart.sh b/artifacts/decompiled/trees/ninca-qk-alarm-app-V2.9.2_20210730.jar.src/BOOT-INF/classes/sh/check-restart.sh index 340e891a..8f6b55f3 100644 --- a/artifacts/decompiled/trees/ninca-qk-alarm-app-V2.9.2_20210730.jar.src/BOOT-INF/classes/sh/check-restart.sh +++ b/artifacts/decompiled/trees/ninca-qk-alarm-app-V2.9.2_20210730.jar.src/BOOT-INF/classes/sh/check-restart.sh @@ -1,3 +1,3 @@ -#!/bin/bash - -systemctl restart ninca-qk-alarm-app.service +#!/bin/bash + +systemctl restart ninca-qk-alarm-app.service diff --git a/cw-elevator-application-V1.0.0.20211103/application.properties b/cw-elevator-application-V1.0.0.20211103/application.properties index 69cdf249..fdadc51d 100644 --- a/cw-elevator-application-V1.0.0.20211103/application.properties +++ b/cw-elevator-application-V1.0.0.20211103/application.properties @@ -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 diff --git a/docs/architecture/Maven工作区子工程版本一览.md b/docs/architecture/Maven工作区子工程版本一览.md index 10ba0088..19b69d1c 100644 --- a/docs/architecture/Maven工作区子工程版本一览.md +++ b/docs/architecture/Maven工作区子工程版本一览.md @@ -3,7 +3,7 @@ > **范围**:`/media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/星河湾星中星/反编译` 下 `maven-*` 目录内全部 `pom.xml`(**主聚合工程 5 个 + 补充反应堆 7 个**,合计 **44** 个 `pom.xml` 文件)。 > **说明**:子模块未单独声明 `` 时,与**反应堆(reactor)父 POM** 的 `` 一致。 > **生成方式**:走查各 `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** | --- diff --git a/docs/architecture/对外接口不变-走查任务与状态.md b/docs/architecture/对外接口不变-走查任务与状态.md index a4679e1e..9b8be5bf 100644 --- a/docs/architecture/对外接口不变-走查任务与状态.md +++ b/docs/architecture/对外接口不变-走查任务与状态.md @@ -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`(约 575~598 行) | **`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`**(约 571~605 行) | 按 `param.getIds()` 循环:`listByParentRule` → 收集子规则 `includeLabels` / `includeOrganizations` → **`deleteById`** → **`imageStorePersonService.updateGroupPersonRef`**(每删一条父规则 1 次 RPC) | -| **同文件其它 `updateGroupPersonRef`** | `addOnlyRule`(约 434~439)、`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. 迭代 4:P1 有界并行(§3.2 / §3.3 / §3.4) - -**实施日期**:2026-04-25 - -### 8.1 行为与约定对齐说明 - -| 项 | 说明 | -|----|------| -| **并发度** | 代码常量与默认池 **`corePoolSize=maxPoolSize=6`**(约定 4~8 区间内),可通过 **`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`(约 575~598 行) | **`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`**(约 571~605 行) | 按 `param.getIds()` 循环:`listByParentRule` → 收集子规则 `includeLabels` / `includeOrganizations` → **`deleteById`** → **`imageStorePersonService.updateGroupPersonRef`**(每删一条父规则 1 次 RPC) | +| **同文件其它 `updateGroupPersonRef`** | `addOnlyRule`(约 434~439)、`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. 迭代 4:P1 有界并行(§3.2 / §3.3 / §3.4) + +**实施日期**:2026-04-25 + +### 8.1 行为与约定对齐说明 + +| 项 | 说明 | +|----|------| +| **并发度** | 代码常量与默认池 **`corePoolSize=maxPoolSize=6`**(约定 4~8 区间内),可通过 **`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`。 diff --git a/docs/architecture/对外接口不变-远程调用与性能优化约定.md b/docs/architecture/对外接口不变-远程调用与性能优化约定.md index a39b6d91..a69b84a8 100644 --- a/docs/architecture/对外接口不变-远程调用与性能优化约定.md +++ b/docs/architecture/对外接口不变-远程调用与性能优化约定.md @@ -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 次数上界**、**下一可实施修正**、**在契约不扩展前提下不可做或须先确认** 的项。 diff --git a/docs/build/本地编译说明.md b/docs/build/本地编译说明.md index e69ca057..3814dd7d 100644 --- a/docs/build/本地编译说明.md +++ b/docs/build/本地编译说明.md @@ -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 未成功)。 diff --git a/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/feign/FilePartFeign.java b/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/feign/FilePartFeign.java index 06bfd2b7..2728b8b3 100644 --- a/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/feign/FilePartFeign.java +++ b/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/feign/FilePartFeign.java @@ -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 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 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 finish(@RequestBody PartFinishDTO paramPartFinishDTO); + + @RequestMapping(value = { "/download" }, method = { RequestMethod.GET }) + Response bigFileDownload(@RequestParam("path") String path); } diff --git a/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/FilePartManager.java b/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/FilePartManager.java index 3756d10c..d6b7e341 100644 --- a/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/FilePartManager.java +++ b/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/FilePartManager.java @@ -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; } diff --git a/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/impl/FilePartManagerImpl.java b/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/impl/FilePartManagerImpl.java index fd214f18..2f561a24 100644 --- a/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/impl/FilePartManagerImpl.java +++ b/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/impl/FilePartManagerImpl.java @@ -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服务,获取大文件流接口异常"); + } + } } diff --git a/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/impl/FileStorageManagerImpl.java b/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/impl/FileStorageManagerImpl.java index 1d0b34d8..618199bb 100644 --- a/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/impl/FileStorageManagerImpl.java +++ b/maven-cloudwalk-intelligent-davinci-manager/davinci-manager-storage/src/main/java/cn/cloudwalk/intelligent/davinci/storage/manager/impl/FileStorageManagerImpl.java @@ -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; diff --git a/maven-cloudwalk-legacy-public/cloudwalk-common-serial/src/main/java/cn/cloudwalk/serial/autoconfig/serial/CloudwalkSerialAutoConfiguration.java b/maven-cloudwalk-legacy-public/cloudwalk-common-serial/src/main/java/cn/cloudwalk/serial/autoconfig/serial/CloudwalkSerialAutoConfiguration.java index 4c945973..890f0d7f 100644 --- a/maven-cloudwalk-legacy-public/cloudwalk-common-serial/src/main/java/cn/cloudwalk/serial/autoconfig/serial/CloudwalkSerialAutoConfiguration.java +++ b/maven-cloudwalk-legacy-public/cloudwalk-common-serial/src/main/java/cn/cloudwalk/serial/autoconfig/serial/CloudwalkSerialAutoConfiguration.java @@ -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 redisTemplate; diff --git a/maven-cloudwalk-legacy-public/cloudwalk-common-serial/src/main/resources/META-INF/spring.factories b/maven-cloudwalk-legacy-public/cloudwalk-common-serial/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..bae3cc30 --- /dev/null +++ b/maven-cloudwalk-legacy-public/cloudwalk-common-serial/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Spring Boot 2.1:注册序列号与 Snowflake 相关自动配置(此前缺失导致 AbstractGeneralCode 等 Bean 未创建) +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.cloudwalk.serial.autoconfig.serial.CloudwalkSerialAutoConfiguration diff --git a/maven-cw-elevator-application/.gitignore b/maven-cw-elevator-application/.gitignore index 3f74a625..aa0d16a7 100644 --- a/maven-cw-elevator-application/.gitignore +++ b/maven-cw-elevator-application/.gitignore @@ -1,2 +1,5 @@ # 发布目录中的可执行 JAR 体积大,默认不纳入 Git;说明与清单可单独跟踪。 releases/**/*.jar + +# scripts/build_nexus_only.sh 使用的隔离 Maven 本地仓库(仅 Nexus 依赖缓存) +.m2-elevator-nexus-only/ diff --git a/maven-cw-elevator-application/cw-elevator-application-common/pom.xml b/maven-cw-elevator-application/cw-elevator-application-common/pom.xml index 94b5abf7..9835256c 100644 --- a/maven-cw-elevator-application/cw-elevator-application-common/pom.xml +++ b/maven-cw-elevator-application/cw-elevator-application-common/pom.xml @@ -58,6 +58,15 @@ cn.cloudwalk.cloud cloudwalk-common-web + + + org.springframework.boot + spring-boot-autoconfigure + + + org.apache.commons + commons-lang3 + com.google.zxing core diff --git a/maven-cw-elevator-application/cw-elevator-application-data/src/main/resources/mapper/mybatis-config.xml b/maven-cw-elevator-application/cw-elevator-application-data/src/main/resources/mapper/mybatis-config.xml new file mode 100644 index 00000000..37659cb8 --- /dev/null +++ b/maven-cw-elevator-application/cw-elevator-application-data/src/main/resources/mapper/mybatis-config.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/maven-cw-elevator-application/cw-elevator-application-service/pom.xml b/maven-cw-elevator-application/cw-elevator-application-service/pom.xml index b45b3ac5..538dc6e8 100644 --- a/maven-cw-elevator-application/cw-elevator-application-service/pom.xml +++ b/maven-cw-elevator-application/cw-elevator-application-service/pom.xml @@ -56,6 +56,10 @@ cn.cloudwalk.cloud cloudwalk-common-event + + cn.cloudwalk.cloud + cwos-sdk-event + cn.cloudwalk.elevator cw-elevator-application-data @@ -73,6 +77,14 @@ org.springframework.cloud spring-cloud-starter-openfeign + + javax.annotation + javax.annotation-api + + + com.google.code.findbugs + jsr305 + diff --git a/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/CloudwalkSessionHolderConfiguration.java b/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/CloudwalkSessionHolderConfiguration.java new file mode 100644 index 00000000..515d8263 --- /dev/null +++ b/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/CloudwalkSessionHolderConfiguration.java @@ -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(); + } +} diff --git a/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/mqtt/client/MqttFeignClient.java b/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/mqtt/client/MqttFeignClient.java index 1e70ffff..e8862910 100644 --- a/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/mqtt/client/MqttFeignClient.java +++ b/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/mqtt/client/MqttFeignClient.java @@ -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; diff --git a/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/visitor/client/VisitorFeignClient.java b/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/visitor/client/VisitorFeignClient.java index bca013c2..10dd3371 100644 --- a/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/visitor/client/VisitorFeignClient.java +++ b/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/visitor/client/VisitorFeignClient.java @@ -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; diff --git a/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/zone/client/ZoneFeignClient.java b/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/zone/client/ZoneFeignClient.java index c7ef5ebc..470edf1f 100644 --- a/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/zone/client/ZoneFeignClient.java +++ b/maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/zone/client/ZoneFeignClient.java @@ -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; diff --git a/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/ElevatorApplication.java b/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/ElevatorApplication.java index 0db6b784..95d339f7 100644 --- a/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/ElevatorApplication.java +++ b/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/ElevatorApplication.java @@ -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); } -} +} \ No newline at end of file diff --git a/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/config/DavinciStorageBeansConfiguration.java b/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/config/DavinciStorageBeansConfiguration.java new file mode 100644 index 00000000..ea8ea6e7 --- /dev/null +++ b/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/config/DavinciStorageBeansConfiguration.java @@ -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); + } +} diff --git a/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/integration/davinci/OpenFeignFileStorageManager.java b/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/integration/davinci/OpenFeignFileStorageManager.java new file mode 100644 index 00000000..4a6c17fa --- /dev/null +++ b/maven-cw-elevator-application/cw-elevator-application-starter/src/main/java/cn/cloudwalk/elevator/integration/davinci/OpenFeignFileStorageManager.java @@ -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 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 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 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 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 result = this.fileManagerFeign.getFileData(path); + requireDavinciResult(result, "getFileData"); + if (result.isSuccess()) { + return result.getData(); + } + throw new DavinciServiceException(result.getCode(), result.getMessage()); + } + + @Override + public List remove(FileRemoveDTO dto) throws DavinciServiceException { + DavinciResult> 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", "读取远程文件流失败"); + } + } +} diff --git a/maven-cw-elevator-application/cw-elevator-application-starter/src/main/resources/logs/logback.xml b/maven-cw-elevator-application/cw-elevator-application-starter/src/main/resources/logs/logback.xml new file mode 100644 index 00000000..e6ec14c4 --- /dev/null +++ b/maven-cw-elevator-application/cw-elevator-application-starter/src/main/resources/logs/logback.xml @@ -0,0 +1,56 @@ + + + + ${CONTEXT_NAME} + + + + + + + + + + + + + + [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{50}:%line - %msg%n + + UTF-8 + + + + + + ${LOG_PATH}/${fileName}.log + + + ${LOG_PATH}/${fileName}.%d{yyyy-MM-dd}.%i.log + + 7 + + + 50MB + + + + + + [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{50}:%line - %msg%n + + UTF-8 + + + + + + + + + + + + + + diff --git a/maven-cw-elevator-application/cw-elevator-application-web/pom.xml b/maven-cw-elevator-application/cw-elevator-application-web/pom.xml index 108b7f80..114c10e1 100644 --- a/maven-cw-elevator-application/cw-elevator-application-web/pom.xml +++ b/maven-cw-elevator-application/cw-elevator-application-web/pom.xml @@ -20,7 +20,7 @@ javax.servlet - servlet-api + javax.servlet-api provided diff --git a/maven-cw-elevator-application/deploy/.gitignore b/maven-cw-elevator-application/deploy/.gitignore new file mode 100644 index 00000000..7617bb59 --- /dev/null +++ b/maven-cw-elevator-application/deploy/.gitignore @@ -0,0 +1,5 @@ +# 体积大,由 sync-jars.sh 生成;属性文件可版本管理 +*.jar + +# 运行/调试产生的日志不纳入版本库 +**/logs/*.log diff --git a/maven-cw-elevator-application/deploy/README.md b/maven-cw-elevator-application/deploy/README.md new file mode 100644 index 00000000..a25b7d8c --- /dev/null +++ b/maven-cw-elevator-application/deploy/README.md @@ -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$//' {} +`(在 `反编译` 根目录) diff --git a/maven-cw-elevator-application/deploy/common-java.sh b/maven-cw-elevator-application/deploy/common-java.sh new file mode 100644 index 00000000..454673fb --- /dev/null +++ b/maven-cw-elevator-application/deploy/common-java.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# 由 v1-legacy/run.sh、v2-maven/run.sh source:JAVA_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" +} diff --git a/maven-cw-elevator-application/deploy/consul-docker/docker-compose.yml b/maven-cw-elevator-application/deploy/consul-docker/docker-compose.yml new file mode 100644 index 00000000..7615104a --- /dev/null +++ b/maven-cw-elevator-application/deploy/consul-docker/docker-compose.yml @@ -0,0 +1,31 @@ +# Consul 单机 Server(Docker),镜像 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: diff --git a/maven-cw-elevator-application/deploy/merge-redis-json.sh b/maven-cw-elevator-application/deploy/merge-redis-json.sh new file mode 100755 index 00000000..ecf7c6c9 --- /dev/null +++ b/maven-cw-elevator-application/deploy/merge-redis-json.sh @@ -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 }" +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 diff --git a/maven-cw-elevator-application/deploy/sync-jars.sh b/maven-cw-elevator-application/deploy/sync-jars.sh new file mode 100755 index 00000000..26b2c38e --- /dev/null +++ b/maven-cw-elevator-application/deploy/sync-jars.sh @@ -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 diff --git a/maven-cw-elevator-application/deploy/v1-legacy/application.properties b/maven-cw-elevator-application/deploy/v1-legacy/application.properties new file mode 100644 index 00000000..4465f9b3 --- /dev/null +++ b/maven-cw-elevator-application/deploy/v1-legacy/application.properties @@ -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-std;Consul 无注册时出现「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 diff --git a/maven-cw-elevator-application/deploy/v1-legacy/bootstrap.properties b/maven-cw-elevator-application/deploy/v1-legacy/bootstrap.properties new file mode 100644 index 00000000..e7611f36 --- /dev/null +++ b/maven-cw-elevator-application/deploy/v1-legacy/bootstrap.properties @@ -0,0 +1,4 @@ +# 与 JAR 同目录,Spring Cloud 会加载本文件,覆盖 jar 内 bootstrap.properties 中的旧 Consul 地址。 +# 对应 192.168.3.12 上 Docker: hashicorp/consul:1.22(8500) +spring.cloud.consul.host=192.168.3.12 +spring.cloud.consul.port=8500 diff --git a/maven-cw-elevator-application/deploy/v1-legacy/redis-override.properties b/maven-cw-elevator-application/deploy/v1-legacy/redis-override.properties new file mode 100644 index 00000000..5b601dfd --- /dev/null +++ b/maven-cw-elevator-application/deploy/v1-legacy/redis-override.properties @@ -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 diff --git a/maven-cw-elevator-application/deploy/v1-legacy/run.sh b/maven-cw-elevator-application/deploy/v1-legacy/run.sh new file mode 100755 index 00000000..d9ad243f --- /dev/null +++ b/maven-cw-elevator-application/deploy/v1-legacy/run.sh @@ -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 "需要 python3(deploy/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 diff --git a/maven-cw-elevator-application/deploy/v2-maven/application.properties b/maven-cw-elevator-application/deploy/v2-maven/application.properties new file mode 100644 index 00000000..127db35b --- /dev/null +++ b/maven-cw-elevator-application/deploy/v2-maven/application.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 diff --git a/maven-cw-elevator-application/deploy/v2-maven/bootstrap.properties b/maven-cw-elevator-application/deploy/v2-maven/bootstrap.properties new file mode 100644 index 00000000..bb898826 --- /dev/null +++ b/maven-cw-elevator-application/deploy/v2-maven/bootstrap.properties @@ -0,0 +1,3 @@ +# 覆盖 fat-jar 内嵌 Consul 地址,指向局域网 Docker Consul。 +spring.cloud.consul.host=192.168.3.12 +spring.cloud.consul.port=8500 diff --git a/maven-cw-elevator-application/deploy/v2-maven/redis-override.properties b/maven-cw-elevator-application/deploy/v2-maven/redis-override.properties new file mode 100644 index 00000000..d1fbf001 --- /dev/null +++ b/maven-cw-elevator-application/deploy/v2-maven/redis-override.properties @@ -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 diff --git a/maven-cw-elevator-application/deploy/v2-maven/run.sh b/maven-cw-elevator-application/deploy/v2-maven/run.sh new file mode 100755 index 00000000..e1571d45 --- /dev/null +++ b/maven-cw-elevator-application/deploy/v2-maven/run.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# 与当前目录下 application.properties 同路径启动 V2(maven 构建)包。 +# 默认优先系统 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 "需要 python3(deploy/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 diff --git a/maven-cw-elevator-application/pom.xml b/maven-cw-elevator-application/pom.xml index 0bd1a1fb..4f15c377 100644 --- a/maven-cw-elevator-application/pom.xml +++ b/maven-cw-elevator-application/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.18.RELEASE + 1.5.17.RELEASE @@ -15,7 +15,7 @@ 2.0-SNAPSHOT pom cw-elevator-application (Maven reactor) - 聚合模块:common → data → service → web。release/cw-elevator-v1-lib-min-risk:cloudwalk-common-service/event 与 V1 cw_lib 一致为 3.7.2;部分第三方与 cw-elevator-application-V1.0.0.20211103/lib 文件名对齐。对照目录见 cw.elevator.v1.lib.dir。 + 聚合模块: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。 cw-elevator-application-common @@ -32,24 +32,31 @@ 3.7.2-Brussels-SRX 3.7.2-Brussels-SRX - 3.0.0-xinghewan - + + 2.9.2-xinghewan + 1.2.73 - 28.2-jre - 4.1.2 - 1.10.12 + 20.0 + 3.15 + 1.9.6 0.4.8 2.5 + + 3.5 2.6.2 3.3.3 5.1.2 1.2.5 4.0.0 - 3.5.6 - 2.0.6 - 2.0.1 - 2.5 + 3.4.6 + 1.3.2 + 1.3.1 + 3.1.0 + 1.3.2 + + 3.0.2 1.0.0-SNAPSHOT + 1.5.0-SNAPSHOT 1.1.1-SNAPSHOT 1.1.7-SNAPSHOT @@ -57,8 +64,8 @@ ${nexus.baseUrl}/repository/maven-public/ 2.24.1 ${project.basedir}/../docs/style/alibaba-eclipse-codestyle.xml - - Greenwich.SR6 + + Edgware.SR6 cw-elevator-application-2.0.0 @@ -107,6 +114,11 @@ cwos-java-sdk-resource ${cwos.sdk.resource.version} + + cn.cloudwalk.cloud + cwos-sdk-event + ${cwos.sdk.event.version} + cn.cloudwalk.intelligent cloudwalk-intelligent-component-lock @@ -147,6 +159,11 @@ commons-io ${commons-io.version} + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + com.alibaba fastjson @@ -194,8 +211,18 @@ javax.servlet - servlet-api - ${servlet-api.version} + javax.servlet-api + ${javax.servlet-api.version} + + + javax.annotation + javax.annotation-api + ${javax.annotation-api.version} + + + com.google.code.findbugs + jsr305 + ${jsr305.version} javax.el diff --git a/maven-cw-elevator-application/scripts/build_nexus_only.sh b/maven-cw-elevator-application/scripts/build_nexus_only.sh new file mode 100755 index 00000000..e0891415 --- /dev/null +++ b/maven-cw-elevator-application/scripts/build_nexus_only.sh @@ -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 父 POM(cwos / davinci)及 cloudwalk-device-sdk 父 POM:若相邻仓库存在对应 pom.xml,则自动 mvn -N install +# 到隔离库:maven-cwos-resource 的 cwos-component-resource、cwos-portal;maven-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 复制 parent(artifactId 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 跳过 install;ELEVATOR_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 "==> 向隔离库安装父 POM(mvn -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-xinghewan(cw_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 diff --git a/maven-cw-elevator-application/scripts/deploy_intelligent_cwoscomponent_to_nexus.sh b/maven-cw-elevator-application/scripts/deploy_intelligent_cwoscomponent_to_nexus.sh new file mode 100755 index 00000000..b2aa1de5 --- /dev/null +++ b/maven-cw-elevator-application/scripts/deploy_intelligent_cwoscomponent_to_nexus.sh @@ -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 id:nexus-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 "==> 跳过本机 ~/.m2(ELEVATOR_INSTALL_LOCAL_M2=0)" +fi + +echo "完成。" +echo " ~/.m2: ls \"\$HOME/.m2/repository/cn/cloudwalk/intelligent/intelligent-cwoscomponent-rest/$ICOMP_VER/\"" diff --git a/maven-cw-elevator-application/scripts/legacy-poms/intelligent-cwoscomponent-2.9.2-xinghewan-parent.pom b/maven-cw-elevator-application/scripts/legacy-poms/intelligent-cwoscomponent-2.9.2-xinghewan-parent.pom new file mode 100644 index 00000000..1a348694 --- /dev/null +++ b/maven-cw-elevator-application/scripts/legacy-poms/intelligent-cwoscomponent-2.9.2-xinghewan-parent.pom @@ -0,0 +1,56 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.18.RELEASE + + + cn.cloudwalk.intelligent + intelligent-cwoscomponent + 2.9.2-xinghewan + pom + intelligent-cwoscomponent (2.9.2 parent stub) + 与 V1 cw_lib 内 intelligent-cwoscomponent-*.pom 中 parent 坐标一致;DM 与电梯 reactor 3.7.2 / Greenwich.SR6 对齐,供 install-file 安装到本机库或隔离库。 + + 1.8 + Greenwich.SR6 + 3.7.2-Brussels-SRX + 3.7.2-Brussels-SRX + 1.2.73 + 2.2.0 + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + cn.cloudwalk.cloud + cloudwalk-common-result + ${cloudwalk.legacy.public.version} + + + cn.cloudwalk.cloud + cloudwalk-common-service + ${cloudwalk.internal.version} + + + com.alibaba + fastjson + ${fastjson.version} + + + cn.cloudwalk + cloudwalk-device-sdk-protocol-entity + ${cloudwalk.device.sdk.version} + + + + diff --git a/maven-cw-elevator-application/scripts/run_elevator_parity.sh b/maven-cw-elevator-application/scripts/run_elevator_parity.sh index 6c3a4208..ebe5f186 100755 --- a/maven-cw-elevator-application/scripts/run_elevator_parity.sh +++ b/maven-cw-elevator-application/scripts/run_elevator_parity.sh @@ -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)" diff --git a/maven-cw-elevator-application/scripts/run_full_elevator_api_suite.sh b/maven-cw-elevator-application/scripts/run_full_elevator_api_suite.sh new file mode 100755 index 00000000..0aab5cbc --- /dev/null +++ b/maven-cw-elevator-application/scripts/run_full_elevator_api_suite.sh @@ -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" diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/.gitignore b/maven-cw-elevator-application/tools/elevator_api_parity/.gitignore index ab09d337..b612d50f 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/.gitignore +++ b/maven-cw-elevator-application/tools/elevator_api_parity/.gitignore @@ -2,3 +2,7 @@ __pycache__/ *.pyc .pytest_cache/ report/parity-*.md +report/smoke-*.md +report/SUITE-*.md +report/*.json +.suite_run_marker diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/README.md b/maven-cw-elevator-application/tools/elevator_api_parity/README.md index 77c01cc4..5f7c0f3b 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/README.md +++ b/maven-cw-elevator-application/tools/elevator_api_parity/README.md @@ -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` — 纳入横向对拍(当前默认对 **访客/人员/规则分页/记录分页** 四个核心场景开启)。 diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/api_catalog.json b/maven-cw-elevator-application/tools/elevator_api_parity/api_catalog.json index 02fa4003..9a5a5cbf 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/api_catalog.json +++ b/maven-cw-elevator-application/tools/elevator_api_parity/api_catalog.json @@ -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 } ] } diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/conftest.py b/maven-cw-elevator-application/tools/elevator_api_parity/conftest.py index 93672553..01688b8e 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/conftest.py +++ b/maven-cw-elevator-application/tools/elevator_api_parity/conftest.py @@ -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}") diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/parity/catalog_loader.py b/maven-cw-elevator-application/tools/elevator_api_parity/parity/catalog_loader.py index da7ab427..d7684dcb 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/parity/catalog_loader.py +++ b/maven-cw-elevator-application/tools/elevator_api_parity/parity/catalog_loader.py @@ -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)) diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/parity/client.py b/maven-cw-elevator-application/tools/elevator_api_parity/parity/client.py index a19baf56..468453ad 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/parity/client.py +++ b/maven-cw-elevator-application/tools/elevator_api_parity/parity/client.py @@ -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]: diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/pytest.ini b/maven-cw-elevator-application/tools/elevator_api_parity/pytest.ini index 533e2901..c06bae43 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/pytest.ini +++ b/maven-cw-elevator-application/tools/elevator_api_parity/pytest.ini @@ -4,5 +4,6 @@ addopts = -q --strict-markers testpaths = tests markers = live: 需要两实例可访问 + smoke: 单机全量 HTTP 探测 unit: 纯逻辑单测 pythonpath = . diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_report.py b/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_report.py index 6a432d67..696f5022 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_report.py +++ b/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_report.py @@ -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") diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_smoke_report.py b/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_smoke_report.py new file mode 100644 index 00000000..0ec78e36 --- /dev/null +++ b/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_smoke_report.py @@ -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" + ) diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_suite_summary.py b/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_suite_summary.py new file mode 100644 index 00000000..9ec34b83 --- /dev/null +++ b/maven-cw-elevator-application/tools/elevator_api_parity/report/generate_suite_summary.py @@ -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() diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/tests/test_parity_endpoints.py b/maven-cw-elevator-application/tools/elevator_api_parity/tests/test_parity_endpoints.py index 560b5b31..f18b4729 100644 --- a/maven-cw-elevator-application/tools/elevator_api_parity/tests/test_parity_endpoints.py +++ b/maven-cw-elevator-application/tools/elevator_api_parity/tests/test_parity_endpoints.py @@ -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"), diff --git a/maven-cw-elevator-application/tools/elevator_api_parity/tests/test_smoke_catalog.py b/maven-cw-elevator-application/tools/elevator_api_parity/tests/test_smoke_catalog.py new file mode 100644 index 00000000..e876e760 --- /dev/null +++ b/maven-cw-elevator-application/tools/elevator_api_parity/tests/test_smoke_catalog.py @@ -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_rows(sessionfinish 落盘)。""" + + +@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 diff --git a/maven-intelligent-cwoscomponent/intelligent-cwoscomponent-interface/pom.xml b/maven-intelligent-cwoscomponent/intelligent-cwoscomponent-interface/pom.xml index 6267dba1..b8033d00 100644 --- a/maven-intelligent-cwoscomponent/intelligent-cwoscomponent-interface/pom.xml +++ b/maven-intelligent-cwoscomponent/intelligent-cwoscomponent-interface/pom.xml @@ -30,6 +30,19 @@ cn.cloudwalk cloudwalk-device-sdk-protocol-entity + + + org.apache.commons + commons-lang3 + + + javax.validation + validation-api + + + com.fasterxml.jackson.core + jackson-annotations + diff --git a/maven-intelligent-cwoscomponent/pom.xml b/maven-intelligent-cwoscomponent/pom.xml index e59b29a4..fb41cf88 100644 --- a/maven-intelligent-cwoscomponent/pom.xml +++ b/maven-intelligent-cwoscomponent/pom.xml @@ -15,7 +15,7 @@ 3.0.0-xinghewan pom intelligent-cwoscomponent (Maven reactor) - 聚合模块:interface → rest。原父 intelligent-cwoscomponent 缺失。 + 聚合模块:interface → rest(反应堆版本 3.0.0-xinghewan)。注:maven-cw-elevator-application 上线口径固定依赖 cw_lib 的 2.9.2-xinghewan,不以此反应堆产物替代;见电梯 scripts/build_nexus_only.sh。 intelligent-cwoscomponent-parent @@ -26,7 +26,8 @@ 1.8 Greenwich.SR6 - 4.0.0-Brussels-SRX + + 3.7.2-Brussels-SRX 3.7.2-Brussels-SRX 1.2.83 diff --git a/maven-ninca-qk-alarm/ninca-qk-alarm-app-starter/src/main/resources/sh/check-restart.sh b/maven-ninca-qk-alarm/ninca-qk-alarm-app-starter/src/main/resources/sh/check-restart.sh index 340e891a..8f6b55f3 100644 --- a/maven-ninca-qk-alarm/ninca-qk-alarm-app-starter/src/main/resources/sh/check-restart.sh +++ b/maven-ninca-qk-alarm/ninca-qk-alarm-app-starter/src/main/resources/sh/check-restart.sh @@ -1,3 +1,3 @@ -#!/bin/bash - -systemctl restart ninca-qk-alarm-app.service +#!/bin/bash + +systemctl restart ninca-qk-alarm-app.service diff --git a/scripts/run_full_elevator_api_suite.sh b/scripts/run_full_elevator_api_suite.sh new file mode 100755 index 00000000..4833243b --- /dev/null +++ b/scripts/run_full_elevator_api_suite.sh @@ -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" "$@"