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

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

Made-with: Cursor

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