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
@@ -6,7 +6,7 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
@@ -14,12 +14,29 @@ import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
@EnableCaching
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableFeignClients(basePackages = "cn.cloudwalk.elevator")
@MapperScan("cn.cloudwalk.elevator")
@SpringBootApplication(exclude = {PageHelperAutoConfiguration.class})
@EnableFeignClients(basePackages = {
"cn.cloudwalk.elevator",
"cn.cloudwalk.rest.cwoscomponent",
"cn.cloudwalk.cwos.client.resource"
})
@MapperScan({
"cn.cloudwalk.elevator.record.mapper",
"cn.cloudwalk.elevator.device.mapper",
"cn.cloudwalk.elevator.passrule.mapper",
"cn.cloudwalk.elevator.person.mapper",
"cn.cloudwalk.elevator.codeElevatorArea.mapper"
})
@SpringBootApplication(
exclude = {PageHelperAutoConfiguration.class},
scanBasePackages = {
"cn.cloudwalk.elevator",
"cn.cloudwalk.rest.cwoscomponent",
"cn.cloudwalk.serial",
"cn.cloudwalk.cwos.client.resource"
})
public class ElevatorApplication {
public static void main(String[] args) {
SpringApplication.run(ElevatorApplication.class, args);
}
}
}
@@ -0,0 +1,37 @@
package cn.cloudwalk.elevator.config;
import cn.cloudwalk.elevator.integration.davinci.OpenFeignFileStorageManager;
import cn.cloudwalk.intelligent.davinci.storage.manager.FilePartManager;
import cn.cloudwalk.intelligent.davinci.storage.manager.FileStorageManager;
import cn.cloudwalk.intelligent.davinci.storage.manager.impl.FilePartManagerImpl;
import feign.Client;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.feign.FeignClientsConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(FeignClientsConfiguration.class)
public class DavinciStorageBeansConfiguration {
@Bean
public FileStorageManager fileStorageManager(
@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName,
Decoder decoder,
Encoder encoder,
Client client) {
return new OpenFeignFileStorageManager(serviceName, decoder, encoder, client);
}
@Bean
public FilePartManager filePartManager(
@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName,
Decoder decoder,
Encoder encoder,
Client client) {
return new FilePartManagerImpl(serviceName, decoder, encoder, client);
}
}
@@ -0,0 +1,228 @@
package cn.cloudwalk.elevator.integration.davinci;
import cn.cloudwalk.intelligent.davinci.common.exception.DavinciServiceException;
import cn.cloudwalk.intelligent.davinci.common.result.DavinciResult;
import cn.cloudwalk.intelligent.davinci.storage.bean.file.dto.FileRemoveDTO;
import cn.cloudwalk.intelligent.davinci.storage.feign.FileManagerFeign;
import cn.cloudwalk.intelligent.davinci.storage.feign.OuterCallFeignClient;
import cn.cloudwalk.intelligent.davinci.storage.manager.FileStorageManager;
import feign.Client;
import feign.Feign;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import feign.okhttp.OkHttpClient;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Locale;
import org.apache.commons.io.IOUtils;
import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/**
* 与 davinci-manager-storage 中逻辑一致,但固定使用 {@link SpringMvcContract}OpenFeign)。
* 避免依赖 Nexus 仍带 Netflix 引用的旧 {@code FileStorageManagerImpl} 字节码导致 NoClassDefFoundError。
*/
public class OpenFeignFileStorageManager implements FileStorageManager {
private final FileManagerFeign fileManagerFeign;
private final FileManagerFeign fileManagerRestFeign;
public OpenFeignFileStorageManager(String serviceName, Decoder decoder, Encoder encoder, Client client) {
String url = "http://" + serviceName + "/portal/fileManager";
this.fileManagerFeign = Feign.builder().client(client).decode404().encoder(new SpringFormEncoder())
.decoder(decoder).contract(new SpringMvcContract()).target(FileManagerFeign.class, url);
this.fileManagerRestFeign = Feign.builder().client(client).decode404().encoder(encoder).decoder(decoder)
.contract(new SpringMvcContract()).target(FileManagerFeign.class, url);
}
static void assertSafeHttpUrl(String urlString) throws DavinciServiceException {
if (StringUtils.isEmpty(urlString)) {
throw new DavinciServiceException("INVALID_URL", "URL 为空");
}
URI uri;
try {
uri = new URI(urlString);
} catch (URISyntaxException e) {
throw new DavinciServiceException("INVALID_URL", "URL 非法");
}
String scheme = uri.getScheme();
if (scheme == null || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
throw new DavinciServiceException("INVALID_URL", "仅允许 http 或 https 协议");
}
String host = uri.getHost();
if (StringUtils.isEmpty(host)) {
throw new DavinciServiceException("INVALID_URL", "缺少主机名");
}
String lowerHost = host.toLowerCase(Locale.ROOT);
if ("localhost".equals(lowerHost) || lowerHost.endsWith(".local")) {
throw new DavinciServiceException("INVALID_URL", "禁止访问该主机");
}
if ("metadata.google.internal".equalsIgnoreCase(host)) {
throw new DavinciServiceException("INVALID_URL", "禁止访问该主机");
}
try {
InetAddress[] all = InetAddress.getAllByName(host);
for (InetAddress addr : all) {
if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress()
|| addr.isSiteLocalAddress() || addr.isMulticastAddress()) {
throw new DavinciServiceException("INVALID_URL", "禁止访问内网或保留地址");
}
}
} catch (UnknownHostException e) {
throw new DavinciServiceException("INVALID_URL", "无法解析主机");
}
}
private static void requireDavinciResult(DavinciResult<?> result, String op) throws DavinciServiceException {
if (result == null) {
throw new DavinciServiceException("NULL_RESULT", "Davinci-portal 返回空结果: " + op);
}
}
private static InputStream attachResponseClose(InputStream bodyStream, Response response) {
return new FilterInputStream(bodyStream) {
@Override
public void close() throws IOException {
try {
super.close();
} finally {
response.close();
}
}
};
}
@Override
public String fileUpload(MultipartFile file) throws DavinciServiceException {
DavinciResult<String> result = this.fileManagerFeign.fileUpload(file);
requireDavinciResult(result, "fileUpload");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public String fileUpload(String moduleCategory, MultipartFile file) throws DavinciServiceException {
DavinciResult<String> result = this.fileManagerFeign.fileUpload(moduleCategory, file);
requireDavinciResult(result, "fileUpload(module)");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public String bigFileUpload(MultipartFile file) throws DavinciServiceException {
DavinciResult<String> result = this.fileManagerFeign.bigFileUpload(file);
requireDavinciResult(result, "bigFileUpload");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public String bigFileUpload(String moduleCategory, MultipartFile file) throws DavinciServiceException {
DavinciResult<String> result = this.fileManagerFeign.bigFileUpload(moduleCategory, file);
requireDavinciResult(result, "bigFileUpload(module)");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public byte[] fileDownload(String path) throws DavinciServiceException {
try (Response response = this.fileManagerFeign.fileDownload(path)) {
if (response == null) {
return null;
}
if (response.body() == null) {
return null;
}
try (InputStream inputStream = response.body().asInputStream()) {
return IOUtils.toByteArray(inputStream);
}
} catch (IOException e) {
throw new DavinciServiceException("FILE_DOWNLOAD_IO", "调用Davinci-portal服务,获取文件流接口异常");
}
}
@Override
public InputStream fileDownloadStream(String path) throws DavinciServiceException {
Response response = this.fileManagerFeign.fileDownload(path);
try {
if (response == null) {
return null;
}
if (response.body() == null) {
response.close();
return null;
}
return attachResponseClose(response.body().asInputStream(), response);
} catch (IOException e) {
if (response != null) {
response.close();
}
throw new DavinciServiceException("FILE_DOWNLOAD_IO", "调用Davinci-portal服务,获取文件流接口异常");
}
}
@Override
public String getFileBase64(String path) throws DavinciServiceException {
if (StringUtils.isEmpty(path)) {
return "";
}
DavinciResult<String> result = this.fileManagerFeign.getFileData(path);
requireDavinciResult(result, "getFileData");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public List<String> remove(FileRemoveDTO dto) throws DavinciServiceException {
DavinciResult<List<String>> result = this.fileManagerRestFeign.remove(dto);
requireDavinciResult(result, "remove");
if (result.isSuccess()) {
return result.getData();
}
throw new DavinciServiceException(result.getCode(), result.getMessage());
}
@Override
public InputStream fileDownLoadWithAbsoluteUrl(String url) throws DavinciServiceException {
assertSafeHttpUrl(url);
OuterCallFeignClient feignClient = Feign.builder().client(new OkHttpClient()).target(OuterCallFeignClient.class,
url);
Response response;
try {
response = feignClient.downLoad();
} catch (RuntimeException e) {
throw new DavinciServiceException("OUTER_DOWNLOAD", "拉取远程文件失败");
}
try {
if (response.body() == null) {
response.close();
return null;
}
return attachResponseClose(response.body().asInputStream(), response);
} catch (IOException e) {
response.close();
throw new DavinciServiceException("OUTER_DOWNLOAD", "读取远程文件流失败");
}
}
}
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true">
<property name="CONTEXT_NAME" value="api.1.0.0"/>
<contextName>${CONTEXT_NAME}</contextName>
<springProperty scope="context" name="fileName" source="logging.file" defaultValue="default"/>
<!--myibatis log configure-->
<logger name="com.apache.ibatis" level="DEBUG"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 控制台输出 -->
<appender name="S" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{50}:%line - %msg%n</pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="R" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_PATH}/${fileName}.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_PATH}/${fileName}.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>7</MaxHistory>
<!--日志文件大小 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{50}:%line - %msg%n</pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
<!--日志文件最大的大小-->
<!-- <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> -->
<!-- <MaxFileSize>10MB</MaxFileSize> -->
<!-- </triggeringPolicy> -->
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="S"/>
<appender-ref ref="R"/>
</root>
</configuration>