Ver código fonte

Merge branch 'vpp-zyj' into server-165

james 1 dia atrás
pai
commit
178ba46c3d
100 arquivos alterados com 3058 adições e 964 exclusões
  1. 82 0
      service-eg/service-eg-biz/pom.xml
  2. 10 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbConfiguration.java
  3. 75 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbProperties.java
  4. 17 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/api/ServiceEgTaskApi.java
  5. 97 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceAdbController.java
  6. 0 86
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFace.java
  7. 0 32
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFaceDevice.java
  8. 0 7
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceMapper.java
  9. 0 18
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceDeviceMapper.java
  10. 0 18
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceMapper.java
  11. 6 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java
  12. 67 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttOutboundConfig.java
  13. 81 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttProperties.java
  14. 20 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceAdbService.java
  15. 19 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceRemoteUpgradeMqttService.java
  16. 325 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceAdbServiceImpl.java
  17. 173 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceRemoteUpgradeMqttServiceImpl.java
  18. 34 7
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.java
  19. 3 1
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgRecordServiceImpl.java
  20. 13 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbEndpointVO.java
  21. 19 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeBatchItemVO.java
  22. 21 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeBatchResultVO.java
  23. 34 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeItemResultVO.java
  24. 36 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeVO.java
  25. 29 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbResultVO.java
  26. 15 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbTcpipVO.java
  27. 4 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonVO.java
  28. 66 0
      service-eg/service-eg-biz/src/main/resources/bootstrap.yml
  29. 0 24
      service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceMapper.xml
  30. 0 11
      service-eg/service-eg-biz/src/main/resources/mapper/eg/MeetingFaceDeviceMapper.xml
  31. 0 21
      service-eg/service-eg-biz/src/main/resources/mapper/eg/MeetingFaceMapper.xml
  32. 13 0
      service-eg/service-eg-biz/src/main/resources/mapper/eg/SysPersonMapper.xml
  33. 18 0
      service-job/src/main/java/com/ruoyi/job/task/RyTask.java
  34. 0 10
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/FaceLog.java
  35. 0 50
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/aspect/FaceAspect.java
  36. 23 2
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingDeviceController.java
  37. 0 182
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceController.java
  38. 0 60
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceVefLogController.java
  39. 6 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevice.java
  40. 25 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevicePersonBind.java
  41. 0 75
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingFaceVefLog.java
  42. 48 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/SysPerson.java
  43. 29 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingDevicePersonBindMapper.java
  44. 0 6
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceMapper.java
  45. 0 16
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceVefLogMapper.java
  46. 21 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/SysPersonMapper.java
  47. 0 19
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/repository/MeetingInfoRepository.java
  48. 12 1
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingDeviceService.java
  49. 0 16
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingFaceVefLogService.java
  50. 192 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingDeviceServiceImpl.java
  51. 0 20
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFaceVefLogServiceImpl.java
  52. 0 1
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFileServiceImpl.java
  53. 23 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonQueryVO.java
  54. 27 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonVO.java
  55. 18 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindPersonRequest.java
  56. 34 0
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingDevicePersonBindMapper.xml
  57. 0 15
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceMapper.xml
  58. 0 16
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceVefLogMapper.xml
  59. 18 0
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/SysPersonMapper.xml
  60. 10 0
      service-vpp/service-vpp-api/src/main/java/com/usky/vpp/RemoteVppTaskService.java
  61. 20 2
      service-vpp/service-vpp-api/src/main/java/com/usky/vpp/factory/RemoteVppTaskFactory.java
  62. 31 14
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnHttpExecutor.java
  63. 50 30
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnTokenHolder.java
  64. 7 11
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnProperties.java
  65. 166 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnPropertiesRegistry.java
  66. 6 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnRestTemplateConfig.java
  67. 61 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnTenantContext.java
  68. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppDrEventStatus.java
  69. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppTsdbConstants.java
  70. 27 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/api/ServiceVppTaskApi.java
  71. 28 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java
  72. 114 7
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  73. 17 31
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java
  74. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResourceController.java
  75. 10 6
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/SiteController.java
  76. 104 34
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/crypto/VppUnCryptoService.java
  77. 232 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/crypto/VppUnSmCryptoUtil.java
  78. 19 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppBiddingConfig.java
  79. 16 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java
  80. 7 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractTemplate.java
  81. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java
  82. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java
  83. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  84. 1 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java
  85. 32 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java
  86. 64 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResponseDeviationPrice.java
  87. 85 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppTenantConfig.java
  88. 0 47
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnBootstrapRunner.java
  89. 0 36
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnPollScheduler.java
  90. 39 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnPropertiesLoader.java
  91. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppResponseDeviationPriceMapper.java
  92. 10 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppTenantConfigMapper.java
  93. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppAliyunSmsService.java
  94. 13 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppBaselineService.java
  95. 18 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractService.java
  96. 15 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrMonitorService.java
  97. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java
  98. 25 9
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java
  99. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceBoardService.java
  100. 2 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceOverviewService.java

+ 82 - 0
service-eg/service-eg-biz/pom.xml

@@ -16,6 +16,16 @@
             <artifactId>common-cloud-starter</artifactId>
         </dependency>
 
+        <!-- MQTT:远程升级指令下发(与终端 connectWithDevice 订阅主题对齐) -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-integration</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.integration</groupId>
+            <artifactId>spring-integration-mqtt</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-configuration-processor</artifactId>
@@ -58,6 +68,78 @@
             <version>0.0.1</version>
         </dependency>
 
+        <!-- SmartJavaAI Face Recognition Dependencies -->
+        <dependency>
+            <groupId>cn.smartjavaai</groupId>
+            <artifactId>face</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.smartjavaai</groupId>
+            <artifactId>common</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+
+        <!-- DJL Core Dependencies -->
+        <dependency>
+            <groupId>ai.djl</groupId>
+            <artifactId>api</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+        <dependency>
+            <groupId>ai.djl</groupId>
+            <artifactId>model-zoo</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+
+        <!-- DJL PyTorch Engine -->
+        <dependency>
+            <groupId>ai.djl.pytorch</groupId>
+            <artifactId>pytorch-engine</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+        <dependency>
+            <groupId>ai.djl.pytorch</groupId>
+            <artifactId>pytorch-model-zoo</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+
+        <!-- DJL ONNX Runtime -->
+        <dependency>
+            <groupId>ai.djl.onnxruntime</groupId>
+            <artifactId>onnxruntime-engine</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+
+        <!-- DJL OpenCV -->
+        <dependency>
+            <groupId>ai.djl.opencv</groupId>
+            <artifactId>opencv</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+
+        <!-- SQLite for face database -->
+        <dependency>
+            <groupId>org.xerial</groupId>
+            <artifactId>sqlite-jdbc</artifactId>
+            <version>3.36.0.3</version>
+        </dependency>
+
+        <!-- Milvus Vector Database (Optional) -->
+        <dependency>
+            <groupId>io.milvus</groupId>
+            <artifactId>milvus-sdk-java</artifactId>
+            <version>2.5.7</version>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Seetaface SDK -->
+        <dependency>
+            <groupId>io.gitee.dengwenjie</groupId>
+            <artifactId>seeta-sdk-platform</artifactId>
+            <version>1.2.2</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 10 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbConfiguration.java

@@ -0,0 +1,10 @@
+package com.usky.eg.adb;
+
+import com.usky.eg.mqtt.EgMqttProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties({EgDeviceAdbProperties.class, EgMqttProperties.class})
+public class EgDeviceAdbConfiguration {
+}

+ 75 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbProperties.java

@@ -0,0 +1,75 @@
+package com.usky.eg.adb;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 服务端调用官方 platform-tools adb(需在 PATH 或可执行路径配置)。
+ */
+@ConfigurationProperties(prefix = "eg.adb")
+public class EgDeviceAdbProperties {
+
+    /**
+     * adb 可执行文件名或绝对路径,默认依赖系统 PATH 中的 adb
+     */
+    private String executable = "adb";
+
+    /**
+     * connect / disconnect / tcpip 等命令超时(秒)
+     */
+    private long commandTimeoutSeconds = 45L;
+
+    /**
+     * screencap 超时(秒)
+     */
+    private long screenshotTimeoutSeconds = 60L;
+
+    /**
+     * tcpip 失败时是否先执行一次 adb connect 再重试 tcpip
+     */
+    private boolean tcpipRetryAfterConnect = true;
+
+    /**
+     * 截屏前是否先执行 adb connect(避免仅调 screenshot 时 adb 侧尚无该 serial,报 device not found)
+     */
+    private boolean screenshotConnectFirst = true;
+
+    public String getExecutable() {
+        return executable;
+    }
+
+    public void setExecutable(String executable) {
+        this.executable = executable;
+    }
+
+    public long getCommandTimeoutSeconds() {
+        return commandTimeoutSeconds;
+    }
+
+    public void setCommandTimeoutSeconds(long commandTimeoutSeconds) {
+        this.commandTimeoutSeconds = commandTimeoutSeconds;
+    }
+
+    public long getScreenshotTimeoutSeconds() {
+        return screenshotTimeoutSeconds;
+    }
+
+    public void setScreenshotTimeoutSeconds(long screenshotTimeoutSeconds) {
+        this.screenshotTimeoutSeconds = screenshotTimeoutSeconds;
+    }
+
+    public boolean isTcpipRetryAfterConnect() {
+        return tcpipRetryAfterConnect;
+    }
+
+    public void setTcpipRetryAfterConnect(boolean tcpipRetryAfterConnect) {
+        this.tcpipRetryAfterConnect = tcpipRetryAfterConnect;
+    }
+
+    public boolean isScreenshotConnectFirst() {
+        return screenshotConnectFirst;
+    }
+
+    public void setScreenshotConnectFirst(boolean screenshotConnectFirst) {
+        this.screenshotConnectFirst = screenshotConnectFirst;
+    }
+}

+ 17 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/api/ServiceEgTaskApi.java

@@ -0,0 +1,17 @@
+package com.usky.eg.controller.api;
+
+import com.usky.eg.RemoteEgService;
+import com.usky.eg.service.EgDeviceHeartbeatService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ServiceEgTaskApi implements RemoteEgService {
+    @Autowired
+    private EgDeviceHeartbeatService egDeviceHeartbeatService;
+
+    @Override
+    public void egDeviceStatus(){
+        egDeviceHeartbeatService.updateEgDeviceStatus();
+    }
+}

+ 97 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceAdbController.java

@@ -0,0 +1,97 @@
+package com.usky.eg.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.eg.service.EgDeviceAdbService;
+import com.usky.eg.service.EgDeviceRemoteUpgradeMqttService;
+import com.usky.eg.service.vo.EgDeviceAdbEndpointVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeVO;
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbTcpipVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 服务端官方 adb:部署机需安装 Android Platform Tools(adb 在 PATH),且网络可达设备。
+ * <p>
+ * 网关前缀一般为 /service-eg,完整路径示例:POST /service-eg/egDevice/adb/connect
+ */
+@RestController
+@RequestMapping("/egDevice/adb")
+public class EgDeviceAdbController {
+
+    @Autowired
+    private EgDeviceAdbService egDeviceAdbService;
+
+    @Autowired
+    private EgDeviceRemoteUpgradeMqttService egDeviceRemoteUpgradeMqttService;
+
+    @PostMapping("/connect")
+    public ApiResult<EgDeviceAdbResultVO> connect(@RequestBody EgDeviceAdbEndpointVO vo) {
+        EgDeviceAdbResultVO r = egDeviceAdbService.connect(vo.getDeviceIp(), vo.getDevicePort());
+        return ApiResult.success(r);
+    }
+
+    @PostMapping("/disconnect")
+    public ApiResult<EgDeviceAdbResultVO> disconnect(@RequestBody EgDeviceAdbEndpointVO vo) {
+        EgDeviceAdbResultVO r = egDeviceAdbService.disconnect(vo.getDeviceIp(), vo.getDevicePort());
+        return ApiResult.success(r);
+    }
+
+    @PostMapping("/tcpip")
+    public ApiResult<EgDeviceAdbResultVO> tcpip(@RequestBody EgDeviceAdbTcpipVO vo) {
+        EgDeviceAdbResultVO r = egDeviceAdbService.tcpip(vo.getDeviceIp(), vo.getDevicePort(), vo.getPort());
+        return ApiResult.success(r);
+    }
+
+    /**
+     * 远程升级:仅通过 MQTT 向 Broker 发布下行指令,由终端 {@code connectWithDevice} 订阅后在设备侧自行下载安装
+     * 单台:{@code fileUrl} 必填;{@code deviceId} 或 {@code deviceCode} 至少其一。
+     * 批量:非空 {@code items},每项含 {@code deviceId} 或 {@code deviceCode},并与同级 {@code fileUrl}、可选 {@code launchPackage} 共用。
+     * 完整路径示例:POST /service-eg/egDevice/adb/remoteUpgrade
+     */
+    @PostMapping("/remoteUpgrade")
+    public ApiResult<?> remoteUpgrade(@RequestBody EgDeviceAdbRemoteUpgradeVO vo) {
+        if (vo.getItems() != null && !vo.getItems().isEmpty()) {
+            EgDeviceAdbRemoteUpgradeBatchResultVO batch = egDeviceRemoteUpgradeMqttService.publishRemoteUpgradeBatch(
+                    vo.getItems(), vo.getFileUrl(), vo.getLaunchPackage());
+            return ApiResult.success(batch);
+        }
+        EgDeviceAdbResultVO r = egDeviceRemoteUpgradeMqttService.publishRemoteUpgrade(vo);
+        return ApiResult.success(r);
+    }
+
+    /**
+     * 原始 PNG 二进制,无 JSON 封装(前端 responseType: blob)。
+     * 使用 {@link HttpServletResponse} 写出流,避免全局 ApiResult 包装器把返回值当成 JSON 再强转为 byte[] 导致 ClassCastException。
+     */
+    @GetMapping(value = "/screenshot", produces = MediaType.IMAGE_PNG_VALUE)
+    public void screenshot(@RequestParam("deviceIp") String deviceIp,
+                           @RequestParam("devicePort") Integer devicePort,
+                           HttpServletResponse response) throws IOException {
+        try {
+            byte[] png = egDeviceAdbService.screenshot(deviceIp, devicePort);
+            response.setStatus(HttpServletResponse.SC_OK);
+            response.setContentType(MediaType.IMAGE_PNG_VALUE);
+            response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, no-cache");
+            response.setContentLength(png.length);
+            response.getOutputStream().write(png);
+            response.getOutputStream().flush();
+        } catch (BusinessException e) {
+            String msg = e.getMessage() != null ? e.getMessage() : "Bad Request";
+            byte[] err = msg.getBytes(StandardCharsets.UTF_8);
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            response.setContentType(MediaType.TEXT_PLAIN_VALUE);
+            response.setContentLength(err.length);
+            response.getOutputStream().write(err);
+            response.getOutputStream().flush();
+        }
+    }
+}

+ 0 - 86
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFace.java

@@ -1,86 +0,0 @@
-package com.usky.eg.domain;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import java.time.LocalDateTime;
-import java.io.Serializable;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * <p>
- * 
- * </p>
- *
- * @author zyj
- * @since 2024-11-27
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-public class MeetingFace implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 主键
-     */
-    @TableId(value = "fid", type = IdType.AUTO)
-    private Integer fid;
-
-    /**
-     * 图片数据 base_64编码
-     */
-    private String faceBase;
-
-    /**
-     * 插入时间
-     */
-    private LocalDateTime createTime;
-
-    /**
-     * 验证次数
-     */
-    private Integer vefNum;
-
-    /**
-     * 人脸名称
-     */
-    private String faceName;
-
-    /**
-     * 人脸备注
-     */
-    private String remark;
-
-    /**
-     * 人脸是否可用,(0==可用,1,不可用)
-     */
-    private Integer faceStatus;
-
-    /**
-     * 卡号
-     */
-    private String cardNum;
-
-    /**
-     * 绑定设备
-     */
-    private String bindDevice;
-
-    /**
-     * 组织机构ID
-     */
-    private Integer deptId;
-
-    /**
-     * 租户ID
-     */
-    private Integer tenantId;
-
-    /**
-     * 用户ID
-     */
-    private Long userId;
-
-
-}

+ 0 - 32
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFaceDevice.java

@@ -1,32 +0,0 @@
-package com.usky.eg.domain;
-
-import java.io.Serializable;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * <p>
- * 人脸设备关联表
- * </p>
- *
- * @author zyj
- * @since 2024-11-27
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-public class MeetingFaceDevice implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 人脸主键ID
-     */
-    private Integer faceId;
-
-    /**
-     * 设备主键ID
-     */
-    private Integer deviceId;
-
-
-}

+ 0 - 7
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceMapper.java

@@ -15,13 +15,6 @@ import org.springframework.stereotype.Repository;
  */
 @Repository
 public interface EgDeviceMapper extends CrudMapper<EgDevice> {
-    void insertMeetingFaceDevice(@Param("fid") Integer fid, @Param("egDeviceId") Integer egDeviceId);
-
-    void deleteMeetingFaceDevice(@Param("egDeviceId") Integer egDeviceId);
 
     Integer sysTenantId(@Param("domain") String domain);
-
-    Integer getMeetingFaceData(@Param("userId") Long userId);
-
-    Integer[] getMeetingFaceDeviceList(@Param("fid") Integer fid);
 }

+ 0 - 18
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceDeviceMapper.java

@@ -1,18 +0,0 @@
-package com.usky.eg.mapper;
-
-import com.usky.eg.domain.MeetingFaceDevice;
-import com.usky.common.mybatis.core.CrudMapper;
-import org.springframework.stereotype.Repository;
-
-/**
- * <p>
- * 人脸设备关联表 Mapper 接口
- * </p>
- *
- * @author zyj
- * @since 2024-11-27
- */
-@Repository
-public interface MeetingFaceDeviceMapper extends CrudMapper<MeetingFaceDevice> {
-
-}

+ 0 - 18
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceMapper.java

@@ -1,18 +0,0 @@
-package com.usky.eg.mapper;
-
-import com.usky.eg.domain.MeetingFace;
-import com.usky.common.mybatis.core.CrudMapper;
-import org.springframework.stereotype.Repository;
-
-/**
- * <p>
- *  Mapper 接口
- * </p>
- *
- * @author zyj
- * @since 2024-11-27
- */
-@Repository
-public interface MeetingFaceMapper extends CrudMapper<MeetingFace> {
-
-}

+ 6 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java

@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 人员信息表 Mapper
@@ -19,4 +20,9 @@ public interface SysPersonMapper extends BaseMapper<SysPerson> {
      */
     @Select("SELECT id, full_name, face_base, face_name, card_num FROM sys_person WHERE face_base IS NOT NULL AND face_base != '' AND face_status = 0")
     List<SysPerson> selectAllValidFace();
+
+    /**
+     * 按人员ID批量查询关联用户ID(sys_user_person)
+     */
+    List<Map<String, Object>> selectUserIdsByPersonIds(@Param("personIds") List<Integer> personIds);
 }

+ 67 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttOutboundConfig.java

@@ -0,0 +1,67 @@
+package com.usky.eg.mqtt;
+
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.annotation.MessagingGateway;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+import org.springframework.messaging.handler.annotation.Header;
+
+/**
+ * 出站 MQTT(发布远程升级下行指令)。
+ */
+@Configuration
+public class EgMqttOutboundConfig {
+
+    public static final String CHANNEL_NAME_OUT = "egMqttOutboundChannel";
+    public static final String DEFAULT_TOPIC = "eg/default";
+
+    @Autowired
+    private EgMqttProperties egMqttProperties;
+
+    @Bean
+    public MqttPahoClientFactory egMqttPahoClientFactory() {
+        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+        MqttConnectOptions options = new MqttConnectOptions();
+        options.setServerURIs(new String[]{egMqttProperties.getUrl()});
+        String user = egMqttProperties.getUsername();
+        if (user != null && !user.isEmpty()) {
+            options.setUserName(user);
+        }
+        String pwd = egMqttProperties.getPassword();
+        if (pwd != null && !pwd.isEmpty()) {
+            options.setPassword(pwd.toCharArray());
+        }
+        factory.setConnectionOptions(options);
+        return factory;
+    }
+
+    @Bean(name = CHANNEL_NAME_OUT)
+    public MessageChannel egMqttOutboundChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean
+    @ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
+    public MessageHandler egMqttOutbound(MqttPahoClientFactory egMqttPahoClientFactory) {
+        String clientId = "service-eg-mqtt-out-" + System.currentTimeMillis();
+        MqttPahoMessageHandler handler = new MqttPahoMessageHandler(clientId, egMqttPahoClientFactory);
+        handler.setAsync(true);
+        handler.setDefaultTopic(DEFAULT_TOPIC);
+        return handler;
+    }
+
+    @MessagingGateway(defaultRequestChannel = CHANNEL_NAME_OUT)
+    public interface EgMqttPublishGateway {
+
+        void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
+    }
+}

+ 81 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttProperties.java

@@ -0,0 +1,81 @@
+package com.usky.eg.mqtt;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 远程升级 MQTT 下发(服务端发布,终端 {@code connectWithDevice} 订阅)。
+ * <p>
+ * 配置前缀 {@code eg.mqtt};{@link com.usky.eg.controller.web.EgDeviceAdbController#remoteUpgrade} 仅通过本配置连接 Broker 并发布。
+ */
+@ConfigurationProperties(prefix = "eg.mqtt")
+public class EgMqttProperties {
+
+    /** 例如 tcp://47.98.201.73:1883 */
+    private String url = "tcp://47.98.201.73:1883";
+
+    private String username = "usky";
+
+    private String password = "usky";
+
+    /**
+     * 发布主题模板,须包含占位符 {@code {deviceId}},与终端 {@code deviceId: getSerial()} 一致。
+     * 示例:usky/device/{deviceId}/command
+     */
+    private String commandTopicTemplate = "usky/device/{deviceId}/command";
+
+    /** 发布 QoS,0/1/2 */
+    private int qos = 1;
+
+    /**
+     * 下发 JSON 中 {@code type} 字段,终端可在 {@code onCommand} 里按类型分支。
+     */
+    private String commandType = "egRemoteUpgrade";
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getCommandTopicTemplate() {
+        return commandTopicTemplate;
+    }
+
+    public void setCommandTopicTemplate(String commandTopicTemplate) {
+        this.commandTopicTemplate = commandTopicTemplate;
+    }
+
+    public int getQos() {
+        return qos;
+    }
+
+    public void setQos(int qos) {
+        this.qos = qos;
+    }
+
+    public String getCommandType() {
+        return commandType;
+    }
+
+    public void setCommandType(String commandType) {
+        this.commandType = commandType;
+    }
+}

+ 20 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceAdbService.java

@@ -0,0 +1,20 @@
+package com.usky.eg.service;
+
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+
+/**
+ * 服务端调用官方 adb(部署机需安装 Android Platform Tools 且能访问设备网段)
+ */
+public interface EgDeviceAdbService {
+
+    EgDeviceAdbResultVO connect(String deviceIp, Integer devicePort);
+
+    EgDeviceAdbResultVO disconnect(String deviceIp, Integer devicePort);
+
+    EgDeviceAdbResultVO tcpip(String deviceIp, Integer devicePort, Integer listenPort);
+
+    /**
+     * exec-out screencap -p 原始 PNG 字节;调用方需保证已与设备建立 adb 会话(通常先 connect)
+     */
+    byte[] screenshot(String deviceIp, Integer devicePort);
+}

+ 19 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceRemoteUpgradeMqttService.java

@@ -0,0 +1,19 @@
+package com.usky.eg.service;
+
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchItemVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeVO;
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+
+import java.util.List;
+
+/**
+ * 通过 MQTT 向终端下发远程升级参数(终端 {@code onCommand} 接收后自行下载安装)。
+ */
+public interface EgDeviceRemoteUpgradeMqttService {
+
+    EgDeviceAdbResultVO publishRemoteUpgrade(EgDeviceAdbRemoteUpgradeVO vo);
+
+    EgDeviceAdbRemoteUpgradeBatchResultVO publishRemoteUpgradeBatch(List<EgDeviceAdbRemoteUpgradeBatchItemVO> items,
+                                                                    String fileUrl, String launchPackage);
+}

+ 325 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceAdbServiceImpl.java

@@ -0,0 +1,325 @@
+package com.usky.eg.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.eg.adb.EgDeviceAdbProperties;
+import com.usky.eg.service.EgDeviceAdbService;
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 通过 {@link ProcessBuilder} 调用本机 PATH 中的官方 adb。
+ */
+@Slf4j
+@Service
+public class EgDeviceAdbServiceImpl implements EgDeviceAdbService {
+
+    private static final int BUFFER = 16384;
+
+    /**
+     * Windows 下 adb 常输出系统本地编码(如 GBK),勿固定按 UTF-8 解码以免日志/接口里出现乱码。
+     */
+    private static final Charset ADB_CONSOLE_CHARSET = Charset.defaultCharset();
+
+    private static final String NETWORK_TIMEOUT_HINT =
+            " 【排查】运行本服务的机器须与设备网络互通并能访问目标 TCP 端口(常见 5555);"
+                    + "检查防火墙/安全组/跨网段路由;确认手机「无线调试」已开启且端口与请求一致;"
+                    + "Android 11+ 有时需先用系统界面配对,端口可能不是 5555;"
+                    + "错误 10060 表示连接超时,多为网络不可达或端口未监听。";
+
+    @Autowired
+    private EgDeviceAdbProperties properties;
+
+    @Override
+    public EgDeviceAdbResultVO connect(String deviceIp, Integer devicePort) {
+        validateEndpoint(deviceIp, devicePort);
+        String endpoint = toEndpoint(deviceIp, devicePort);
+        try {
+            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "connect", endpoint));
+            return EgDeviceAdbResultVO.ok(out);
+        } catch (Exception e) {
+            log.warn("adb connect failed: {}", endpoint, e);
+            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+        }
+    }
+
+    @Override
+    public EgDeviceAdbResultVO disconnect(String deviceIp, Integer devicePort) {
+        validateEndpoint(deviceIp, devicePort);
+        String endpoint = toEndpoint(deviceIp, devicePort);
+        try {
+            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "disconnect", endpoint));
+            return EgDeviceAdbResultVO.ok(out);
+        } catch (Exception e) {
+            log.warn("adb disconnect failed: {}", endpoint, e);
+            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+        }
+    }
+
+    @Override
+    public EgDeviceAdbResultVO tcpip(String deviceIp, Integer devicePort, Integer listenPort) {
+        validateEndpoint(deviceIp, devicePort);
+        if (listenPort == null || listenPort < 1 || listenPort > 65535) {
+            throw new BusinessException("port 无效(tcpip 监听端口)");
+        }
+        String serial = toSerial(deviceIp, devicePort);
+        EgDeviceAdbResultVO first = runTcpipOnce(serial, listenPort);
+        if (EgDeviceAdbResultVO.STATUS_SUCCESS.equals(first.getStatus())) {
+            return first;
+        }
+        if (!properties.isTcpipRetryAfterConnect()) {
+            return first;
+        }
+        log.info("adb tcpip 首次失败,尝试 connect 后重试: {}", serial);
+        EgDeviceAdbResultVO c = connect(deviceIp, devicePort);
+        if (!EgDeviceAdbResultVO.STATUS_SUCCESS.equals(c.getStatus())) {
+            return EgDeviceAdbResultVO.fail("tcpip 失败且自动 connect 未成功: " + first.getDetail() + " | connect: " + c.getDetail());
+        }
+        return runTcpipOnce(serial, listenPort);
+    }
+
+    private EgDeviceAdbResultVO runTcpipOnce(String serial, int listenPort) {
+        try {
+            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "-s", serial, "tcpip", String.valueOf(listenPort)));
+            return EgDeviceAdbResultVO.ok(out);
+        } catch (Exception e) {
+            log.warn("adb tcpip failed: {} {}", serial, listenPort, e);
+            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+        }
+    }
+
+    @Override
+    public byte[] screenshot(String deviceIp, Integer devicePort) {
+        validateEndpoint(deviceIp, devicePort);
+        String serial = toSerial(deviceIp, devicePort);
+        if (properties.isScreenshotConnectFirst()) {
+            try {
+                ensureAdbDeviceOnline(deviceIp, devicePort);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new BusinessException("设备未就绪,无法截屏: " + e.getMessage());
+            } catch (IOException e) {
+                throw new BusinessException("设备未就绪,无法截屏: " + e.getMessage());
+            }
+        }
+        List<String> cmd = Arrays.asList(
+                properties.getExecutable(),
+                "-s", serial,
+                "exec-out", "screencap", "-p");
+        try {
+            byte[] png = runAdbBinaryStdout(cmd, properties.getScreenshotTimeoutSeconds());
+            checkPng(png);
+            return png;
+        } catch (Exception e) {
+            log.warn("adb screencap failed: {}", serial, e);
+            throw new BusinessException("截屏失败: " + e.getMessage()
+                    + "(请确认设备已开无线调试、端口正确,或先调用 /egDevice/adb/connect)");
+        }
+    }
+
+    /**
+     * 让本机 adb 真正识别该 serial:执行 connect、校验 {@code adb devices} 为 {@code device} 状态。
+     * 若仅“假装 connect”或忽略失败,会继续出现 {@code device 'ip:port' not found}。
+     */
+    private void ensureAdbDeviceOnline(String deviceIp, int devicePort) throws IOException, InterruptedException {
+        String serial = toSerial(deviceIp, devicePort);
+        String endpoint = toEndpoint(deviceIp, devicePort);
+
+        String connectOut = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                Arrays.asList(properties.getExecutable(), "connect", endpoint));
+        if (connectOutputIndicatesFailure(connectOut)) {
+            throw new IOException(withTimeoutHint("adb connect 输出表明失败: " + connectOut));
+        }
+
+        if (waitUntilDeviceOnline(serial, 3, 400)) {
+            Thread.sleep(200);
+            return;
+        }
+
+        log.warn("首次 connect 后 devices 仍未就绪,重试 connect: {}", endpoint);
+        connectOut = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                Arrays.asList(properties.getExecutable(), "connect", endpoint));
+        if (connectOutputIndicatesFailure(connectOut)) {
+            throw new IOException(withTimeoutHint("adb connect(重试)失败: " + connectOut));
+        }
+        if (!waitUntilDeviceOnline(serial, 5, 500)) {
+            String devicesList = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "devices", "-l"));
+            throw new IOException(withTimeoutHint("adb devices 中无可用设备 " + serial
+                    + "。connect 输出: " + connectOut + ";当前列表:\n" + devicesList));
+        }
+        Thread.sleep(200);
+    }
+
+    private boolean waitUntilDeviceOnline(String serial, int attempts, long gapMs) throws IOException, InterruptedException {
+        for (int i = 0; i < attempts; i++) {
+            if (i > 0) {
+                Thread.sleep(gapMs);
+            }
+            String list = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "devices"));
+            String state = findSerialState(list, serial);
+            if ("device".equals(state)) {
+                return true;
+            }
+            if ("unauthorized".equals(state)) {
+                throw new IOException("设备已发现但未授权调试,请在设备上点「允许 USB 调试」: " + serial);
+            }
+            if ("offline".equals(state)) {
+                log.warn("设备当前为 offline: {}", serial);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 解析 {@code adb devices},返回 serial 对应第二列状态;未找到返回 null
+     */
+    private static String findSerialState(String devicesOutput, String serial) {
+        if (devicesOutput == null) {
+            return null;
+        }
+        for (String raw : devicesOutput.split("\\r?\\n")) {
+            String line = raw.trim();
+            if (line.isEmpty() || line.startsWith("List of devices")) {
+                continue;
+            }
+            String[] parts = line.split("\\s+");
+            if (parts.length >= 2 && serial.equals(parts[0])) {
+                return parts[1];
+            }
+        }
+        return null;
+    }
+
+    private static boolean connectOutputIndicatesFailure(String connectOut) {
+        if (connectOut == null || connectOut.isEmpty()) {
+            return false;
+        }
+        String s = connectOut.toLowerCase();
+        return s.contains("cannot connect")
+                || s.contains("failed to connect")
+                || s.contains("unable to connect")
+                || s.contains("connection refused")
+                || s.contains("10061")
+                || s.contains("10060")
+                || s.contains("timed out")
+                || connectOut.contains("连接尝试失败")
+                || connectOut.contains("没有正确答复");
+    }
+
+    private static String withTimeoutHint(String message) {
+        if (message == null) {
+            return NETWORK_TIMEOUT_HINT.trim();
+        }
+        String m = message;
+        String lower = m.toLowerCase();
+        if (lower.contains("10060") || lower.contains("10061") || lower.contains("cannot connect")
+                || lower.contains("timed out") || m.contains("连接尝试失败") || m.contains("没有正确答复")) {
+            return m + NETWORK_TIMEOUT_HINT;
+        }
+        return m;
+    }
+
+    /**
+     * adb 文本类输出(stdout/stderr)按 JVM 默认字符集解码
+     */
+    private static String decodeAdbConsole(byte[] raw) {
+        if (raw == null || raw.length == 0) {
+            return "";
+        }
+        return new String(raw, ADB_CONSOLE_CHARSET).trim();
+    }
+
+    private static void validateEndpoint(String deviceIp, Integer devicePort) {
+        if (StringUtils.isBlank(deviceIp)) {
+            throw new BusinessException("deviceIp 不能为空");
+        }
+        if (devicePort == null || devicePort < 1 || devicePort > 65535) {
+            throw new BusinessException("devicePort 无效");
+        }
+    }
+
+    private static String toEndpoint(String deviceIp, int devicePort) {
+        return deviceIp.trim() + ":" + devicePort;
+    }
+
+    private static String toSerial(String deviceIp, int devicePort) {
+        return toEndpoint(deviceIp, devicePort);
+    }
+
+    /**
+     * 合并 stdout/stderr,非 0 退出码抛出异常(文本类命令)
+     */
+    private String runAdbMergedOutput(long timeoutSeconds, List<String> command) throws IOException, InterruptedException {
+        ProcessBuilder pb = new ProcessBuilder(command);
+        pb.redirectErrorStream(true);
+        Process p = pb.start();
+        byte[] raw = readAll(p.getInputStream());
+        String text = decodeAdbConsole(raw);
+        boolean finished = p.waitFor(timeoutSeconds, TimeUnit.SECONDS);
+        if (!finished) {
+            p.destroyForcibly();
+            throw new IOException("adb 执行超时(" + timeoutSeconds + "s)");
+        }
+        int code = p.exitValue();
+        if (code != 0) {
+            throw new IOException(withTimeoutHint("adb 退出码 " + code + (text.isEmpty() ? "" : (": " + text))));
+        }
+        return text.isEmpty() ? "OK" : text;
+    }
+
+    private byte[] runAdbBinaryStdout(List<String> command, long timeoutSeconds) throws IOException, InterruptedException {
+        ProcessBuilder pb = new ProcessBuilder(command);
+        pb.redirectErrorStream(false);
+        Process p = pb.start();
+        byte[] out = readAll(p.getInputStream());
+        byte[] err = readAll(p.getErrorStream());
+        boolean finished = p.waitFor(timeoutSeconds, TimeUnit.SECONDS);
+        if (!finished) {
+            p.destroyForcibly();
+            throw new IOException("adb 执行超时(" + timeoutSeconds + "s)");
+        }
+        int code = p.exitValue();
+        String errStr = err.length == 0 ? "" : decodeAdbConsole(err);
+        if (code != 0) {
+            throw new IOException(withTimeoutHint("adb 退出码 " + code + (errStr.isEmpty() ? "" : (": " + errStr))));
+        }
+        if (out.length == 0 && !errStr.isEmpty()) {
+            throw new IOException(withTimeoutHint(errStr));
+        }
+        return out;
+    }
+
+    private static byte[] readAll(InputStream in) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        byte[] buf = new byte[BUFFER];
+        int n;
+        while ((n = in.read(buf)) != -1) {
+            bos.write(buf, 0, n);
+        }
+        return bos.toByteArray();
+    }
+
+    private static void checkPng(byte[] data) {
+        if (data == null || data.length < 24) {
+            throw new BusinessException("截屏数据无效(过小)");
+        }
+        if (data[0] != (byte) 0x89 || data[1] != 0x50 || data[2] != 0x4E || data[3] != 0x47) {
+            throw new BusinessException("截屏数据不是有效的 PNG");
+        }
+    }
+}

+ 173 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceRemoteUpgradeMqttServiceImpl.java

@@ -0,0 +1,173 @@
+package com.usky.eg.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.eg.mqtt.EgMqttOutboundConfig.EgMqttPublishGateway;
+import com.usky.eg.mqtt.EgMqttProperties;
+import com.usky.eg.service.EgDeviceRemoteUpgradeMqttService;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchItemVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeItemResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeVO;
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@Service
+public class EgDeviceRemoteUpgradeMqttServiceImpl implements EgDeviceRemoteUpgradeMqttService {
+
+    @Autowired
+    private EgMqttPublishGateway mqttPublishGateway;
+
+    @Autowired
+    private EgMqttProperties mqttProperties;
+
+    @Override
+    public EgDeviceAdbResultVO publishRemoteUpgrade(EgDeviceAdbRemoteUpgradeVO vo) {
+        if (vo == null) {
+            return EgDeviceAdbResultVO.fail("请求体为空");
+        }
+        if (StringUtils.isBlank(vo.getFileUrl())) {
+            throw new BusinessException("fileUrl 不能为空");
+        }
+        String urlTrim = vo.getFileUrl().trim();
+        if (!urlTrim.startsWith("http://") && !urlTrim.startsWith("https://")) {
+            throw new BusinessException("fileUrl 须为 http 或 https 地址");
+        }
+        String deviceId = resolveDeviceId(vo.getDeviceId(), vo.getDeviceCode());
+        if (StringUtils.isBlank(deviceId)) {
+            throw new BusinessException("MQTT 下发须指定 deviceId 或 deviceCode(与终端 connectWithDevice.deviceId 一致)");
+        }
+        String topic = buildTopic(deviceId);
+        String cmdId = UUID.randomUUID().toString();
+        String payload = buildPayload(cmdId, urlTrim, vo.getLaunchPackage(),
+                vo.getDeviceCode(), vo.getDeviceIp(), vo.getDevicePort());
+        return publishOne(topic, payload, cmdId);
+    }
+
+    @Override
+    public EgDeviceAdbRemoteUpgradeBatchResultVO publishRemoteUpgradeBatch(List<EgDeviceAdbRemoteUpgradeBatchItemVO> items,
+                                                                           String fileUrl, String launchPackage) {
+        if (items == null || items.isEmpty()) {
+            throw new BusinessException("items 不能为空");
+        }
+        if (StringUtils.isBlank(fileUrl)) {
+            throw new BusinessException("fileUrl 不能为空");
+        }
+        String urlTrim = fileUrl.trim();
+        if (!urlTrim.startsWith("http://") && !urlTrim.startsWith("https://")) {
+            throw new BusinessException("fileUrl 须为 http 或 https 地址");
+        }
+        List<EgDeviceAdbRemoteUpgradeItemResultVO> rows = new ArrayList<>(items.size());
+        int success = 0;
+        int fail = 0;
+        int index = 0;
+        for (EgDeviceAdbRemoteUpgradeBatchItemVO item : items) {
+            EgDeviceAdbResultVO r;
+            String mqttTopic = null;
+            String mqttCmdId = null;
+            if (item == null) {
+                r = EgDeviceAdbResultVO.fail("请求项为空");
+            } else {
+                String deviceId = resolveDeviceId(item.getDeviceId(), item.getDeviceCode());
+                if (StringUtils.isBlank(deviceId)) {
+                    r = EgDeviceAdbResultVO.fail("须指定 deviceId 或 deviceCode");
+                } else {
+                    try {
+                        String topic = buildTopic(deviceId);
+                        String cmdId = UUID.randomUUID().toString();
+                        String payload = buildPayload(cmdId, urlTrim, launchPackage,
+                                item.getDeviceCode(), item.getDeviceIp(), item.getDevicePort());
+                        r = publishOne(topic, payload, cmdId);
+                        mqttTopic = topic;
+                        mqttCmdId = cmdId;
+                    } catch (BusinessException e) {
+                        r = EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : "参数错误");
+                    } catch (Exception e) {
+                        log.warn("mqtt remote upgrade publish failed index={} deviceId={}", index, deviceId, e);
+                        r = EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+                    }
+                }
+            }
+            if (EgDeviceAdbResultVO.STATUS_SUCCESS.equals(r.getStatus())) {
+                success++;
+            } else {
+                fail++;
+            }
+            String code = item != null ? item.getDeviceCode() : null;
+            String ip = item != null ? item.getDeviceIp() : null;
+            String port = item != null ? item.getDevicePort() : null;
+            String routeId = item != null ? resolveDeviceId(item.getDeviceId(), item.getDeviceCode()) : null;
+            rows.add(new EgDeviceAdbRemoteUpgradeItemResultVO(index++, code, ip, port, routeId, mqttTopic, mqttCmdId, r));
+        }
+        return new EgDeviceAdbRemoteUpgradeBatchResultVO(items.size(), success, fail, rows);
+    }
+
+    private EgDeviceAdbResultVO publishOne(String topic, String payload, String cmdId) {
+        try {
+            int qos = clampQos(mqttProperties.getQos());
+            mqttPublishGateway.sendToMqtt(topic, qos, payload);
+            log.info("mqtt remote upgrade published topic={} cmdId={}", topic, cmdId);
+            return EgDeviceAdbResultVO.ok("topic=" + topic + "; cmdId=" + cmdId);
+        } catch (Exception e) {
+            log.warn("mqtt publish failed topic={} cmdId={}", topic, cmdId, e);
+            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+        }
+    }
+
+    private String buildPayload(String cmdId, String fileUrl, String launchPackage,
+                                String deviceCode, String deviceIp, String devicePort) {
+        JSONObject o = new JSONObject();
+        o.put("id", cmdId);
+        o.put("type", mqttProperties.getCommandType() != null ? mqttProperties.getCommandType() : "egRemoteUpgrade");
+        o.put("fileUrl", fileUrl.trim());
+        if (StringUtils.isNotBlank(launchPackage)) {
+            o.put("launchPackage", launchPackage.trim());
+        }
+        if (StringUtils.isNotBlank(deviceCode)) {
+            o.put("deviceCode", deviceCode.trim());
+        }
+        if (StringUtils.isNotBlank(deviceIp)) {
+            o.put("deviceIp", deviceIp.trim());
+        }
+        if (StringUtils.isNotBlank(devicePort)) {
+            o.put("devicePort", devicePort.trim());
+        }
+        return o.toJSONString();
+    }
+
+    private static String resolveDeviceId(String deviceId, String deviceCode) {
+        if (StringUtils.isNotBlank(deviceId)) {
+            return deviceId.trim();
+        }
+        if (StringUtils.isNotBlank(deviceCode)) {
+            return deviceCode.trim();
+        }
+        return null;
+    }
+
+    private String buildTopic(String deviceId) {
+        String template = mqttProperties.getCommandTopicTemplate();
+        if (StringUtils.isBlank(template) || !template.contains("{deviceId}")) {
+            throw new BusinessException("eg.mqtt.command-topic-template 须为非空且包含占位符 {deviceId}");
+        }
+        return template.replace("{deviceId}", deviceId);
+    }
+
+    private static int clampQos(int qos) {
+        if (qos < 0) {
+            return 0;
+        }
+        if (qos > 2) {
+            return 2;
+        }
+        return qos;
+    }
+}

+ 34 - 7
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.java

@@ -279,8 +279,8 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
             }
         }
 
-        // 若未传deviceCode但传了deviceIp,尝试从心跳表中获取deviceCode
-        if (StringUtils.isBlank(egDevice.getDeviceCode()) && StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+        // 若传了deviceIp,尝试从心跳表中获取deviceCode
+        if (StringUtils.isNotBlank(egDevice.getDeviceIp())) {
             LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatCheckWrapper = Wrappers.lambdaQuery();
             heartbeatCheckWrapper.eq(EgDeviceHeartbeat::getIpAddr, egDevice.getDeviceIp())
                     .eq(EgDeviceHeartbeat::getDeviceStatus, 1);
@@ -311,11 +311,6 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
 
     @Override
     public void attachUpdate(EgDevice egDevice){
-
-        EgDevice one = this.getById(egDevice.getId());
-        egDevice.setBindFace(one.getBindFace());
-
-
         if(checkDeviceNameUnique(egDevice)){
             throw new BusinessException("新增门禁设备'"+egDevice.getDeviceName()+"'失败,设备已存在");
         }
@@ -548,9 +543,18 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         page = sysPersonMapper.selectPage(page, personWrapper);
 
         List<SysPerson> records = page.getRecords();
+        List<Integer> pagePersonIds = new ArrayList<>();
+        for (SysPerson p : records) {
+            if (p != null && p.getId() != null) {
+                pagePersonIds.add(p.getId());
+            }
+        }
+        Map<Integer, Long> personUserIdMap = buildPersonUserIdMap(pagePersonIds);
         List<EgDeviceBindFacePersonVO> rows = new ArrayList<>(records.size());
         for (SysPerson p : records) {
             EgDeviceBindFacePersonVO vo = new EgDeviceBindFacePersonVO();
+            vo.setId(p.getId());
+            vo.setUserId(personUserIdMap.get(p.getId()));
             vo.setFullName(p.getFullName());
             vo.setAge(p.getAge());
             vo.setGender(p.getGender());
@@ -564,6 +568,29 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         return new CommonPage<>(rows, page.getTotal(), sz, cur);
     }
 
+    private Map<Integer, Long> buildPersonUserIdMap(List<Integer> personIds) {
+        Map<Integer, Long> map = new HashMap<>();
+        if (CollectionUtils.isEmpty(personIds)) {
+            return map;
+        }
+        List<Map<String, Object>> rows = sysPersonMapper.selectUserIdsByPersonIds(personIds);
+        if (CollectionUtils.isEmpty(rows)) {
+            return map;
+        }
+        for (Map<String, Object> row : rows) {
+            if (row == null) {
+                continue;
+            }
+            Object personIdObj = row.get("personId");
+            Object userIdObj = row.get("userId");
+            if (personIdObj == null || userIdObj == null) {
+                continue;
+            }
+            map.put(Integer.valueOf(personIdObj.toString()), Long.valueOf(userIdObj.toString()));
+        }
+        return map;
+    }
+
     @Override
     public void bindPerson(Integer deviceId, String personIds, Integer isLoginNotify) {
         if (deviceId == null) {

+ 3 - 1
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgRecordServiceImpl.java

@@ -61,7 +61,9 @@ public class EgRecordServiceImpl extends AbstractCrudService<EgRecordMapper, EgR
         }
         tenantId = one.getTenantId();
         egRecord.setEgDeviceId(one.getId());
-        egRecord.setCreateTime(LocalDateTime.now());
+        LocalDateTime now = LocalDateTime.now();
+        egRecord.setPassTime(now);
+        egRecord.setCreateTime(now);
         egRecord.setTenantId(tenantId);
         
         // 安全获取部门ID

+ 13 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbEndpointVO.java

@@ -0,0 +1,13 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+/**
+ * adb connect / disconnect 请求体
+ */
+@Data
+public class EgDeviceAdbEndpointVO {
+
+    private String deviceIp;
+    private Integer devicePort;
+}

+ 19 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeBatchItemVO.java

@@ -0,0 +1,19 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+/**
+ * 批量远程升级中的单台设备:设备标识与连接信息;MQTT 模式须含 {@link #deviceId} 或 {@link #deviceCode}。
+ * 安装包与启动参数与 {@link EgDeviceAdbRemoteUpgradeVO} 中 {@code fileUrl}、{@code launchPackage} 同级。
+ */
+@Data
+public class EgDeviceAdbRemoteUpgradeBatchItemVO {
+
+    /** MQTT 路由用,未传时使用 {@link #deviceCode} */
+    private String deviceId;
+
+    private String deviceCode;
+    private String deviceIp;
+    /** 无线调试端口,缺省由服务端按 5555 处理 */
+    private String devicePort;
+}

+ 21 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeBatchResultVO.java

@@ -0,0 +1,21 @@
+package com.usky.eg.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 批量远程升级汇总结果。
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class EgDeviceAdbRemoteUpgradeBatchResultVO {
+
+    private int total;
+    private int successCount;
+    private int failCount;
+    private List<EgDeviceAdbRemoteUpgradeItemResultVO> items;
+}

+ 34 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeItemResultVO.java

@@ -0,0 +1,34 @@
+package com.usky.eg.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 批量远程升级中单条设备的结果(与请求 {@code items} 下标对应)。
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class EgDeviceAdbRemoteUpgradeItemResultVO {
+
+    /** 与请求体 {@code items} 数组下标一致,从 0 开始 */
+    private int index;
+    private String deviceCode;
+    private String deviceIp;
+    private String devicePort;
+    /**
+     * MQTT 下发时:用于路由的主题中的设备标识(与请求 deviceId 或 deviceCode 解析结果一致)。
+     */
+    private String mqttRouteDeviceId;
+    /** MQTT 下发成功时填充 */
+    private String mqttTopic;
+    /** MQTT 下发成功时填充,与下行 JSON 中 {@code id} 一致 */
+    private String mqttCmdId;
+    private EgDeviceAdbResultVO result;
+
+    public EgDeviceAdbRemoteUpgradeItemResultVO(int index, String deviceCode, String deviceIp, String devicePort,
+                                                EgDeviceAdbResultVO result) {
+        this(index, deviceCode, deviceIp, devicePort, null, null, null, result);
+    }
+}

+ 36 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeVO.java

@@ -0,0 +1,36 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * MQTT 远程升级请求体:向 Broker 发布下行 JSON(终端 {@code onCommand} 接收后自行下载安装)。
+ * <p>
+ * {@code fileUrl} 必填(http/https)。须指定 {@code deviceId} 或与终端序列号一致的 {@code deviceCode},用于主题
+ * {@code eg.mqtt.command-topic-template} 中的占位符 {@code {deviceId}}。
+ * {@code deviceIp}、{@code devicePort} 可选,会一并写入下行 JSON 供终端展示或日志。
+ * <p>
+ * 批量:当 {@code items} 非空时,与本对象同级的 {@code fileUrl}、{@code launchPackage} 对所有设备共用;
+ * 忽略顶层 {@code deviceCode}、{@code deviceIp}、{@code devicePort}。{@code items} 每项见 {@link EgDeviceAdbRemoteUpgradeBatchItemVO}。
+ */
+@Data
+public class EgDeviceAdbRemoteUpgradeVO {
+
+    /** 非空且非空列表时启用批量模式 */
+    private List<EgDeviceAdbRemoteUpgradeBatchItemVO> items;
+
+    /**
+     * 与终端 {@code connectWithDevice({ deviceId: getSerial() })} 一致;
+     * 未传时与 {@link #deviceCode} 等价(二选一至少填一个)。
+     */
+    private String deviceId;
+
+    private String deviceCode;
+    private String deviceIp;
+    /** 可选,写入下行 JSON */
+    private String devicePort;
+    private String fileUrl;
+    /** 可选,写入下行 JSON,终端安装成功后自行拉起 */
+    private String launchPackage;
+}

+ 29 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbResultVO.java

@@ -0,0 +1,29 @@
+package com.usky.eg.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 官方 adb 文本类命令的统一返回(与前端约定 status)
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class EgDeviceAdbResultVO {
+
+    public static final String STATUS_SUCCESS = "SUCCESS";
+    public static final String STATUS_FAIL = "FAIL";
+
+    private String status;
+    /** adb 标准输出/合并日志摘要,便于排查 */
+    private String detail;
+
+    public static EgDeviceAdbResultVO ok(String detail) {
+        return new EgDeviceAdbResultVO(STATUS_SUCCESS, detail == null ? "" : detail.trim());
+    }
+
+    public static EgDeviceAdbResultVO fail(String detail) {
+        return new EgDeviceAdbResultVO(STATUS_FAIL, detail == null ? "" : detail.trim());
+    }
+}

+ 15 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbTcpipVO.java

@@ -0,0 +1,15 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+/**
+ * adb tcpip 请求体(对应 adb -s ip:devicePort tcpip &lt;port&gt;)
+ */
+@Data
+public class EgDeviceAdbTcpipVO {
+
+    private String deviceIp;
+    private Integer devicePort;
+    /** tcpip 命令中的监听端口参数,例如 5555 */
+    private Integer port;
+}

+ 4 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonVO.java

@@ -10,6 +10,10 @@ public class EgDeviceBindFacePersonVO implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
+    /** 人员主键ID(sys_person.id) */
+    private Integer id;
+    /** 用户ID(sys_user_person.user_id) */
+    private Long userId;
     private String fullName;
     private Integer age;
     private Integer gender;

+ 66 - 0
service-eg/service-eg-biz/src/main/resources/bootstrap.yml

@@ -23,3 +23,69 @@ spring:
         # 共享配置
         shared-configs:
           - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
+
+# SmartFace 人脸识别配置
+smartface:
+  # 启用人脸识别功能
+  enabled: true
+  # 是否延迟加载模型(true=首次使用时加载,false=启动时加载)
+  # 如果模型文件还未准备好,建议设置为true,避免启动失败
+  lazy-load: true
+  # 人脸检测配置
+  detection:
+    # 模型类型: MTCNN, RETINA_FACE, YOLOV5_FACE_320, YOLOV5_FACE_640
+    model-type: MTCNN
+    # 人脸检测模型路径(留空,将自动从classpath复制到临时目录)
+    model-path:
+    # 置信度阈值
+    confidence-threshold: 0.5
+    # NMS阈值
+    nms-threshold: 0.4
+    # 设备类型: CPU, GPU
+    device: CPU
+  # 人脸识别配置
+  recognition:
+    # 模型类型: INSIGHT_FACE_IRSE50_MODEL, INSIGHT_FACE_MOBILE_FACENET_MODEL, FACE_NET_MODEL
+    model-type: INSIGHT_FACE_IRSE50_MODEL
+    # 人脸识别模型路径(留空,将自动从classpath复制到临时目录)
+    model-path:
+    # 是否裁剪人脸
+    crop-face: true
+    # 是否启用人脸对齐
+    align: true
+    # 相似度阈值(0-1之间,推荐0.62)
+    similarity-threshold: 0.62
+    # 设备类型: CPU, GPU
+    device: CPU
+  # 向量数据库配置
+  vector-db:
+    # 数据库类型: SQLITE, MILVUS
+    type: SQLITE
+    # SQLite数据库文件路径
+    sqlite-path: ./data/face_db.sqlite
+    # Milvus配置(如果使用Milvus)
+    milvus:
+      host: 127.0.0.1
+      port: 19530
+      collection-name: face_collection
+
+# 服务端调用官方 adb(platform-tools 需在 PATH);详见 EgDeviceAdbController:/egDevice/adb/connect 等
+eg:
+  adb:
+    executable: adb
+    command-timeout-seconds: 45
+    screenshot-timeout-seconds: 60
+    tcpip-retry-after-connect: true
+    # 截屏前先 adb connect,避免服务端 adb 未登记设备 serial
+    screenshot-connect-first: true
+
+  # 远程升级仅 MQTT 下发(与终端 connectWithDevice 订阅主题一致)
+  mqtt:
+    url: tcp://47.98.201.73:1883
+    username: usky
+    password: usky
+    # 须含 {deviceId},与终端 deviceId(如 getSerial())一致
+    command-topic-template: usky/device/{deviceId}/command
+    qos: 1
+    # 下行 JSON 的 type 字段,终端 onCommand 可按此分支
+    command-type: egRemoteUpgrade

+ 0 - 24
service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceMapper.xml

@@ -25,12 +25,6 @@
         <result column="password" property="password" />
         <result column="work_status" property="workStatus" />
     </resultMap>
-    <insert id="insertMeetingFaceDevice" parameterType="integer">
-        insert into meeting_face_device (face_id,device_id) value (#{fid}, #{egDeviceId});
-    </insert>
-    <delete id="deleteMeetingFaceDevice" parameterType="integer">
-        delete from meeting_face_device where device_id = #{egDeviceId};
-    </delete>
     <select id="sysTenantId" resultType="integer">
         select id
         from sys_tenant
@@ -40,23 +34,5 @@
             </if>
         </where>
     </select>
-    <select id="getMeetingFaceData" resultType="integer">
-        select mf.fid as fid
-        from meeting_face mf
-        <where>
-            <if test="userId != null">
-                and mf.user_id = #{userId}
-            </if>
-        </where>
-    </select>
-    <select id="getMeetingFaceDeviceList" resultType="integer">
-        select mfd.device_id as deviceFid
-        from meeting_face_device mfd
-        <where>
-            <if test="fid != null">
-                and mfd.face_id = #{fid}
-            </if>
-        </where>
-    </select>
 
 </mapper>

+ 0 - 11
service-eg/service-eg-biz/src/main/resources/mapper/eg/MeetingFaceDeviceMapper.xml

@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.usky.eg.mapper.MeetingFaceDeviceMapper">
-
-    <!-- 通用查询映射结果 -->
-    <resultMap id="BaseResultMap" type="com.usky.eg.domain.MeetingFaceDevice">
-        <id column="face_id" property="faceId" />
-        <result column="device_id" property="deviceId" />
-    </resultMap>
-
-</mapper>

+ 0 - 21
service-eg/service-eg-biz/src/main/resources/mapper/eg/MeetingFaceMapper.xml

@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.usky.eg.mapper.MeetingFaceMapper">
-
-    <!-- 通用查询映射结果 -->
-    <resultMap id="BaseResultMap" type="com.usky.eg.domain.MeetingFace">
-        <id column="fid" property="fid" />
-        <result column="face_base" property="faceBase" />
-        <result column="create_time" property="createTime" />
-        <result column="vef_num" property="vefNum" />
-        <result column="face_name" property="faceName" />
-        <result column="remark" property="remark" />
-        <result column="face_status" property="faceStatus" />
-        <result column="card_num" property="cardNum" />
-        <result column="bind_device" property="bindDevice" />
-        <result column="dept_id" property="deptId" />
-        <result column="tenant_id" property="tenantId" />
-        <result column="user_id" property="userId" />
-    </resultMap>
-
-</mapper>

+ 13 - 0
service-eg/service-eg-biz/src/main/resources/mapper/eg/SysPersonMapper.xml

@@ -29,4 +29,17 @@
         <result column="card_num" property="cardNum"/>
     </resultMap>
 
+    <select id="selectUserIdsByPersonIds" resultType="java.util.HashMap">
+        select person_id as personId, user_id as userId
+        from sys_user_person
+        <where>
+            <if test="personIds != null and personIds.size() > 0">
+                and person_id in
+                <foreach collection="personIds" item="id" open="(" separator="," close=")">
+                    #{id}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
 </mapper>

+ 18 - 0
service-job/src/main/java/com/ruoyi/job/task/RyTask.java

@@ -151,4 +151,22 @@ public class RyTask {
         remoteVppTaskService.refreshSiteAvgCompletionRate();
 	}
 
+    // 运管平台 UN Poll 拉取事件/反馈
+    public void pollUn(Integer tenantId) {
+        System.out.println("租户:" + tenantId + " 运管平台 UN Poll 定时任务开始执行......");
+        remoteVppTaskService.pollUn(tenantId);
+    }
+
+    // 运管平台 UN Token 刷新与注册
+    public void registerUn(Integer tenantId) {
+        System.out.println("租户:" + tenantId + " 运管平台 UN 注册定时任务开始执行......");
+        remoteVppTaskService.registerUn(tenantId);
+    }
+
+    // 运管平台 UN 曲线数据报告(IntervalDataReport)
+    public void intervalDataReportUn(Integer tenantId) {
+        System.out.println("租户:" + tenantId + " 运管平台 UN 曲线数据报告定时任务开始执行......");
+        remoteVppTaskService.intervalDataReportUn(tenantId);
+    }
+
 }

+ 0 - 10
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/FaceLog.java

@@ -1,10 +0,0 @@
-package com.usky.meeting.annotation;
-
-import java.lang.annotation.*;
-
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-@Target(value = ElementType.METHOD)
-public @interface FaceLog {
-
-}

+ 0 - 50
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/aspect/FaceAspect.java

@@ -1,50 +0,0 @@
-package com.usky.meeting.annotation.aspect;
-
-
-
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.meeting.service.MeetingFaceVefLogService;
-import com.usky.meeting.service.vo.FaceResultVO;
-import com.usky.meeting.service.vo.MeetingFaceVO;
-import org.aspectj.lang.JoinPoint;
-import org.aspectj.lang.annotation.AfterReturning;
-import org.aspectj.lang.annotation.Aspect;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import java.time.LocalDateTime;
-import java.util.Objects;
-
-/**
- * @author tanyongpeng
- * <p>方法执行后,添加日志</p>
- **/
-@Component
-@Aspect
-public class FaceAspect {
-
-    @Autowired
-    private MeetingFaceVefLogService faceVefLogService;
-
-    @AfterReturning(value = "@annotation(com.usky.meeting.annotation.FaceLog)",returning = "res")
-    public void addLog(JoinPoint joinPoint ,Object res){
-        MeetingFaceVO result = (MeetingFaceVO)res;
-        MeetingFaceVefLog faceVefLog = new MeetingFaceVefLog();
-        faceVefLog.setVefTime(LocalDateTime.now());
-        faceVefLog.setVefCode(result.getCode());
-        faceVefLog.setVefMsg(result.getMsg());
-        faceVefLog.setLoginName(result.getFaceName());
-        faceVefLog.setTenantId(result.getTenantId());
-        if(Objects.nonNull(result.getFaceBase())){
-            faceVefLog.setFaceBase(result.getFaceBase());
-            faceVefLog.setVefType(1);
-        }
-        if(Objects.nonNull(result.getCardNum())){
-            faceVefLog.setCardNum(result.getCardNum());
-            faceVefLog.setVefType(2);
-        }
-        faceVefLogService.save(faceVefLog);
-    }
-
-
-}

+ 23 - 2
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingDeviceController.java

@@ -7,13 +7,14 @@ import com.usky.common.log.annotation.Log;
 import com.usky.common.log.enums.BusinessType;
 import com.usky.meeting.domain.MeetingDevice;
 import com.usky.meeting.service.MeetingDeviceService;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonQueryVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindPersonRequest;
 import com.usky.meeting.service.vo.MeetingDeviceRequestVO;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
-import org.springframework.stereotype.Controller;
-
 /**
  * <p>
  * 设备表 前端控制器
@@ -40,6 +41,15 @@ public class MeetingDeviceController {
         return ApiResult.success(meetingDeviceService.meetingDeviceList(requestVO));
     }
 
+    /**
+     * 分页查询设备已绑定人员(可通过 deviceCode 过滤)
+     */
+    @PostMapping("/bindFacePerson/page")
+    public ApiResult<CommonPage<MeetingDeviceBindFacePersonVO>> pageBindFacePersons(
+            @RequestBody MeetingDeviceBindFacePersonQueryVO queryVO) {
+        return ApiResult.success(meetingDeviceService.pageBindFacePersons(queryVO));
+    }
+
     /**
      * 新增
      * @param meetingDevice
@@ -76,5 +86,16 @@ public class MeetingDeviceController {
         return ApiResult.success();
     }
 
+    /**
+     * 设备人员授权(覆盖式)
+     * personIds 为空字符串时解除该设备全部授权
+     */
+    @Log(title = "会议设备人员授权", businessType = BusinessType.UPDATE)
+    @PostMapping("/bindPerson")
+    public ApiResult<Void> bindPerson(@RequestBody MeetingDeviceBindPersonRequest request) {
+        meetingDeviceService.bindPerson(request);
+        return ApiResult.success();
+    }
+
 }
 

+ 0 - 182
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceController.java

@@ -1,182 +0,0 @@
-package com.usky.meeting.controller.web;
-
-
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
-import com.baomidou.mybatisplus.core.toolkit.StringUtils;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.usky.common.core.bean.ApiResult;
-import com.usky.common.core.bean.CommonPage;
-import com.usky.common.core.exception.BusinessException;
-import com.usky.common.security.utils.SecurityUtils;
-import com.usky.meeting.annotation.FaceLog;
-import com.usky.meeting.domain.MeetingFace;
-import com.usky.meeting.domain.SysDept;
-import com.usky.meeting.mapper.MeetingFaceMapper;
-import com.usky.meeting.mapper.SysDeptMapper;
-import com.usky.meeting.repository.MeetingInfoRepository;
-import com.usky.meeting.service.MeetingFaceService;
-import com.usky.meeting.service.vo.DmpDeviceInfoVO;
-import com.usky.meeting.service.vo.FaceResultVO;
-import com.usky.meeting.service.vo.MeetingFaceRequestVO;
-import com.usky.meeting.service.vo.MeetingFaceVO;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.models.auth.In;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.web.bind.annotation.*;
-
-import org.springframework.stereotype.Controller;
-
-import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * <p>
- *  前端控制器
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-@RestController
-@RequestMapping("/meetingFace")
-@Api("人脸验证接口")
-@Slf4j
-public class MeetingFaceController {
-    @Autowired
-    MeetingFaceService faceService;
-    @Autowired
-    MeetingInfoRepository meetingInfoRepository;
-    @Autowired
-    private MeetingFaceMapper meetingFaceMapper;
-    @Autowired
-    private SysDeptMapper sysDeptMapper;
-
-    @PostMapping("/vef")
-    @ApiOperation(value="人脸验证", notes="根据传入的base64编码和数据的base64编码进行对比")
-    @FaceLog
-    public MeetingFaceVO faceVef(@RequestBody MeetingFaceRequestVO requestVO){
-        return faceService.vef(requestVO);
-    }
-
-    @GetMapping("/faceList")
-    @ApiOperation(value = "人脸列表",notes = "查询所有的人脸信息")
-    public ApiResult<CommonPage<MeetingFace>> faceList(
-            @RequestParam Integer current,
-            @RequestParam Integer size,
-            @RequestParam(required = false) String faceName,
-            @RequestParam(required = false) String faceStatus,
-            @RequestParam(required = false) Integer fid,
-            @RequestParam(required = false) Long userId,
-            @RequestParam(required = false) Integer deptId
-    ){
-        IPage<MeetingFace> page = faceService.page(new Page(current, size), new QueryWrapper<MeetingFace>()
-                .like(StrUtil.isNotBlank(faceName),"face_name",faceName)
-                .like(StrUtil.isNotBlank(faceStatus),"face_status",faceStatus)
-                .eq(fid != null,"fid",fid)
-                .eq(userId != null,"user_id",userId)
-                .eq(deptId != null,"dept_id",deptId)
-                .eq("tenant_id", SecurityUtils.getTenantId()));
-        if(page.getRecords().size() > 0){
-//            for (int i = 0; i < page.getRecords().size(); i++) {
-//                if(Objects.nonNull(page.getRecords().get(i).getBindDevice()) || StringUtils.isNotBlank(page.getRecords().get(i).getBindDevice())){
-//                    String[] deviceIdStr = page.getRecords().get(i).getBindDevice().split(",");
-//                    Integer[] deviceIds = Arrays.stream(deviceIdStr).map(Integer::parseInt).toArray(Integer[]::new);
-//                    List<DmpDeviceInfoVO> list = meetingFaceMapper.getDeviceInfo(Arrays.asList(deviceIds));
-//                    page.getRecords().get(i).setDeviceInfos(list);
-//
-//                }
-//            }
-
-            QueryWrapper<SysDept> queryWrapper = Wrappers.query();
-            queryWrapper.eq("del_flag",0).eq("tenant_id",SecurityUtils.getTenantId());
-            List<SysDept> sysDeptList = sysDeptMapper.selectList(queryWrapper);
-            if(sysDeptList.size() > 0){
-                for (int i = 0; i < page.getRecords().size(); i++) {
-                    Integer deptId1 = page.getRecords().get(i).getDeptId();
-                    for (int j = 0; j < sysDeptList.size(); j++) {
-                        if(deptId1.equals(Integer.valueOf(sysDeptList.get(j).getDeptId().toString()))){
-                            page.getRecords().get(i).setSysDept(sysDeptList.get(j));
-                            break;
-                        }
-                    }
-
-                }
-            }
-
-        }
-        return ApiResult.success(new CommonPage<>(page.getRecords(),page.getTotal(),size,current));
-    }
-
-
-    @GetMapping("/faceDelete/{fid}")
-    @ApiOperation(value = "删除人脸",notes = "根据id进行删除")
-    public FaceResultVO faceDelete(@PathVariable Integer fid){
-        faceService.removeById(fid);
-        meetingInfoRepository.delMeetingFaceDevice(fid);
-        return FaceResultVO.success("删除成功");
-    }
-
-    @GetMapping("/info/{fid}")
-    @ApiOperation(value = "查询方法",notes = "根据id查询人脸信息")
-    public FaceResultVO info(@PathVariable Integer fid){
-        return FaceResultVO.success(faceService.getById(fid));
-    }
-
-    @PostMapping("/save")
-    @ApiOperation(value = "添加")
-    public FaceResultVO save(@RequestBody MeetingFace face){
-        Long userId = face.getUserId();
-        LambdaQueryWrapper<MeetingFace> queryWrapper = Wrappers.lambdaQuery();
-        queryWrapper.eq(MeetingFace::getUserId,userId);
-        List<MeetingFace> list = faceService.list(queryWrapper);
-        if(CollectionUtils.isNotEmpty(list)){
-            throw new BusinessException("人员已绑定,请检查");
-        }
-
-        face.setVefNum(0);
-        face.setCreateTime(LocalDateTime.now());
-        face.setTenantId(SecurityUtils.getTenantId());
-        String[] devicefids = new String[0];
-        if(Objects.nonNull(face.getBindDevice()) || StringUtils.isNotBlank(face.getBindDevice())){
-            devicefids = face.getBindDevice().split(",");
-        }
-        faceService.save(face);
-        if(devicefids.length > 0){
-            for (int i = 0; i < devicefids.length; i++) {
-                meetingInfoRepository.insertMeetingFaceDevice(face.getFid(),Integer.parseInt(devicefids[i]));
-            }
-        }
-        return FaceResultVO.success("添加成功");
-    }
-
-    @PostMapping("/update")
-    @ApiOperation(value = "修改")
-    public FaceResultVO update(@RequestBody MeetingFace face){
-        faceService.updateById(face);
-        meetingInfoRepository.delMeetingFaceDevice(face.getFid());
-        String[] devicefids = new String[0];
-        if(Objects.nonNull(face.getBindDevice()) || StringUtils.isNotBlank(face.getBindDevice())){
-            devicefids = face.getBindDevice().split(",");
-        }
-        if(devicefids.length > 0){
-            for (int i = 0; i < devicefids.length; i++) {
-                meetingInfoRepository.insertMeetingFaceDevice(face.getFid(),Integer.parseInt(devicefids[i]));
-            }
-        }
-        return FaceResultVO.success("修改成功");
-    }
-}
-

+ 0 - 60
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceVefLogController.java

@@ -1,60 +0,0 @@
-package com.usky.meeting.controller.web;
-
-
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.usky.common.core.bean.ApiResult;
-import com.usky.common.core.bean.CommonPage;
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.meeting.service.MeetingFaceVefLogService;
-import com.usky.meeting.service.vo.FaceResultVO;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-/**
- * <p>
- *  前端控制器
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-@RestController
-@RequestMapping("/meetingFaceVefLog")
-@Api("人脸日志接口")
-@Slf4j
-public class MeetingFaceVefLogController {
-
-    @Autowired
-    MeetingFaceVefLogService faceVefLogService;
-
-    @GetMapping("/list")
-    @ApiOperation(value = "日志接口",notes = "查询所有验证人脸日志信息")
-    public ApiResult<CommonPage<MeetingFaceVefLog>> faceList(
-            @RequestParam Integer current,
-            @RequestParam Integer size,
-            @RequestParam(required = false) String startTime,
-            @RequestParam(required = false) String endTime,
-            @RequestParam(required = false) String vefCode,
-            @RequestParam(required = false) String loginName
-    ){
-        IPage<MeetingFaceVefLog> page = faceVefLogService.page(new Page(current, size), new QueryWrapper<MeetingFaceVefLog>()
-                .like(StrUtil.isNotBlank(vefCode),"vef_code",vefCode)
-                .like(StrUtil.isNotBlank(loginName),"login_name",loginName)
-                .between(StrUtil.isNotBlank(startTime) && StrUtil.isNotBlank(endTime),"vef_time",startTime,endTime)
-                .orderByDesc("vef_time"));
-        return ApiResult.success(new CommonPage<>(page.getRecords(),page.getTotal(),size,current));
-    }
-
-}
-

+ 6 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevice.java

@@ -166,4 +166,10 @@ public class MeetingDevice implements Serializable {
      * 设备方向 1横屏 2竖屏
      */
     private Integer deviceDirection;
+
+    /**
+     * 绑定人员ID(逗号分隔,来自 meeting_device_person_bind)
+     */
+    @TableField(exist = false)
+    private String bindPerson;
 }

+ 25 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevicePersonBind.java

@@ -0,0 +1,25 @@
+package com.usky.meeting.domain;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * 会议设备人员绑定表 meeting_device_person_bind
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class MeetingDevicePersonBind implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 设备主键ID(meeting_device.device_uuid) */
+    private Long deviceUuid;
+
+    /** 人员ID */
+    private Integer personId;
+
+    /** 是否打开登录通知(1 是 0 否 默认0) */
+    private Integer isLoginNotify;
+}

+ 0 - 75
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingFaceVefLog.java

@@ -1,75 +0,0 @@
-package com.usky.meeting.domain;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import java.time.LocalDateTime;
-import java.io.Serializable;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * <p>
- * 
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-public class MeetingFaceVefLog implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 主键
-     */
-    @TableId(value = "lid", type = IdType.AUTO)
-    private Integer lid;
-
-    /**
-     * 验证时间
-     */
-    private LocalDateTime vefTime;
-
-    /**
-     * 返回code
-     */
-    private Integer vefCode;
-
-    /**
-     * 返回的消息
-     */
-    private String vefMsg;
-
-    /**
-     * 验证人
-     */
-    private String loginName;
-
-    /**
-     * 组织机构ID
-     */
-    private Integer deptId;
-
-    /**
-     * 租户ID
-     */
-    private Integer tenantId;
-
-    /**
-     * 图片数据 base_64编码
-     */
-    private String faceBase;
-
-    /**
-     * 卡号
-     */
-    private String cardNum;
-
-    /**
-     * 验证类型(1、人脸 2、卡号)
-     */
-    private Integer vefType;
-
-}

+ 48 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/SysPerson.java

@@ -0,0 +1,48 @@
+package com.usky.meeting.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 人员信息表 sys_person
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sys_person")
+public class SysPerson implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    private String fullName;
+    private Integer age;
+    private Integer gender;
+    private String address;
+    private Integer educationDegree;
+    private String idNumber;
+    private String linkPhone;
+    private Long postId;
+    private Long deptId;
+    private LocalDateTime entryTime;
+    private String certificateUrl1;
+    private String certificateUrl2;
+    private String certificateUrl3;
+    private String creator;
+    private LocalDateTime createTime;
+    private String updatePerson;
+    private LocalDateTime updateTime;
+    private String faceBase;
+    private Integer vefNum;
+    private String faceName;
+    private String remark;
+    private Byte faceStatus;
+    private String cardNum;
+}

+ 29 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingDevicePersonBindMapper.java

@@ -0,0 +1,29 @@
+package com.usky.meeting.mapper;
+
+import com.usky.meeting.domain.MeetingDevicePersonBind;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 会议设备人员绑定 Mapper
+ */
+@Repository
+public interface MeetingDevicePersonBindMapper {
+
+    /**
+     * 根据设备UUID删除绑定关系
+     */
+    void deleteByDeviceUuid(@Param("deviceUuid") Long deviceUuid);
+
+    /**
+     * 根据多个设备UUID查询绑定关系
+     */
+    List<MeetingDevicePersonBind> selectByDeviceUuids(@Param("deviceUuids") List<Long> deviceUuids);
+
+    /**
+     * 新增绑定关系
+     */
+    void insert(MeetingDevicePersonBind bind);
+}

+ 0 - 6
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceMapper.java

@@ -2,13 +2,8 @@ package com.usky.meeting.mapper;
 
 import com.usky.meeting.domain.MeetingFace;
 import com.usky.common.mybatis.core.CrudMapper;
-import com.usky.meeting.service.vo.DmpDeviceInfoVO;
-import io.swagger.models.auth.In;
-import org.apache.ibatis.annotations.Param;
 import org.springframework.stereotype.Repository;
 
-import java.util.List;
-
 /**
  * <p>
  *  Mapper 接口
@@ -19,5 +14,4 @@ import java.util.List;
  */
 @Repository
 public interface MeetingFaceMapper extends CrudMapper<MeetingFace> {
-    List<DmpDeviceInfoVO> getDeviceInfo(@Param("deviceIdList") List<Integer> deviceIdList);
 }

+ 0 - 16
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceVefLogMapper.java

@@ -1,16 +0,0 @@
-package com.usky.meeting.mapper;
-
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.common.mybatis.core.CrudMapper;
-
-/**
- * <p>
- *  Mapper 接口
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-public interface MeetingFaceVefLogMapper extends CrudMapper<MeetingFaceVefLog> {
-
-}

+ 21 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/SysPersonMapper.java

@@ -0,0 +1,21 @@
+package com.usky.meeting.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.meeting.domain.SysPerson;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 人员信息表 Mapper
+ */
+@Repository
+public interface SysPersonMapper extends CrudMapper<SysPerson> {
+
+    /**
+     * 按人员ID批量查询关联用户ID(sys_user_person)
+     */
+    List<Map<String, Object>> selectUserIdsByPersonIds(@Param("personIds") List<Integer> personIds);
+}

+ 0 - 19
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/repository/MeetingInfoRepository.java

@@ -306,25 +306,6 @@ public interface MeetingInfoRepository extends JpaRepository<MeetingInfo, Long>,
     @Query(value = "select * from meeting_info where room_id = :roomId",nativeQuery = true)
     List<MeetingInfo> queryAllMeetingByRoomId(@Param("roomId") Long roomId);
 
-    /**
-     * 新增人脸设备关联表记录
-     * @param faceId  人脸主键ID
-     * @param deviceId  设备主键ID
-     */
-    @Transactional
-    @Modifying
-    @Query(value = "insert into meeting_face_device (face_id,device_id) values (:faceId,:deviceId)",nativeQuery = true)
-    void insertMeetingFaceDevice(@Param("faceId") Integer faceId, @Param("deviceId") Integer deviceId);
-
-    /**
-     * 删除人脸设备关联表记录
-     * @param faceId
-     */
-    @Transactional
-    @Modifying
-    @Query(value = "delete from meeting_face_device where face_id = :faceId",nativeQuery = true)
-    void delMeetingFaceDevice(@Param("faceId") Integer faceId);
-
 //
 //
 //    /**

+ 12 - 1
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingDeviceService.java

@@ -3,8 +3,10 @@ package com.usky.meeting.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.meeting.domain.MeetingDevice;
 import com.usky.common.mybatis.core.CrudService;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonQueryVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindPersonRequest;
 import com.usky.meeting.service.vo.MeetingDeviceRequestVO;
-import org.springframework.web.bind.annotation.RequestBody;
 
 /**
  * <p>
@@ -24,4 +26,13 @@ public interface MeetingDeviceService extends CrudService<MeetingDevice> {
 
     void remove(Long deviceId);
 
+    /**
+     * 设备人员授权(覆盖式):先清空该设备绑定,再按 personIds 重新绑定;空字符串表示解除全部授权
+     */
+    void bindPerson(MeetingDeviceBindPersonRequest request);
+
+    /**
+     * 分页查询设备已绑定人员(支持 deviceCode / 姓名 / 人脸状态)
+     */
+    CommonPage<MeetingDeviceBindFacePersonVO> pageBindFacePersons(MeetingDeviceBindFacePersonQueryVO queryVO);
 }

+ 0 - 16
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingFaceVefLogService.java

@@ -1,16 +0,0 @@
-package com.usky.meeting.service;
-
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.common.mybatis.core.CrudService;
-
-/**
- * <p>
- *  服务类
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-public interface MeetingFaceVefLogService extends CrudService<MeetingFaceVefLog> {
-
-}

+ 192 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingDeviceServiceImpl.java

@@ -19,6 +19,9 @@ import com.usky.meeting.repository.MeetingRoomRepository;
 import com.usky.meeting.service.MeetingDeviceService;
 import com.usky.common.mybatis.core.AbstractCrudService;
 import com.usky.meeting.service.MeetingRoomService;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonQueryVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindPersonRequest;
 import com.usky.meeting.service.vo.MeetingDeviceRequestVO;
 import net.bytebuddy.implementation.bytecode.Throw;
 import org.aspectj.weaver.patterns.ThisOrTargetAnnotationPointcut;
@@ -56,6 +59,10 @@ public class MeetingDeviceServiceImpl extends AbstractCrudService<MeetingDeviceM
     private MeetingDeviceHeartbeatMapper heartbeatMapper;
     @Autowired
     private MeetingTemplateMapper meetingTemplateMapper;
+    @Autowired
+    private MeetingDevicePersonBindMapper meetingDevicePersonBindMapper;
+    @Autowired
+    private SysPersonMapper sysPersonMapper;
 
     @Override
     public CommonPage<MeetingDevice> meetingDeviceList(MeetingDeviceRequestVO requestVO) {
@@ -174,11 +181,54 @@ public class MeetingDeviceServiceImpl extends AbstractCrudService<MeetingDeviceM
                     MeetingDevice::getTemplateId,
                     buildIdMap(meetingTemplateList, MeetingTemplate::getId),
                     MeetingDevice::setMeetingTemplate);
+
+            // 5. 填充绑定人员
+            fillBindPerson(page.getRecords());
         }
 
         return new CommonPage<>(page.getRecords(), page.getTotal(), size, current);
     }
 
+    /**
+     * 根据设备UUID集合查询 meeting_device_person_bind,将 person_id 按逗号拼接后写入 bindPerson
+     */
+    private void fillBindPerson(List<MeetingDevice> devices) {
+        if (CollectionUtils.isEmpty(devices)) {
+            return;
+        }
+        List<Long> deviceUuids = devices.stream()
+                .map(MeetingDevice::getDeviceUuid)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (deviceUuids.isEmpty()) {
+            return;
+        }
+        List<MeetingDevicePersonBind> bindList = meetingDevicePersonBindMapper.selectByDeviceUuids(deviceUuids);
+        Map<Long, List<String>> devicePersonMap = new HashMap<>();
+        if (!CollectionUtils.isEmpty(bindList)) {
+            for (MeetingDevicePersonBind bind : bindList) {
+                if (bind.getDeviceUuid() == null || bind.getPersonId() == null) {
+                    continue;
+                }
+                devicePersonMap
+                        .computeIfAbsent(bind.getDeviceUuid(), k -> new ArrayList<>())
+                        .add(String.valueOf(bind.getPersonId()));
+            }
+        }
+        for (MeetingDevice device : devices) {
+            if (device == null || device.getDeviceUuid() == null) {
+                continue;
+            }
+            List<String> personIds = devicePersonMap.get(device.getDeviceUuid());
+            if (personIds != null && !personIds.isEmpty()) {
+                device.setBindPerson(String.join(",", personIds));
+            } else {
+                device.setBindPerson("");
+            }
+        }
+    }
+
     /**
      * 构建ID到对象的Map,用于快速查找
      * @param dataList 待处理的列表
@@ -286,6 +336,148 @@ public class MeetingDeviceServiceImpl extends AbstractCrudService<MeetingDeviceM
         // remoteIotTaskService.deleteDeviceInfo(deviceId.toString());
     }
 
+    @Override
+    public void bindPerson(MeetingDeviceBindPersonRequest request) {
+        if (request == null || request.getDeviceUuid() == null) {
+            throw new BusinessException("设备UUID不能为空");
+        }
+        Long deviceUuid = request.getDeviceUuid();
+        MeetingDevice device = this.getById(deviceUuid);
+        Optional.ofNullable(device).orElseThrow(() -> new BusinessException("会议设备信息不存在"));
+
+        // 覆盖式:先清空原有绑定
+        meetingDevicePersonBindMapper.deleteByDeviceUuid(deviceUuid);
+
+        String personIds = request.getPersonIds();
+        // 空字符串视为解除全部授权
+        if (!StringUtils.isNotBlank(personIds)) {
+            return;
+        }
+
+        String[] personIdArr = personIds.split(",");
+        for (String personIdStr : personIdArr) {
+            if (!StringUtils.isNotBlank(personIdStr)) {
+                continue;
+            }
+            MeetingDevicePersonBind bind = new MeetingDevicePersonBind();
+            bind.setDeviceUuid(deviceUuid);
+            bind.setPersonId(Integer.parseInt(personIdStr.trim()));
+            bind.setIsLoginNotify(0);
+            meetingDevicePersonBindMapper.insert(bind);
+        }
+    }
+
+    @Override
+    public CommonPage<MeetingDeviceBindFacePersonVO> pageBindFacePersons(MeetingDeviceBindFacePersonQueryVO queryVO) {
+        Integer cur = queryVO.getCurrent();
+        Integer sz = queryVO.getSize();
+        IPage<SysPerson> page = new Page<>(cur, sz);
+
+        List<Long> deviceUuids = new ArrayList<>();
+        if (StringUtils.isNotBlank(queryVO.getDeviceCode())) {
+            MeetingDevice tempDevice = deviceMapper.selectOne(Wrappers.<MeetingDevice>lambdaQuery()
+                    .eq(MeetingDevice::getDeviceCode, queryVO.getDeviceCode()));
+            if (tempDevice == null) {
+                return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+            }
+            MeetingDevice device = deviceMapper.selectOne(Wrappers.<MeetingDevice>lambdaQuery()
+                    .eq(MeetingDevice::getDeviceCode, queryVO.getDeviceCode())
+                    .eq(MeetingDevice::getTenantId, tempDevice.getTenantId()));
+            if (device == null || device.getDeviceUuid() == null) {
+                return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+            }
+            deviceUuids.add(device.getDeviceUuid());
+        } else {
+            List<MeetingDevice> devices = deviceMapper.selectList(Wrappers.<MeetingDevice>lambdaQuery()
+                    .eq(MeetingDevice::getTenantId, SecurityUtils.getTenantId()));
+            if (!CollectionUtils.isEmpty(devices)) {
+                for (MeetingDevice d : devices) {
+                    if (d != null && d.getDeviceUuid() != null) {
+                        deviceUuids.add(d.getDeviceUuid());
+                    }
+                }
+            }
+        }
+
+        if (deviceUuids.isEmpty()) {
+            return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+        }
+
+        List<MeetingDevicePersonBind> bindList = meetingDevicePersonBindMapper.selectByDeviceUuids(deviceUuids);
+        LinkedHashSet<Integer> personIdSet = new LinkedHashSet<>();
+        if (!CollectionUtils.isEmpty(bindList)) {
+            for (MeetingDevicePersonBind b : bindList) {
+                if (b.getPersonId() != null) {
+                    personIdSet.add(b.getPersonId());
+                }
+            }
+        }
+        if (personIdSet.isEmpty()) {
+            return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+        }
+        List<Integer> personIds = new ArrayList<>(personIdSet);
+
+        LambdaQueryWrapper<SysPerson> personWrapper = Wrappers.lambdaQuery();
+        personWrapper.in(SysPerson::getId, personIds);
+        if (StringUtils.isNotBlank(queryVO.getFullName())) {
+            personWrapper.like(SysPerson::getFullName, queryVO.getFullName());
+        }
+        Integer faceStatusFilter = queryVO.getFaceStatus();
+        if (faceStatusFilter != null) {
+            personWrapper.eq(SysPerson::getFaceStatus, faceStatusFilter.byteValue());
+        }
+        personWrapper.orderByAsc(SysPerson::getId);
+        page = sysPersonMapper.selectPage(page, personWrapper);
+
+        List<SysPerson> records = page.getRecords();
+        List<Integer> pagePersonIds = new ArrayList<>();
+        for (SysPerson p : records) {
+            if (p != null && p.getId() != null) {
+                pagePersonIds.add(p.getId());
+            }
+        }
+        Map<Integer, Long> personUserIdMap = buildPersonUserIdMap(pagePersonIds);
+        List<MeetingDeviceBindFacePersonVO> rows = new ArrayList<>(records.size());
+        for (SysPerson p : records) {
+            MeetingDeviceBindFacePersonVO vo = new MeetingDeviceBindFacePersonVO();
+            vo.setId(p.getId());
+            vo.setUserId(personUserIdMap.get(p.getId()));
+            vo.setFullName(p.getFullName());
+            vo.setAge(p.getAge());
+            vo.setGender(p.getGender());
+            vo.setLinkPhone(p.getLinkPhone());
+            vo.setFaceBase(p.getFaceBase());
+            vo.setVefNum(p.getVefNum());
+            vo.setFaceStatus(p.getFaceStatus());
+            vo.setCardNum(p.getCardNum());
+            rows.add(vo);
+        }
+        return new CommonPage<>(rows, page.getTotal(), sz, cur);
+    }
+
+    private Map<Integer, Long> buildPersonUserIdMap(List<Integer> personIds) {
+        Map<Integer, Long> map = new HashMap<>();
+        if (CollectionUtils.isEmpty(personIds)) {
+            return map;
+        }
+        List<Map<String, Object>> rows = sysPersonMapper.selectUserIdsByPersonIds(personIds);
+        if (CollectionUtils.isEmpty(rows)) {
+            return map;
+        }
+        for (Map<String, Object> row : rows) {
+            if (row == null) {
+                continue;
+            }
+            Object personIdObj = row.get("personId");
+            Object userIdObj = row.get("userId");
+            if (personIdObj == null || userIdObj == null) {
+                continue;
+            }
+            map.put(Integer.valueOf(personIdObj.toString()), Long.valueOf(userIdObj.toString()));
+        }
+        return map;
+    }
+
     public boolean checkNameUnique(MeetingDevice meetingDevice) {
         Long deviceId = null == meetingDevice.getDeviceUuid() ? -1 : meetingDevice.getDeviceUuid();
         LambdaQueryWrapper<MeetingDevice> queryWrapper = Wrappers.lambdaQuery();

+ 0 - 20
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFaceVefLogServiceImpl.java

@@ -1,20 +0,0 @@
-package com.usky.meeting.service.impl;
-
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.meeting.mapper.MeetingFaceVefLogMapper;
-import com.usky.meeting.service.MeetingFaceVefLogService;
-import com.usky.common.mybatis.core.AbstractCrudService;
-import org.springframework.stereotype.Service;
-
-/**
- * <p>
- *  服务实现类
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-@Service
-public class MeetingFaceVefLogServiceImpl extends AbstractCrudService<MeetingFaceVefLogMapper, MeetingFaceVefLog> implements MeetingFaceVefLogService {
-
-}

+ 0 - 1
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFileServiceImpl.java

@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.security.utils.SecurityUtils;
-import com.usky.meeting.domain.MeetingFace;
 import com.usky.meeting.domain.MeetingFile;
 import com.usky.meeting.mapper.MeetingFileMapper;
 import com.usky.meeting.service.MeetingFileService;

+ 23 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonQueryVO.java

@@ -0,0 +1,23 @@
+package com.usky.meeting.service.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 会议设备绑定人员分页查询参数
+ */
+@Data
+public class MeetingDeviceBindFacePersonQueryVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer current;
+    private Integer size;
+    /** 设备编码,空则当前租户全部设备 */
+    private String deviceCode;
+    /** 姓名模糊,可空 */
+    private String fullName;
+    /** 人脸状态 0/1,可空 */
+    private Integer faceStatus;
+}

+ 27 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonVO.java

@@ -0,0 +1,27 @@
+package com.usky.meeting.service.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 会议设备绑定人员列表项
+ */
+@Data
+public class MeetingDeviceBindFacePersonVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 人员主键ID(sys_person.id) */
+    private Integer id;
+    /** 用户ID(sys_user_person.user_id) */
+    private Long userId;
+    private String fullName;
+    private Integer age;
+    private Integer gender;
+    private String linkPhone;
+    private String faceBase;
+    private Integer vefNum;
+    private Byte faceStatus;
+    private String cardNum;
+}

+ 18 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindPersonRequest.java

@@ -0,0 +1,18 @@
+package com.usky.meeting.service.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+/**
+ * 会议设备人员授权请求
+ */
+@Data
+public class MeetingDeviceBindPersonRequest {
+
+    /** 会议设备 UUID(meeting_device.device_uuid) */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long deviceUuid;
+
+    /** 人员 ID,逗号分隔;空字符串表示清空授权 */
+    private String personIds;
+}

+ 34 - 0
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingDevicePersonBindMapper.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.meeting.mapper.MeetingDevicePersonBindMapper">
+
+    <resultMap id="BaseResultMap" type="com.usky.meeting.domain.MeetingDevicePersonBind">
+        <result column="device_uuid" property="deviceUuid"/>
+        <result column="person_id" property="personId"/>
+        <result column="is_login_notify" property="isLoginNotify"/>
+    </resultMap>
+
+    <delete id="deleteByDeviceUuid">
+        delete from meeting_device_person_bind
+        where device_uuid = #{deviceUuid}
+    </delete>
+
+    <select id="selectByDeviceUuids" resultMap="BaseResultMap">
+        select device_uuid, person_id, is_login_notify
+        from meeting_device_person_bind
+        <where>
+            <if test="deviceUuids != null and deviceUuids.size() > 0">
+                and device_uuid in
+                <foreach collection="deviceUuids" item="id" open="(" separator="," close=")">
+                    #{id}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
+    <insert id="insert" parameterType="com.usky.meeting.domain.MeetingDevicePersonBind">
+        insert into meeting_device_person_bind (device_uuid, person_id, is_login_notify)
+        values (#{deviceUuid}, #{personId}, #{isLoginNotify})
+    </insert>
+
+</mapper>

+ 0 - 15
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceMapper.xml

@@ -17,20 +17,5 @@
         <result column="tenant_id" property="tenantId" />
         <result column="user_id" property="userId" />
     </resultMap>
-    <select id="getDeviceInfo" resultType="com.usky.meeting.service.vo.DmpDeviceInfoVO">
-        SELECT dd.id,dd.device_id,dd.device_name,dd.product_id,dd.tenant_id,dd.company_code,dd.install_address,dd.service_status,dd.product_code,dd.device_uuid
-            FROM dmp_device dd
-        <where>
-            <if test="deviceIdList != null and deviceIdList.size() > 0">
-                AND dd.id in
-                <foreach item="item" collection="deviceIdList" open="(" separator="," close=")">
-                    #{item}
-                </foreach>
-            </if>
-            <if test="1 == 1">
-                and dd.delete_flag = 0
-            </if>
-        </where>
-    </select>
 
 </mapper>

+ 0 - 16
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceVefLogMapper.xml

@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.usky.meeting.mapper.MeetingFaceVefLogMapper">
-
-    <!-- 通用查询映射结果 -->
-    <resultMap id="BaseResultMap" type="com.usky.meeting.domain.MeetingFaceVefLog">
-        <id column="lid" property="lid" />
-        <result column="vef_time" property="vefTime" />
-        <result column="vef_code" property="vefCode" />
-        <result column="vef_msg" property="vefMsg" />
-        <result column="login_name" property="loginName" />
-        <result column="dept_id" property="deptId" />
-        <result column="tenant_id" property="tenantId" />
-    </resultMap>
-
-</mapper>

+ 18 - 0
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/SysPersonMapper.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.meeting.mapper.SysPersonMapper">
+
+    <select id="selectUserIdsByPersonIds" resultType="java.util.HashMap">
+        select person_id as personId, user_id as userId
+        from sys_user_person
+        <where>
+            <if test="personIds != null and personIds.size() > 0">
+                and person_id in
+                <foreach collection="personIds" item="id" open="(" separator="," close=")">
+                    #{id}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
+</mapper>

+ 10 - 0
service-vpp/service-vpp-api/src/main/java/com/usky/vpp/RemoteVppTaskService.java

@@ -3,10 +3,20 @@ package com.usky.vpp;
 import com.usky.vpp.factory.RemoteVppTaskFactory;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 
 @FeignClient(contextId = "remoteVppTaskService", value = "service-vpp", fallbackFactory = RemoteVppTaskFactory.class)
 public interface RemoteVppTaskService {
 
     @GetMapping("/refreshSiteAvgCompletionRate")
     void refreshSiteAvgCompletionRate();
+
+    @GetMapping("/pollUn")
+    void pollUn(@RequestParam(value = "tenantId") Integer tenantId);
+
+    @GetMapping("/registerUn")
+    void registerUn(@RequestParam(value = "tenantId") Integer tenantId);
+
+    @GetMapping("/intervalDataReportUn")
+    void intervalDataReportUn(@RequestParam(value = "tenantId") Integer tenantId);
 }

+ 20 - 2
service-vpp/service-vpp-api/src/main/java/com/usky/vpp/factory/RemoteVppTaskFactory.java

@@ -15,8 +15,26 @@ public class RemoteVppTaskFactory implements FallbackFactory<RemoteVppTaskServic
     @Override
     public RemoteVppTaskService create(Throwable throwable) {
         log.error("VPP 定时任务服务调用失败: {}", throwable.getMessage());
-        return () -> {
-            throw new FeignBadRequestException(500, "定时刷新站点平均完成率异常" + throwable.getMessage());
+        return new RemoteVppTaskService() {
+            @Override
+            public void refreshSiteAvgCompletionRate() {
+                throw new FeignBadRequestException(500, "定时刷新站点平均完成率异常" + throwable.getMessage());
+            }
+
+            @Override
+            public void pollUn(Integer tenantId) {
+                throw new FeignBadRequestException(500, "定时 UN Poll 异常, tenantId=" + tenantId + throwable.getMessage());
+            }
+
+            @Override
+            public void registerUn(Integer tenantId) {
+                throw new FeignBadRequestException(500, "定时 UN 注册异常, tenantId=" + tenantId + throwable.getMessage());
+            }
+
+            @Override
+            public void intervalDataReportUn(Integer tenantId) {
+                throw new FeignBadRequestException(500, "定时 UN 曲线数据报告异常, tenantId=" + tenantId + throwable.getMessage());
+            }
         };
     }
 }

+ 31 - 14
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnHttpExecutor.java

@@ -3,7 +3,11 @@ package com.usky.vpp.client;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.config.VppUnPropertiesRegistry;
+import com.usky.vpp.config.VppUnTenantContext;
 import com.usky.vpp.crypto.VppUnCryptoService;
+import com.usky.vpp.crypto.VppUnSmCryptoUtil;
+import com.usky.vpp.util.VppUnCipherPayloadHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -30,8 +34,11 @@ public class VppUnHttpExecutor {
 
     private static final Logger log = LoggerFactory.getLogger(VppUnHttpExecutor.class);
 
+    /** Token 获取接口使用明文,不参与 SM2/SM3 加解密。 */
+    private static final String TOKEN_REQUEST = "TokenRequest";
+
     @Autowired
-    private VppUnProperties properties;
+    private VppUnPropertiesRegistry propertiesRegistry;
 
     @Autowired
     private VppUnCryptoService cryptoService;
@@ -62,6 +69,7 @@ public class VppUnHttpExecutor {
     }
 
     private String executePost(String serviceName, Map<String, Object> body, boolean withToken) {
+        VppUnProperties properties = propertiesRegistry.getCurrent();
         if (!properties.isOutboundActive()) {
             throw new BusinessException("运管平台 outbound 未启用或未配置 baseUrl");
         }
@@ -72,20 +80,23 @@ public class VppUnHttpExecutor {
             headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON));
 
             String requestBody = plainJson;
-            if (cryptoService.isActive()) {
-                requestBody = cryptoService.encryptRequest(plainJson);
-                headers.set("X-Sign", cryptoService.signRequest(requestBody));
+            boolean useCrypto = useCrypto(serviceName);
+            if (useCrypto && cryptoService.isActive()) {
+                VppUnSmCryptoUtil.EncryptedPayload payload = cryptoService.prepareRequest(plainJson);
+                requestBody = VppUnCipherPayloadHelper.wrapCipherPayload(payload.getCipherBase64(), objectMapper);
+                headers.set("X-Sign", payload.getSignBase64());
             }
             if (withToken) {
-                headers.set(properties.getTokenHeader(), properties.getTokenPrefix() + tokenHolder.getToken());
+                headers.set("token", tokenHolder.getToken());
             }
 
             String url = normalizeUrl(properties.getBaseUrl()) + "/" + serviceName;
-            log.debug("UN 请求 {} crypto={} token={}", url, cryptoService.isActive(), withToken);
+            log.debug("租户 {} UN 请求 {} crypto={} token={}",
+                    VppUnTenantContext.resolveTenantId(), url, useCrypto && cryptoService.isActive(), withToken);
 
             ResponseEntity<String> response = restTemplate.exchange(
                     url, HttpMethod.POST, new HttpEntity<>(requestBody, headers), String.class);
-            return decodeResponseBody(response.getBody(), response.getHeaders().getFirst("X-Sign"));
+            return decodeResponseBody(response.getBody(), response.getHeaders().getFirst("X-Sign"), useCrypto);
         } catch (HttpStatusCodeException ex) {
             String message = ex.getResponseBodyAsString();
             log.warn("UN 请求 {} 失败 status={} body={}", serviceName, ex.getRawStatusCode(), message);
@@ -114,18 +125,24 @@ public class VppUnHttpExecutor {
                 || message.contains("JWTDecodeException"));
     }
 
-    private String decodeResponseBody(String body, String signHeader) {
+    private String decodeResponseBody(String body, String signHeader, boolean useCrypto) {
         if (!StringUtils.hasText(body)) {
             return "{}";
         }
         String trimmed = body.trim();
-        if (cryptoService.isActive() && !trimmed.startsWith("{")) {
-            if (!cryptoService.verifyResponse(trimmed, signHeader)) {
-                throw new BusinessException("UN 响应验签失败");
-            }
-            return cryptoService.decryptResponse(trimmed);
+        if (!useCrypto || !cryptoService.isActive()) {
+            return trimmed;
+        }
+        if (trimmed.startsWith("{")) {
+            log.warn("UN 响应为明文 JSON,跳过验签解密");
+            return trimmed;
         }
-        return trimmed;
+        String cipherBase64 = VppUnCipherPayloadHelper.extractCipherPayload(trimmed, objectMapper);
+        return cryptoService.parseResponse(cipherBase64, signHeader);
+    }
+
+    private static boolean useCrypto(String serviceName) {
+        return !TOKEN_REQUEST.equals(serviceName);
     }
 
     private static String normalizeUrl(String baseUrl) {

+ 50 - 30
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnTokenHolder.java

@@ -3,6 +3,8 @@ package com.usky.vpp.client;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.config.VppUnPropertiesRegistry;
+import com.usky.vpp.config.VppUnTenantContext;
 import com.usky.vpp.util.VppUnMessageBuilder;
 import com.usky.vpp.util.VppUnPayloadHelper;
 import org.slf4j.Logger;
@@ -13,20 +15,22 @@ import org.springframework.util.StringUtils;
 
 import java.time.Instant;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.locks.ReentrantLock;
 
 /**
- * UN Token 缓存(TokenRequest 获取,默认约 30 分钟过期)
+ * UN Token 缓存(按租户维度,TokenRequest 获取,默认约 30 分钟过期)
  */
 @Component
 public class VppUnTokenHolder {
 
     private static final Logger log = LoggerFactory.getLogger(VppUnTokenHolder.class);
 
-    private final ReentrantLock lock = new ReentrantLock();
+    private final ConcurrentHashMap<Integer, TokenState> tokenStates = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<Integer, ReentrantLock> locks = new ConcurrentHashMap<>();
 
     @Autowired
-    private VppUnProperties properties;
+    private VppUnPropertiesRegistry propertiesRegistry;
 
     @Autowired
     private VppUnHttpExecutor httpExecutor;
@@ -34,55 +38,61 @@ public class VppUnTokenHolder {
     @Autowired
     private ObjectMapper objectMapper;
 
-    private volatile String token;
-    private volatile Instant expiresAt = Instant.EPOCH;
-
     public String getToken() {
-        if (!needsRefresh()) {
-            return token;
+        Integer tenantId = VppUnTenantContext.resolveTenantId();
+        TokenState state = tokenStates.computeIfAbsent(tenantId, id -> createInitialState(id));
+        if (!state.needsRefresh()) {
+            return state.token;
         }
+        ReentrantLock lock = locks.computeIfAbsent(tenantId, id -> new ReentrantLock());
         lock.lock();
         try {
-            if (!needsRefresh()) {
-                return token;
+            state = tokenStates.computeIfAbsent(tenantId, id -> createInitialState(id));
+            if (!state.needsRefresh()) {
+                return state.token;
             }
-            refreshTokenInternal();
-            return token;
+            refreshTokenInternal(tenantId);
+            return state.token;
         } finally {
             lock.unlock();
         }
     }
 
     public void invalidate() {
+        Integer tenantId = VppUnTenantContext.resolveTenantId();
+        ReentrantLock lock = locks.computeIfAbsent(tenantId, id -> new ReentrantLock());
         lock.lock();
         try {
-            token = null;
-            expiresAt = Instant.EPOCH;
+            tokenStates.remove(tenantId);
         } finally {
             lock.unlock();
         }
     }
 
     public void applyTokenResponse(Map<String, Object> response) {
+        Integer tenantId = VppUnTenantContext.resolveTenantId();
         String newToken = VppUnPayloadHelper.getString(response, "token");
         if (!StringUtils.hasText(newToken)) {
             throw new IllegalStateException("TokenResponse 缺少 token");
         }
-        lock.lock();
-        try {
-            token = newToken;
-            int ttlMinutes = properties.getTokenTtlMinutes() != null ? properties.getTokenTtlMinutes() : 25;
-            expiresAt = Instant.now().plusSeconds(ttlMinutes * 60L);
-        } finally {
-            lock.unlock();
-        }
+        VppUnProperties properties = propertiesRegistry.require(tenantId);
+        int ttlMinutes = properties.getTokenTtlMinutes() != null ? properties.getTokenTtlMinutes() : 25;
+        TokenState state = new TokenState(newToken, Instant.now().plusSeconds(ttlMinutes * 60L));
+        tokenStates.put(tenantId, state);
+        propertiesRegistry.updateToken(tenantId, newToken);
     }
 
-    private boolean needsRefresh() {
-        return !StringUtils.hasText(token) || Instant.now().isAfter(expiresAt);
+    private TokenState createInitialState(Integer tenantId) {
+        VppUnProperties properties = propertiesRegistry.get(tenantId);
+        if (properties != null && StringUtils.hasText(properties.getToken())) {
+            int ttlMinutes = properties.getTokenTtlMinutes() != null ? properties.getTokenTtlMinutes() : 25;
+            return new TokenState(properties.getToken(), Instant.now().plusSeconds(ttlMinutes * 60L));
+        }
+        return new TokenState(null, Instant.EPOCH);
     }
 
-    private void refreshTokenInternal() {
+    private void refreshTokenInternal(Integer tenantId) {
+        VppUnProperties properties = propertiesRegistry.require(tenantId);
         Map<String, Object> request = VppUnMessageBuilder.buildTokenRequest(properties);
         String responseBody = httpExecutor.postRaw("TokenRequest", request, false);
         try {
@@ -92,16 +102,26 @@ public class VppUnTokenHolder {
             if (code != null && code != 200) {
                 throw new IllegalStateException("TokenRequest 失败: " + response.get("description"));
             }
-            String newToken = VppUnPayloadHelper.getString(response, "token");
-            if (!StringUtils.hasText(newToken)) {
-                throw new IllegalStateException("TokenResponse 缺少 token");
-            }
             applyTokenResponse(response);
-            log.info("UN Token 已刷新");
+            log.info("租户 {} UN Token 已刷新", tenantId);
         } catch (IllegalStateException ex) {
             throw ex;
         } catch (Exception ex) {
             throw new IllegalStateException("TokenResponse 解析失败", ex);
         }
     }
+
+    private static final class TokenState {
+        private final String token;
+        private final Instant expiresAt;
+
+        private TokenState(String token, Instant expiresAt) {
+            this.token = token;
+            this.expiresAt = expiresAt;
+        }
+
+        private boolean needsRefresh() {
+            return !StringUtils.hasText(token) || Instant.now().isAfter(expiresAt);
+        }
+    }
 }

+ 7 - 11
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnProperties.java

@@ -1,17 +1,16 @@
 package com.usky.vpp.config;
 
 import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
 
 /**
- * 运管平台 UN/DN 对接配置
+ * 运管平台 UN/DN 对接配置(按租户维度,由 vpp_tenant_config 加载至内存)
  */
 @Data
-@Component
-@ConfigurationProperties(prefix = "vpp.un")
 public class VppUnProperties {
 
+    /** 租户 ID */
+    private Integer tenantId;
+
     /** 运管平台 UN 基地址 */
     private String baseUrl;
 
@@ -33,6 +32,9 @@ public class VppUnProperties {
     /** DN 私钥 Base64 */
     private String dnPrivateKey;
 
+    /** 数据库持久化的 token */
+    private String token;
+
     /** Poll 轮询间隔秒,默认 10 */
     private Integer pollIntervalSec = 10;
 
@@ -51,12 +53,6 @@ public class VppUnProperties {
     /** 已保存的 registrationID(首次注册后需持久化) */
     private String registrationId;
 
-    /** Token 请求头,默认 Authorization */
-    private String tokenHeader = "Authorization";
-
-    /** Token 前缀,默认 Bearer  */
-    private String tokenPrefix = "Bearer ";
-
     /** Token 有效期分钟(文档默认 30),用于本地缓存刷新 */
     private Integer tokenTtlMinutes = 25;
 

+ 166 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnPropertiesRegistry.java

@@ -0,0 +1,166 @@
+package com.usky.vpp.config;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.domain.VppRegistration;
+import com.usky.vpp.domain.VppTenantConfig;
+import com.usky.vpp.mapper.VppRegistrationMapper;
+import com.usky.vpp.mapper.VppTenantConfigMapper;
+import com.usky.vpp.util.VppAuditHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 租户 UN 配置全局内存缓存,key=tenantId,value=VppUnProperties
+ */
+@Component
+public class VppUnPropertiesRegistry {
+
+    private static final Logger log = LoggerFactory.getLogger(VppUnPropertiesRegistry.class);
+
+    private final ConcurrentHashMap<Integer, VppUnProperties> cache = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<String, Integer> dnIdToTenantId = new ConcurrentHashMap<>();
+
+    @Autowired
+    private VppTenantConfigMapper tenantConfigMapper;
+
+    @Autowired
+    private VppRegistrationMapper registrationMapper;
+
+    public void refreshAll() {
+        List<VppTenantConfig> configs = tenantConfigMapper.selectList(new LambdaQueryWrapper<>());
+        if (CollectionUtils.isEmpty(configs)) {
+            cache.clear();
+            dnIdToTenantId.clear();
+            log.warn("vpp_tenant_config 无配置记录,UN 对接配置缓存为空");
+            return;
+        }
+
+        Map<Integer, VppUnProperties> nextCache = new ConcurrentHashMap<>();
+        Map<String, Integer> nextDnIndex = new ConcurrentHashMap<>();
+        for (VppTenantConfig config : configs) {
+            if (config.getTenantId() == null) {
+                continue;
+            }
+            VppUnProperties properties = toProperties(config);
+            enrichRegistrationId(properties);
+            nextCache.put(config.getTenantId(), properties);
+            if (StringUtils.hasText(config.getDnId())) {
+                nextDnIndex.put(config.getDnId(), config.getTenantId());
+            }
+        }
+        cache.clear();
+        cache.putAll(nextCache);
+        dnIdToTenantId.clear();
+        dnIdToTenantId.putAll(nextDnIndex);
+        log.info("已加载 {} 条租户 UN 配置至内存", cache.size());
+    }
+
+    public VppUnProperties get(Integer tenantId) {
+        if (tenantId == null) {
+            return null;
+        }
+        return cache.get(tenantId);
+    }
+
+    public VppUnProperties require(Integer tenantId) {
+        VppUnProperties properties = get(tenantId);
+        if (properties == null) {
+            throw new BusinessException("租户 " + tenantId + " 未配置运管平台 UN 对接信息");
+        }
+        return properties;
+    }
+
+    public VppUnProperties getCurrent() {
+        // 定时任务:runWithTenant 传入的 tenantId;接口调用:SecurityUtils.getTenantId()
+        return require(VppUnTenantContext.resolveTenantId());
+    }
+
+    public VppUnProperties getByDnId(String dnId) {
+        if (!StringUtils.hasText(dnId)) {
+            return null;
+        }
+        Integer tenantId = dnIdToTenantId.get(dnId);
+        return tenantId != null ? get(tenantId) : null;
+    }
+
+    public Collection<VppUnProperties> getAll() {
+        return Collections.unmodifiableCollection(cache.values());
+    }
+
+    public boolean hasCryptoEnabledTenant() {
+        return cache.values().stream().anyMatch(this::isCryptoActive);
+    }
+
+    private boolean isCryptoActive(VppUnProperties properties) {
+        return Boolean.TRUE.equals(properties.getCryptoEnabled())
+                && StringUtils.hasText(properties.getUnPublicKey())
+                && StringUtils.hasText(properties.getDnPrivateKey());
+    }
+
+    public void updateRegistrationId(Integer tenantId, String registrationId) {
+        VppUnProperties properties = get(tenantId);
+        if (properties != null) {
+            properties.setRegistrationId(registrationId);
+        }
+    }
+
+    public void updateToken(Integer tenantId, String token) {
+        VppUnProperties properties = get(tenantId);
+        if (properties != null) {
+            properties.setToken(token);
+        }
+    }
+
+    private VppUnProperties toProperties(VppTenantConfig config) {
+        VppUnProperties properties = new VppUnProperties();
+        properties.setTenantId(config.getTenantId());
+        properties.setDnId(config.getDnId());
+        properties.setDnName(config.getDnName());
+        properties.setBaseUrl(config.getBaseUrl());
+        properties.setTransportAddress(config.getTransportAddress());
+        properties.setUnPublicKey(config.getUnPublicKey());
+        properties.setDnPublicKey(config.getDnPublicKey());
+        properties.setDnPrivateKey(config.getDnPrivateKey());
+        properties.setToken(config.getToken());
+        properties.setPollIntervalSec(config.getPollIntervalSec() != null ? config.getPollIntervalSec() : 10);
+        properties.setOutboundEnabled(config.getOutboundEnabled());
+        properties.setPollEnabled(config.getPollEnabled());
+        properties.setCryptoEnabled(config.getCryptoEnabled());
+        properties.setPriceDownCoeff(StringUtils.hasText(config.getPriceDownCoeff()) ? config.getPriceDownCoeff() : "0.8");
+        properties.setAutoAckClearing(config.getAutoAckClearing());
+        properties.setAutoRegisterOnStartup(config.getAutoRegisterOnStartup());
+        properties.setTokenTtlMinutes(25);
+        properties.setConnectTimeoutMs(10000);
+        properties.setReadTimeoutMs(30000);
+        return properties;
+    }
+
+    private void enrichRegistrationId(VppUnProperties properties) {
+        if (!StringUtils.hasText(properties.getDnId())) {
+            return;
+        }
+        LambdaQueryWrapper<VppRegistration> wrapper = new LambdaQueryWrapper<VppRegistration>()
+                .eq(VppRegistration::getDnId, properties.getDnId())
+                .eq(VppRegistration::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByDesc(VppRegistration::getRegisteredAt)
+                .last("LIMIT 1");
+        if (properties.getTenantId() != null) {
+            wrapper.eq(VppRegistration::getTenantId, properties.getTenantId());
+        }
+        VppRegistration registration = registrationMapper.selectOne(wrapper);
+        if (registration != null && StringUtils.hasText(registration.getRegistrationId())) {
+            properties.setRegistrationId(registration.getRegistrationId());
+        }
+    }
+}

+ 6 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnRestTemplateConfig.java

@@ -10,13 +10,14 @@ import java.time.Duration;
 @Configuration
 public class VppUnRestTemplateConfig {
 
+    private static final int DEFAULT_CONNECT_TIMEOUT_MS = 10000;
+    private static final int DEFAULT_READ_TIMEOUT_MS = 30000;
+
     @Bean
-    public RestTemplate vppUnRestTemplate(VppUnProperties properties, RestTemplateBuilder builder) {
-        int connectMs = properties.getConnectTimeoutMs() != null ? properties.getConnectTimeoutMs() : 10000;
-        int readMs = properties.getReadTimeoutMs() != null ? properties.getReadTimeoutMs() : 30000;
+    public RestTemplate vppUnRestTemplate(RestTemplateBuilder builder) {
         return builder
-                .setConnectTimeout(Duration.ofMillis(connectMs))
-                .setReadTimeout(Duration.ofMillis(readMs))
+                .setConnectTimeout(Duration.ofMillis(DEFAULT_CONNECT_TIMEOUT_MS))
+                .setReadTimeout(Duration.ofMillis(DEFAULT_READ_TIMEOUT_MS))
                 .build();
     }
 }

+ 61 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnTenantContext.java

@@ -0,0 +1,61 @@
+package com.usky.vpp.config;
+
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+
+/**
+ * 当前线程 UN 对接所使用的租户上下文
+ */
+public final class VppUnTenantContext {
+
+    private static final ThreadLocal<Integer> TENANT_ID = new ThreadLocal<>();
+
+    private VppUnTenantContext() {
+    }
+
+    public static void setTenantId(Integer tenantId) {
+        TENANT_ID.set(tenantId);
+    }
+
+    public static Integer getTenantId() {
+        return TENANT_ID.get();
+    }
+
+    /**
+     * 解析当前租户 ID:定时任务等场景优先使用 {@link #runWithTenant} 传入的 tenantId;
+     * 接口调用场景使用 {@link SecurityUtils#getTenantId()}。
+     */
+    public static Integer resolveTenantId() {
+        Integer tenantId = TENANT_ID.get();
+        if (tenantId != null) {
+            return tenantId;
+        }
+        tenantId = SecurityUtils.getTenantId();
+        if (tenantId != null && tenantId > 0) {
+            return tenantId;
+        }
+        throw new BusinessException("缺少租户上下文,无法获取运管平台配置租户Id");
+    }
+
+    public static void clear() {
+        TENANT_ID.remove();
+    }
+
+    public static void runWithTenant(Integer tenantId, Runnable action) {
+        setTenantId(tenantId);
+        try {
+            action.run();
+        } finally {
+            clear();
+        }
+    }
+
+    public static <T> T callWithTenant(Integer tenantId, java.util.function.Supplier<T> action) {
+        setTenantId(tenantId);
+        try {
+            return action.get();
+        } finally {
+            clear();
+        }
+    }
+}

+ 19 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppDrEventStatus.java

@@ -0,0 +1,19 @@
+package com.usky.vpp.constant;
+
+/**
+ * 需求响应事件 / 邀约响应状态:0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消
+ * <p>vpp_dr_event.event_status 与 vpp_dr_invitation.response_status 共用同一套取值。</p>
+ */
+public final class VppDrEventStatus {
+
+    public static final int PENDING = 0;
+    public static final int DECLARED = 1;
+    public static final int DECLARE_COMPLETED = 2;
+    public static final int EXECUTING = 3;
+    public static final int ENDED = 4;
+    public static final int CANCELLED = 5;
+    public static final int LAST_PLACE_SPLITTING = 6;
+
+    private VppDrEventStatus() {
+    }
+}

+ 2 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppTsdbConstants.java

@@ -36,7 +36,8 @@ public final class VppTsdbConstants {
             "device_status",
             "p",
             "ua",
-            "ia"
+            "ia",
+            "soc"
     );
 
     /** 日发电量(光伏) */

+ 27 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/api/ServiceVppTaskApi.java

@@ -2,6 +2,9 @@ package com.usky.vpp.controller.api;
 
 import com.usky.vpp.RemoteVppTaskService;
 import com.usky.vpp.service.VppSiteCompletionRateTaskService;
+import com.usky.vpp.service.VppUnIntervalDataReportTaskService;
+import com.usky.vpp.service.VppUnPollTaskService;
+import com.usky.vpp.service.VppUnRegisterTaskService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -11,8 +14,32 @@ public class ServiceVppTaskApi implements RemoteVppTaskService {
     @Autowired
     private VppSiteCompletionRateTaskService siteCompletionRateTaskService;
 
+    @Autowired
+    private VppUnPollTaskService unPollTaskService;
+
+    @Autowired
+    private VppUnRegisterTaskService unRegisterTaskService;
+
+    @Autowired
+    private VppUnIntervalDataReportTaskService unIntervalDataReportTaskService;
+
     @Override
     public void refreshSiteAvgCompletionRate() {
         siteCompletionRateTaskService.refreshSiteAvgCompletionRate();
     }
+
+    @Override
+    public void pollUn(Integer tenantId) {
+        unPollTaskService.pollUn(tenantId);
+    }
+
+    @Override
+    public void registerUn(Integer tenantId) {
+        unRegisterTaskService.registerUn(tenantId);
+    }
+
+    @Override
+    public void intervalDataReportUn(Integer tenantId) {
+        unIntervalDataReportTaskService.intervalDataReportUn(tenantId);
+    }
 }

+ 28 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java

@@ -4,6 +4,8 @@ import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.VppContractService;
 import com.usky.vpp.service.VppContractTemplateService;
+import com.usky.vpp.service.vo.VppContractExpireStatVO;
+import com.usky.vpp.service.vo.VppContractOverviewVO;
 import com.usky.vpp.service.vo.VppContractRequestVO;
 import com.usky.vpp.service.vo.VppContractResponseVO;
 import com.usky.vpp.service.vo.VppContractTemplateRequestVO;
@@ -111,7 +113,10 @@ public class ContractController {
      * templateId     模板ID
      * contractType   合同类型
      * customerId     客户ID
-     * contractStatus 合同状态
+     * customerName   客户名称 模糊匹配
+     * signDateStart  签订时间起始
+     * signDateEnd    签订时间截止
+     * contractStatus 合同状态 0草稿 1审核中 2待生效 3已生效 4已驳回 5已到期 6已终止 7即将到期(三月内过期)
      * current        页码
      * size           页大小
      */
@@ -123,11 +128,32 @@ public class ContractController {
             @RequestParam(value = "templateId", required = false) Long templateId,
             @RequestParam(value = "contractType", required = false) Integer contractType,
             @RequestParam(value = "customerId", required = false) Long customerId,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "signDateStart", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate signDateStart,
+            @RequestParam(value = "signDateEnd", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate signDateEnd,
             @RequestParam(value = "contractStatus", required = false) Integer contractStatus,
             @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
             @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
         return ApiResult.success(vppContractService.page(id, startDate, endDate,
-                templateId, contractType, customerId, contractStatus, current, size));
+                templateId, contractType, customerId, customerName, signDateStart, signDateEnd, contractStatus, current, size));
+    }
+
+    /**
+     * 按客户最新合同统计已到期 / 即将到期数量
+     * <p>租户隔离;每个客户仅取创建时间最新的一份合同,返回已到期、即将到期(近三个月)两类数量。
+     * 列表请调用分页接口:contractStatus=5 查已到期,contractStatus=7 查即将到期。</p>
+     */
+    @GetMapping("/expireStat")
+    public ApiResult<VppContractExpireStatVO> expireStat() {
+        return ApiResult.success(vppContractService.expireStat());
+    }
+
+    /**
+     * 合同概况(甲方、乙方、签订日期、合同金额、起止时间、生效状态)
+     */
+    @GetMapping("/{id}/overview")
+    public ApiResult<VppContractOverviewVO> overview(@PathVariable("id") Long id) {
+        return ApiResult.success(vppContractService.overview(id));
     }
 
     /**

+ 114 - 7
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java

@@ -8,9 +8,11 @@ import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.service.VppDrInvitationService;
 import com.usky.vpp.service.VppDrService;
 import com.usky.vpp.service.VppDrSubsidyPredictionService;
+import com.usky.vpp.service.VppResponseDeviationPriceService;
 import com.usky.vpp.service.vo.DrClearingRequest;
 import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
+import com.usky.vpp.service.vo.DrEventStatusRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrInvitationNotifyRequest;
 import com.usky.vpp.service.vo.DrInvitationReplyRequest;
@@ -19,10 +21,15 @@ import com.usky.vpp.service.vo.DrInvitationVO;
 import com.usky.vpp.service.vo.DrParticipateRequest;
 import com.usky.vpp.service.vo.DrStrategyRequest;
 import com.usky.vpp.service.vo.DrStrategyVO;
+import com.usky.vpp.service.vo.DrSubsidyPredictionCalculateRequest;
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
+import com.usky.vpp.service.vo.ResponseDeviationPriceAdjustmentRequest;
+import com.usky.vpp.service.vo.ResponseDeviationPriceAdjustmentVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
 import java.util.Map;
 
 /**
@@ -39,12 +46,25 @@ public class DrController {
     private VppDrInvitationService vppDrInvitationService;
     @Autowired
     private VppDrSubsidyPredictionService vppDrSubsidyPredictionService;
+    @Autowired
+    private VppResponseDeviationPriceService responseDeviationPriceService;
 
     @GetMapping(value = "/event")
     public ApiResult<CommonPage<VppDrEvent>> pageEvent(@RequestParam(required = false) Map<String, Object> params) {
         return ApiResult.success(vppDrService.pageEvent(params));
     }
 
+    /**
+     * 修改事件状态(独立接口,不修改事件其它字段)。
+     * <p>校验租户后,仅允许从 0待参与 改为 1已申报。</p>
+     * <p>事件状态:0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消</p>
+     */
+    @PutMapping(value = "/event/status")
+    public ApiResult<Void> updateEventStatus(@RequestBody DrEventStatusRequest body) {
+        vppDrService.updateEventStatus(body);
+        return ApiResult.success();
+    }
+
     @GetMapping(value = "/event/{id}")
     public ApiResult<DrEventDetailVO> getEvent(@PathVariable("id") Long id) {
         return ApiResult.success(vppDrService.getEvent(id));
@@ -139,6 +159,9 @@ public class DrController {
         return ApiResult.success();
     }
 
+    /**
+     * 邀约响应状态:0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消(与事件状态一致)
+     */
     @GetMapping(value = "/invitation")
     public ApiResult<CommonPage<DrInvitationVO>> pageInvitation(@RequestParam(required = false) Map<String, Object> params) {
         return ApiResult.success(vppDrInvitationService.pageInvitation(params));
@@ -182,19 +205,103 @@ public class DrController {
 
     /**
      * 分页查询补贴金额预测
-     * siteName  站点名称(模糊匹配)
-     * startTime 响应开始时间起始
-     * endTime   响应开始时间截止
-     * current   页码
-     * size      页大小
+     * siteName     站点名称(模糊匹配)
+     * customerName 企业名称(模糊匹配)
+     * startTime    响应开始时间起始
+     * endTime      响应开始时间截止
+     * current      页码
+     * size         页大小
      */
     @GetMapping(value = "/subsidyPrediction")
     public ApiResult<CommonPage<DrSubsidyPredictionVO>> pageSubsidyPrediction(
             @RequestParam(value = "siteName", required = false) String siteName,
+            @RequestParam(value = "customerName", required = false) String customerName,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
             @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
-        return ApiResult.success(vppDrSubsidyPredictionService.page(siteName, startTime, endTime, current, size));
+        return ApiResult.success(vppDrSubsidyPredictionService.page(
+                siteName, customerName, startTime, endTime, current, size));
+    }
+
+    /**
+     * 试算补贴金额预测(不落库)。
+     * <p>入参:邀约编号、实际响应容量(电网);其余计算字段由服务端按邀约/事件查询。
+     * 前端拿到结果后再调用新增接口入库。</p>
+     */
+    @PostMapping(value = "/subsidyPrediction/calculate")
+    public ApiResult<DrSubsidyPredictionVO> calculateSubsidyPrediction(
+            @RequestBody DrSubsidyPredictionCalculateRequest request) {
+        return ApiResult.success(vppDrSubsidyPredictionService.calculate(request));
+    }
+
+    /**
+     * 新增补贴金额预测。
+     * <p>按前端传参直接入库(通常携带试算结果),服务端不再重算。</p>
+     */
+    @PostMapping(value = "/subsidyPrediction")
+    public ApiResult<Boolean> createSubsidyPrediction(@RequestBody DrSubsidyPredictionRequest request) {
+        return vppDrSubsidyPredictionService.create(request)
+                ? ApiResult.success(true) : ApiResult.error("新增补贴预测失败!请重试!");
+    }
+
+    /**
+     * 修改补贴金额预测。
+     * <p>按前端传参直接入库,服务端不再重算。</p>
+     */
+    @PutMapping(value = "/subsidyPrediction")
+    public ApiResult<Boolean> updateSubsidyPrediction(@RequestBody DrSubsidyPredictionRequest request) {
+        return vppDrSubsidyPredictionService.update(request)
+                ? ApiResult.success(true) : ApiResult.error("修改补贴预测失败!请重试!");
+    }
+
+    /**
+     * 删除补贴金额预测
+     */
+    @DeleteMapping(value = "/subsidyPrediction/{id}")
+    public ApiResult<Boolean> deleteSubsidyPrediction(@PathVariable("id") Long id) {
+        return vppDrSubsidyPredictionService.delete(id)
+                ? ApiResult.success(true) : ApiResult.error("删除补贴预测失败!请重试!");
+    }
+
+    // ==================== 响应量偏差价格调整 ====================
+    /**
+     * 分页查询响应量偏差价格调整。
+     * completionRate 可选,传入时按结构化区间匹配命中配置。
+     * current   页码
+     * size      页大小
+     */
+    @GetMapping(value = "/responseDeviationPrice")
+    public ApiResult<CommonPage<ResponseDeviationPriceAdjustmentVO>> pageResponseDeviationPriceAdjustment(
+            @RequestParam(value = "completionRate", required = false) BigDecimal completionRate,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(responseDeviationPriceService.page(completionRate, current, size));
+    }
+
+    @GetMapping(value = "/responseDeviationPrice/{id}")
+    public ApiResult<ResponseDeviationPriceAdjustmentVO> getResponseDeviationPriceAdjustment(
+            @PathVariable("id") Long id) {
+        return ApiResult.success(responseDeviationPriceService.get(id));
+    }
+
+    @PostMapping(value = "/responseDeviationPrice")
+    public ApiResult<Long> createResponseDeviationPriceAdjustment(
+            @RequestBody ResponseDeviationPriceAdjustmentRequest request) {
+        return ApiResult.success(responseDeviationPriceService.create(request));
+    }
+
+    @PutMapping(value = "/responseDeviationPrice/{id}")
+    public ApiResult<Void> updateResponseDeviationPriceAdjustment(
+            @PathVariable("id") Long id,
+            @RequestBody ResponseDeviationPriceAdjustmentRequest request) {
+        responseDeviationPriceService.update(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping(value = "/responseDeviationPrice/{id}")
+    public ApiResult<Void> deleteResponseDeviationPriceAdjustment(@PathVariable("id") Long id) {
+        responseDeviationPriceService.delete(id);
+        return ApiResult.success();
     }
-}
+}

+ 17 - 31
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java

@@ -5,21 +5,16 @@ import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.VppDrMonitorService;
 import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
+import com.usky.vpp.service.vo.DrMonitorSiteResourceVO;
 import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 /**
  * 需求响应监测(响应记录)
@@ -34,7 +29,7 @@ public class DrMonitorController {
 
     /**
      * 顶部 KPI:响应次数 / 响应容量(kW) / 历史达标率
-     * <p>必传站点;先查站点下 is_support_peak=1 资源点,再按资源点平均。</p>
+     * <p>时间区间内事件开始、结束均落在区间且状态为执行中/已结束,再关联参与且执行中/已结束的邀约。</p>
      */
     @GetMapping("/summary")
     public ApiResult<DrMonitorSummaryVO> summary(@RequestParam("startDate") String startDate,
@@ -44,8 +39,8 @@ public class DrMonitorController {
     }
 
     /**
-     * 响应记录分页(字段对齐 vpp_dr_event
-     * <p>必传站点;仅统计可调峰资源点参与过的事件。</p>
+     * 响应记录分页(口径与 summary 一致,按邀约展开
+     * <p>在事件字段基础上补充客户、站点;目标容量取申报出清容量,出清容量取实际响应容量。</p>
      */
     @GetMapping("/records")
     public ApiResult<CommonPage<DrMonitorRecordVO>> records(@RequestParam("startDate") String startDate,
@@ -56,29 +51,20 @@ public class DrMonitorController {
     }
 
     /**
-     * 折线图:基线/实际负荷 + 申报/实际响应容量
-     * <p>必传站点;仅 is_support_peak=1 资源点;曲线按资源点加权平均。</p>
+     * 折线图:基线负荷全天曲线 + 实际响应容量
+     * <p>id 为邀约表主键;按邀约站点与响应时间调用 getSiteBaseline。</p>
      */
-    @GetMapping("/records/{id}/curve")
-    public ApiResult<DrMonitorCurveVO> curve(@PathVariable("id") Long id,
-                                             @RequestParam("siteIds") List<Long> siteIds) {
-        return ApiResult.success(drMonitorService.getCurve(id, siteIds));
+    @GetMapping("/records/curve")
+    public ApiResult<DrMonitorCurveVO> curve(@RequestParam("id") Long id) {
+        return ApiResult.success(drMonitorService.getCurve(id));
     }
 
-    // private List<Long> parseSiteIds(List<String> raw) {
-    //     if (raw == null || raw.isEmpty()) {
-    //         return Collections.emptyList();
-    //     }
-    //     List<Long> ids = new ArrayList<>();
-    //     for (String item : raw) {
-    //         if (!StringUtils.hasText(item)) {
-    //             continue;
-    //         }
-    //         Arrays.stream(item.split(","))
-    //                 .map(String::trim)
-    //                 .filter(StringUtils::hasText)
-    //                 .forEach(token -> ids.add(Long.parseLong(token)));
-    //     }
-    //     return ids.stream().distinct().collect(Collectors.toList());
-    // }
+    /**
+     * 按响应事件查询邀约站点及其可调峰资源点
+     * <p>id 为 vpp_dr_event 主键;先查事件下全部邀约站点(去重),再查各站点 is_support_peak=1 的资源点,按站点分组返回。</p>
+     */
+    @GetMapping("/site-resources")
+    public ApiResult<List<DrMonitorSiteResourceVO>> siteResources(@RequestParam("id") Long id) {
+        return ApiResult.success(drMonitorService.listEventSitePeakResources(id));
+    }
 }

+ 1 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResourceController.java

@@ -90,7 +90,7 @@ public class ResourceController {
 
 //    /**
 //     * 资源概览 - 实时告警统计
-//     * <p>按紧急/重要/一般统计告警数量,含跳转告警列表路径(模拟数据)。</p>
+//     * <p>按紧急/重要/一般统计告警数量,含跳转告警列表路径。</p>
 //     */
 //    @GetMapping("/overview/alarm-stats")
 //    public ApiResult<List<AlarmLevelStatVO>> resourceOverviewAlarmStats() {

+ 10 - 6
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/SiteController.java

@@ -160,7 +160,7 @@ public class SiteController {
      *   <li>responseDayTypeLabel — 工作日 / 非工作日</li>
      *   <li>referenceDates — 参与计算的典型历史日(yyyy-MM-dd)</li>
      *   <li>correctionFactorK — 日内修正系数 K</li>
-     *   <li>dataSource — tsdb(时序库)/ mock(联调模拟)</li>
+     *   <li>dataSource — tsdb(时序库)</li>
      *   <li>points — 曲线点列表(BaselinePointVO:time、predictedBaselineKw、todayActualKw、yesterdayActualKw)</li>
      * </ul>
      *
@@ -173,12 +173,12 @@ public class SiteController {
     }
 
     /**
-     * 响应申报容量
+     * 响应申报容量和预估响应容量
      * <p><b>GET</b> /site/{siteId}/declared-capacity</p>
      * <p>完整路径:GET /prod-api/service-vpp/site/{siteId}/declared-capacity</p>
      *
      * <p>根据站点 ID、响应开始/结束时间,计算需求响应申报容量:</p>
-     * <p><b>申报容量 = 响应时段内同时间段历史负荷均值 × 修正系数 K × 执行时段小时数 / 2</b></p>
+     * <p><b>申报容量 = 响应时段内同时间段历史负荷均值 × 执行时段小时数 / 2</b></p>
      *
      * <p><b>Query 参数</b></p>
      * <ul>
@@ -186,13 +186,17 @@ public class SiteController {
      *   <li>responseEndTime — 响应结束时间,必填,格式 yyyy-MM-dd HH:mm:ss(需与开始时间同一天)</li>
      * </ul>
      *
-     * <p>典型历史日、修正系数 K 规则与基线管理接口一致;历史均值仅统计响应开始至结束时段。</p>
+     * <p><b>典型历史日选取规则</b>(基于 Hutool 法定节假日 + 调休判定)</p>
+     * <ul>
+     *   <li>响应日为工作日:取邀约日前最近 5 个工作日;剔除响应当日、历史响应日(vpp_dr_event 已结束)和响应日 &gt;= 今日</li>
+     *   <li>响应日为非工作日:取邀约日前最近 3 个非工作日(含法定节假日);剔除响应当日、历史响应日和响应日 &gt;= 今日</li>
+     * </ul>
+     * <p>历史均值仅统计响应开始至结束时段。</p>
      *
      * <p><b>响应 data:SiteDeclaredCapacityVO</b></p>
      * <ul>
      *   <li>declaredCapacityKw — 申报容量 kW</li>
-     *   <li>correctionFactorK — 修正系数 K</li>
-     *   <li>referenceDates — 典型历史日</li>
+     *   <li>referenceDates — 参与计算的典型历史日</li>
      *   <li>points — 响应时段各时间点申报容量(DeclaredCapacityPointVO)</li>
      * </ul>
      *

+ 104 - 34
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/crypto/VppUnCryptoService.java

@@ -1,20 +1,19 @@
 package com.usky.vpp.crypto;
 
-import cn.hutool.core.codec.Base64;
-import cn.hutool.crypto.SmUtil;
-import cn.hutool.crypto.asymmetric.KeyType;
-import cn.hutool.crypto.asymmetric.SM2;
+import com.usky.common.core.exception.BusinessException;
 import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.config.VppUnPropertiesRegistry;
+import com.usky.vpp.config.VppUnTenantContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
-import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 
 /**
- * 运管平台 UN/DN 国密加解密与签名(SM2/SM3withSM2)
+ * 运管平台 UN/DN 加解密门面(除 TokenRequest 外均启用)。
  */
 @Service
 public class VppUnCryptoService {
@@ -22,56 +21,127 @@ public class VppUnCryptoService {
     private static final Logger log = LoggerFactory.getLogger(VppUnCryptoService.class);
 
     @Autowired
-    private VppUnProperties properties;
+    private VppUnPropertiesRegistry propertiesRegistry;
 
     public boolean isActive() {
-        return Boolean.TRUE.equals(properties.getCryptoEnabled())
-                && StringUtils.hasText(properties.getUnPublicKey())
-                && StringUtils.hasText(properties.getDnPrivateKey());
+        return isCryptoActive(currentProperties());
     }
 
-    public String encryptRequest(String plainJson) {
-        SM2 sm2 = SmUtil.sm2(null, properties.getUnPublicKey());
-        return sm2.encryptBase64(plainJson, KeyType.PublicKey);
+    public boolean isAnyActive() {
+        return propertiesRegistry.hasCryptoEnabledTenant();
     }
 
-    public String signRequest(String cipherBase64) {
-        SM2 sm2 = SmUtil.sm2(properties.getDnPrivateKey(), null);
-        byte[] sign = sm2.sign(cipherBase64.getBytes(StandardCharsets.UTF_8));
-        return Base64.encode(sign);
+    /** DN 主动请求 UN:UN 公钥加密 + DN 私钥对密文字节签名。 */
+    public VppUnSmCryptoUtil.EncryptedPayload prepareRequest(String plainJson) {
+        VppUnProperties properties = requireCryptoProperties();
+        try {
+            return VppUnSmCryptoUtil.encryptAndSignRequest(
+                    plainJson, properties.getUnPublicKey(), properties.getDnPrivateKey());
+        } catch (Exception ex) {
+            throw new BusinessException("UN 请求加密签名失败: " + ex.getMessage());
+        }
     }
 
-    public String decryptResponse(String cipherBase64) {
-        SM2 sm2 = SmUtil.sm2(properties.getDnPrivateKey(), properties.getDnPublicKey());
-        return sm2.decryptStr(cipherBase64, KeyType.PrivateKey);
+    /** DN 解析 UN 响应:UN 公钥验签 + DN 私钥解密。 */
+    public String parseResponse(String cipherBase64, String signBase64) {
+        if (!StringUtils.hasText(signBase64)) {
+            log.warn("UN 响应缺少 X-Sign");
+            throw new BusinessException("UN 响应验签失败");
+        }
+        VppUnProperties properties = requireCryptoProperties();
+        try {
+            return VppUnSmCryptoUtil.verifyAndDecryptResponse(
+                    cipherBase64, signBase64, properties.getUnPublicKey(), properties.getDnPrivateKey());
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            if (ex.getMessage() != null && ex.getMessage().toLowerCase().contains("sign verify")) {
+                throw new BusinessException("UN 响应验签失败");
+            }
+            throw new BusinessException("UN 响应解密失败: " + ex.getMessage());
+        }
     }
 
-    public boolean verifyResponse(String cipherBase64, String signBase64) {
-        return verifyInbound(cipherBase64, signBase64);
+    /** UN 被动调用 DN:验签后解密。 */
+    public String decryptInboundWithAutoTenant(String cipherBase64, String signBase64) {
+        Integer tenantId = VppUnTenantContext.getTenantId();
+        if (tenantId != null) {
+            assertInboundVerified(cipherBase64, signBase64);
+            return decryptInbound(cipherBase64);
+        }
+        for (VppUnProperties properties : propertiesRegistry.getAll()) {
+            if (!isCryptoActive(properties)) {
+                continue;
+            }
+            VppUnTenantContext.setTenantId(properties.getTenantId());
+            try {
+                if (verifyInbound(cipherBase64, signBase64)) {
+                    return decryptInbound(cipherBase64);
+                }
+            } finally {
+                VppUnTenantContext.clear();
+            }
+        }
+        throw new BusinessException("UN 请求验签失败");
     }
 
-    /** UN→DN 请求验签 */
+    /** UN→DN 请求验签(UN 公钥,对密文字节验签)。 */
     public boolean verifyInbound(String cipherBase64, String signBase64) {
         if (!StringUtils.hasText(signBase64)) {
-            log.warn("UN 请求缺少 X-Sign,跳过验签");
+            log.warn("UN 请求缺少 X-Sign");
+            return false;
+        }
+        VppUnProperties properties = currentProperties();
+        if (!isCryptoActive(properties)) {
             return true;
         }
-        SM2 sm2 = SmUtil.sm2(null, properties.getUnPublicKey());
-        return sm2.verify(cipherBase64.getBytes(StandardCharsets.UTF_8), Base64.decode(signBase64));
+        try {
+            byte[] cipherBytes = Base64.getDecoder().decode(cipherBase64.trim());
+            return VppUnSmCryptoUtil.verifyCipherBytes(
+                    cipherBytes, signBase64, properties.getUnPublicKey());
+        } catch (Exception ex) {
+            log.warn("UN 请求验签异常: {}", ex.getMessage());
+            return false;
+        }
     }
 
-    /** UN→DN 请求解密 */
+    /** UN→DN 请求解密(DN 私钥)。 */
     public String decryptInbound(String cipherBase64) {
-        return decryptResponse(cipherBase64);
+        VppUnProperties properties = requireCryptoProperties();
+        try {
+            return VppUnSmCryptoUtil.decryptByDnPrivateKey(cipherBase64, properties.getDnPrivateKey());
+        } catch (Exception ex) {
+            throw new BusinessException("UN 请求解密失败: " + ex.getMessage());
+        }
     }
 
-    /** DN→UN 响应加密 */
-    public String encryptOutbound(String plainJson) {
-        return encryptRequest(plainJson);
+    /** DN→UN 被动响应。 */
+    public VppUnSmCryptoUtil.EncryptedPayload prepareOutbound(String plainJson) {
+        return prepareRequest(plainJson);
     }
 
-    /** DN→UN 响应签名 */
-    public String signOutbound(String cipherBase64) {
-        return signRequest(cipherBase64);
+    private void assertInboundVerified(String cipherBase64, String signBase64) {
+        if (!verifyInbound(cipherBase64, signBase64)) {
+            throw new BusinessException("UN 请求验签失败");
+        }
+    }
+
+    private VppUnProperties requireCryptoProperties() {
+        VppUnProperties properties = currentProperties();
+        if (!isCryptoActive(properties)) {
+            throw new BusinessException("运管平台国密加解密未启用或密钥未配置");
+        }
+        return properties;
+    }
+
+    private VppUnProperties currentProperties() {
+        return propertiesRegistry.getCurrent();
+    }
+
+    private boolean isCryptoActive(VppUnProperties properties) {
+        return properties != null
+                && Boolean.TRUE.equals(properties.getCryptoEnabled())
+                && StringUtils.hasText(properties.getUnPublicKey())
+                && StringUtils.hasText(properties.getDnPrivateKey());
     }
 }

+ 232 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/crypto/VppUnSmCryptoUtil.java

@@ -0,0 +1,232 @@
+package com.usky.vpp.crypto;
+
+import org.bouncycastle.crypto.engines.SM2Engine;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.crypto.params.ParametersWithRandom;
+import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.SM2;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.Signature;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+
+/**
+ * 运管平台 UN/DN 国密工具(SM2 加解密 + SM3withSM2 签名/验签)。
+ * <p>加解密优先使用 BC {@code Cipher("SM2")}(与联调 SM3Util 一致,C1C2C3);签名/验签针对密文字节。</p>
+ */
+public final class VppUnSmCryptoUtil {
+
+    private static final String EC = "EC";
+    private static final String BC = BouncyCastleProvider.PROVIDER_NAME;
+    private static final String SM2 = "SM2";
+    private static final String SIGNATURE_ALGORITHM = "SM3withSM2";
+
+    static {
+        if (Security.getProvider(BC) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+    }
+
+    private VppUnSmCryptoUtil() {
+    }
+
+    public static final class EncryptedPayload {
+        private final String cipherBase64;
+        private final String signBase64;
+
+        public EncryptedPayload(String cipherBase64, String signBase64) {
+            this.cipherBase64 = cipherBase64;
+            this.signBase64 = signBase64;
+        }
+
+        public String getCipherBase64() {
+            return cipherBase64;
+        }
+
+        public String getSignBase64() {
+            return signBase64;
+        }
+    }
+
+    /** DN 请求 UN:UN 公钥加密 + DN 私钥对密文字节签名。 */
+    public static EncryptedPayload encryptAndSignRequest(String plainJson,
+                                                         String unPublicKeyBase64,
+                                                         String dnPrivateKeyBase64) throws Exception {
+        byte[] cipherBytes = encryptByPublicKey(plainJson, unPublicKeyBase64);
+        String cipherBase64 = Base64.getEncoder().encodeToString(cipherBytes);
+        String signBase64 = signCipherBytes(cipherBytes, dnPrivateKeyBase64);
+        return new EncryptedPayload(cipherBase64, signBase64);
+    }
+
+    /** DN 解析 UN 响应:UN 公钥验签密文字节 + DN 私钥解密。 */
+    public static String verifyAndDecryptResponse(String cipherBase64,
+                                                  String signBase64,
+                                                  String unPublicKeyBase64,
+                                                  String dnPrivateKeyBase64) throws Exception {
+        byte[] cipherBytes = decodeBase64(cipherBase64);
+        if (!verifyCipherBytes(cipherBytes, signBase64, unPublicKeyBase64)) {
+            throw new IllegalStateException("sign verify fail");
+        }
+        return new String(decryptByPrivateKey(cipherBytes, dnPrivateKeyBase64), StandardCharsets.UTF_8);
+    }
+
+    /** 仅 SM2 解密(已验签场景)。 */
+    public static String decryptByDnPrivateKey(String cipherBase64, String dnPrivateKeyBase64) throws Exception {
+        byte[] cipherBytes = decodeBase64(cipherBase64);
+        return new String(decryptByPrivateKey(cipherBytes, dnPrivateKeyBase64), StandardCharsets.UTF_8);
+    }
+
+    public static String signCipherBytes(byte[] cipherBytes, String privateKeyBase64) throws Exception {
+        return sign(cipherBytes, privateKeyBase64);
+    }
+
+    public static boolean verifyCipherBytes(byte[] cipherBytes, String signBase64, String publicKeyBase64)
+            throws Exception {
+        return verify(cipherBytes, publicKeyBase64, signBase64);
+    }
+
+    private static String sign(byte[] data, String privateKeyBase64) throws Exception {
+        try {
+            PrivateKey privateKey = loadPrivateKey(privateKeyBase64);
+            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, BC);
+            signature.initSign(privateKey);
+            signature.update(data);
+            return Base64.getEncoder().encodeToString(signature.sign());
+        } catch (Exception ex) {
+            SM2 sm2 = SmUtil.sm2(privateKeyBase64.trim(), null);
+            return Base64.getEncoder().encodeToString(sm2.sign(data));
+        }
+    }
+
+    private static boolean verify(byte[] data, String publicKeyBase64, String signBase64) throws Exception {
+        try {
+            PublicKey publicKey = loadPublicKey(publicKeyBase64);
+            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, BC);
+            signature.initVerify(publicKey);
+            signature.update(data);
+            return signature.verify(decodeBase64(signBase64));
+        } catch (Exception ex) {
+            SM2 sm2 = SmUtil.sm2(null, publicKeyBase64.trim());
+            return sm2.verify(data, decodeBase64(signBase64));
+        }
+    }
+
+    /** 与 SM3Util.encryptByPublicKey 一致:BC Cipher SM2。 */
+    private static byte[] encryptByPublicKey(String data, String publicKeyBase64) throws Exception {
+        Exception last = null;
+        try {
+            return encryptByPublicKeyCipher(data, publicKeyBase64);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            return encryptByPublicKeyEngine(data, publicKeyBase64, SM2Engine.Mode.C1C2C3);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            return encryptByPublicKeyEngine(data, publicKeyBase64, SM2Engine.Mode.C1C3C2);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            SM2 sm2 = SmUtil.sm2(null, publicKeyBase64.trim());
+            return sm2.encrypt(data.getBytes(StandardCharsets.UTF_8), KeyType.PublicKey);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        throw last != null ? last : new IllegalStateException("SM2 加密失败");
+    }
+
+    /** 与 SM3Util.decryptByPrivateKey 一致,并兼容 C1C3C2 密文。 */
+    private static byte[] decryptByPrivateKey(byte[] cipherBytes, String privateKeyBase64) throws Exception {
+        Exception last = null;
+        try {
+            return decryptByPrivateKeyCipher(cipherBytes, privateKeyBase64);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            return decryptByPrivateKeyEngine(cipherBytes, privateKeyBase64, SM2Engine.Mode.C1C2C3);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            return decryptByPrivateKeyEngine(cipherBytes, privateKeyBase64, SM2Engine.Mode.C1C3C2);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        try {
+            SM2 sm2 = SmUtil.sm2(privateKeyBase64.trim(), null);
+            return sm2.decrypt(cipherBytes, KeyType.PrivateKey);
+        } catch (Exception ex) {
+            last = ex;
+        }
+        throw last != null ? last : new IllegalStateException("SM2 解密失败");
+    }
+
+    private static byte[] encryptByPublicKeyCipher(String data, String publicKeyBase64) throws Exception {
+        PublicKey publicKey = loadPublicKey(publicKeyBase64);
+        Cipher cipher = Cipher.getInstance(SM2, BC);
+        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+        return cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static byte[] decryptByPrivateKeyCipher(byte[] cipherBytes, String privateKeyBase64) throws Exception {
+        PrivateKey privateKey = loadPrivateKey(privateKeyBase64);
+        Cipher cipher = Cipher.getInstance(SM2, BC);
+        cipher.init(Cipher.DECRYPT_MODE, privateKey);
+        return cipher.doFinal(cipherBytes);
+    }
+
+    private static byte[] encryptByPublicKeyEngine(String data, String publicKeyBase64, SM2Engine.Mode mode)
+            throws Exception {
+        PublicKey publicKey = loadPublicKey(publicKeyBase64);
+        ECPublicKeyParameters publicKeyParameters =
+                (ECPublicKeyParameters) ECUtil.generatePublicKeyParameter(publicKey);
+        SM2Engine engine = new SM2Engine(mode);
+        engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
+        byte[] input = data.getBytes(StandardCharsets.UTF_8);
+        return engine.processBlock(input, 0, input.length);
+    }
+
+    private static byte[] decryptByPrivateKeyEngine(byte[] cipherBytes, String privateKeyBase64, SM2Engine.Mode mode)
+            throws Exception {
+        PrivateKey privateKey = loadPrivateKey(privateKeyBase64);
+        ECPrivateKeyParameters privateKeyParameters =
+                (ECPrivateKeyParameters) ECUtil.generatePrivateKeyParameter(privateKey);
+        SM2Engine engine = new SM2Engine(mode);
+        engine.init(false, privateKeyParameters);
+        return engine.processBlock(cipherBytes, 0, cipherBytes.length);
+    }
+
+    private static PublicKey loadPublicKey(String publicKeyBase64) throws Exception {
+        byte[] keyBytes = decodeBase64(publicKeyBase64);
+        KeyFactory keyFactory = KeyFactory.getInstance(EC, BC);
+        return keyFactory.generatePublic(new X509EncodedKeySpec(keyBytes));
+    }
+
+    private static PrivateKey loadPrivateKey(String privateKeyBase64) throws Exception {
+        byte[] keyBytes = decodeBase64(privateKeyBase64);
+        KeyFactory keyFactory = KeyFactory.getInstance(EC, BC);
+        return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
+    }
+
+    private static byte[] decodeBase64(String value) {
+        return Base64.getDecoder().decode(value.trim());
+    }
+}

+ 19 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppBiddingConfig.java

@@ -34,18 +34,33 @@ public class VppBiddingConfig implements Serializable {
     private String attachmentUrl;
     @TableField("attachment_name")
     private String attachmentName;
+
+    /** 1 中长期响应
+     *  2 日内响应
+     *  3 快速响应
+     * */
     @TableField("bidding_mode")
     private Integer biddingMode;
+
     @TableField("typical_coefficient")
     private BigDecimal typicalCoefficient;
-    @TableField("advance_close_hours")
-    private Integer advanceCloseHours;
+
+    /**
+     * 提前通知最小小时数(下限,不含本值,如 8 表示 >8)
+     */
+    @TableField("notice_min_hours")
+    private BigDecimal noticeMinHours;
+
+    /**
+     * 提前通知最大小时数(上限,含本值,如 24 表示 ≤24)
+     */
+    @TableField("notice_max_hours")
+    private BigDecimal noticeMaxHours;
+
     @TableField("max_price")
     private BigDecimal maxPrice;
     @TableField("min_price")
     private BigDecimal minPrice;
-    @TableField("site_ids")
-    private String siteIds;
     @TableField("tenant_id")
     private Integer tenantId;
 

+ 16 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java

@@ -40,6 +40,10 @@ public class VppContract implements Serializable {
     @NotNull(message = "合同模板ID不能为空!")
     private Long templateId;
 
+    /** 关联合同ID(居间/普通一对一) */
+    @TableField("related_contract_id")
+    private Long relatedContractId;
+
     /**
      * 1购售电 2需求响应合作 3聚合代理 4服务代理 5居民充电桩 6自有资产
      */
@@ -51,6 +55,18 @@ public class VppContract implements Serializable {
     @NotNull(message = "合同名称不能为空!")
     private String contractName;
 
+    /** 甲方 */
+    @TableField("party_a")
+    private String partyA;
+
+    /** 乙方 */
+    @TableField("party_b")
+    private String partyB;
+
+    /** 合同金额 */
+    @TableField("contract_amount")
+    private BigDecimal contractAmount;
+
     /**
      * 0草稿 1审核中 2待生效 3已生效 4已驳回 5已到期 6已终止
      */

+ 7 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractTemplate.java

@@ -38,6 +38,13 @@ public class VppContractTemplate implements Serializable {
     @TableField("contract_type")
     @NotNull(message = "合同类型不能为空!")
     private Integer contractType;
+
+    /**
+     * 模板分类:1居间/层间 2普通/直网(用于一对一关联校验)
+     */
+    @TableField("template_kind")
+    private Integer templateKind;
+
     private String version;
     @TableField("file_url")
     @NotBlank(message = "文件路径不能为空!")

+ 24 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java

@@ -78,6 +78,30 @@ public class VppCustomer implements Serializable {
      */
     private String province;
 
+    /**
+     * 是否集团内用户 0否 1是
+     */
+    @TableField("is_group_user")
+    private Integer isGroupUser;
+
+    /**
+     * 是否绿电 0否 1是
+     */
+    @TableField("is_green_power")
+    private Integer isGreenPower;
+
+    /**
+     * 是否合作开发用户 0否 1是
+     */
+    @TableField("is_coop_dev_user")
+    private Integer isCoopDevUser;
+
+    /**
+     * 合作开发单位名称
+     */
+    @TableField("coop_partner_name")
+    private String coopPartnerName;
+
     /**
      * 市
      */

+ 5 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java

@@ -42,6 +42,11 @@ public class VppDevice implements Serializable {
     private LocalDateTime lastOnlineAt;
     @TableField("gateway_id")
     private String gatewayId;
+    /**
+     * 运管资源ID,全局唯一
+     */
+    @TableField("un_resource_id")
+    private String unResourceId;
     private String remark;
     @TableField("tenant_id")
     private Integer tenantId;

+ 5 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
@@ -56,6 +57,10 @@ public class VppDrEvent implements Serializable {
     private String attachmentName;
     @TableField("subsidy_price")
     private BigDecimal subsidyPrice;
+    /** 下浮系数(必填) */
+    @TableField("floating_coefficient")
+    private BigDecimal floatingCoefficient;
+    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
     @TableField("event_status")
     private Integer eventStatus;
     @TableField("raw_payload")

+ 1 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java

@@ -68,6 +68,7 @@ public class VppDrInvitation implements Serializable {
     private Integer isWinningBid;
     @TableField("reply_status")
     private Integer replyStatus;
+    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
     @TableField("response_status")
     private Integer responseStatus;
     @TableField("sms_notify_status")

+ 32 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java

@@ -37,6 +37,14 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("site_name")
     private String siteName;
 
+    /** 企业/客户ID */
+    @TableField("customer_id")
+    private Long customerId;
+
+    /** 企业名称 */
+    @TableField("customer_name")
+    private String customerName;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @TableField("event_start_time")
     private LocalDateTime eventStartTime;
@@ -51,15 +59,39 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("subsidy_price")
     private BigDecimal subsidyPrice;
 
+    /** 预估响应容量(兼容旧字段,等同平台实际响应量) */
     @TableField("estimated_capacity_kw")
     private BigDecimal estimatedCapacityKw;
 
+    /** 实际出清(兼容旧字段,等同申报出清量) */
     @TableField("cleared_capacity_kw")
     private BigDecimal clearedCapacityKw;
 
+    /** 实际响应量-电网 kW */
+    @TableField("actual_response_grid_kw")
+    private BigDecimal actualResponseGridKw;
+
+    /** 实际响应量-平台 kW */
+    @TableField("actual_response_platform_kw")
+    private BigDecimal actualResponsePlatformKw;
+
+    /** 申报出清量 kW */
+    @TableField("declared_cleared_kw")
+    private BigDecimal declaredClearedKw;
+
+    /** 分成比例 0~1 */
+    @TableField("share_ratio")
+    private BigDecimal shareRatio;
+
+    /** 按完成率(比例)命中响应量偏差价格调整表得到的价格调整系数。 */
+    @TableField("price_adjustment_coefficient")
+    private BigDecimal priceAdjustmentCoefficient;
+
+    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
     @TableField("completion_rate")
     private BigDecimal completionRate;
 
+    /** 偏差率(百分比)= (1 − 完成率) × 100 */
     @TableField("deviation_rate")
     private BigDecimal deviationRate;
 

+ 64 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResponseDeviationPrice.java

@@ -0,0 +1,64 @@
+package com.usky.vpp.domain;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 响应量偏差价格调整表。
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("vpp_response_deviation_price")
+public class VppResponseDeviationPrice implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /** 响应量系数下限,null 表示无下界。 */
+    @TableField(value = "coeff_lower", updateStrategy = FieldStrategy.IGNORED)
+    private BigDecimal coeffLower;
+
+    /** 响应量系数上限,null 表示无上界。 */
+    @TableField(value = "coeff_upper", updateStrategy = FieldStrategy.IGNORED)
+    private BigDecimal coeffUpper;
+
+    /** 下限是否闭区间。 */
+    @TableField("lower_inclusive")
+    private Boolean lowerInclusive;
+
+    /** 上限是否闭区间。 */
+    @TableField("upper_inclusive")
+    private Boolean upperInclusive;
+
+    /** 命中区间后用于补贴计算的价格调整系数。 */
+    @TableField("price_adjustment_coefficient")
+    private BigDecimal priceAdjustmentCoefficient;
+
+    private String remark;
+
+    @TableField("tenant_id")
+    private Integer tenantId;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private String createdBy;
+    @TableField("updated_by")
+    private String updatedBy;
+    @TableField("delete_flag")
+    private Integer deleteFlag;
+    @TableField("deleted_at")
+    private LocalDateTime deletedAt;
+}

+ 85 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppTenantConfig.java

@@ -0,0 +1,85 @@
+package com.usky.vpp.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * vpp_tenant_config 虚拟电厂租户配置
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("vpp_tenant_config")
+public class VppTenantConfig implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("tenant_id")
+    private Integer tenantId;
+
+    @TableField("dn_id")
+    private String dnId;
+
+    @TableField("dn_name")
+    private String dnName;
+
+    @TableField("base_url")
+    private String baseUrl;
+
+    @TableField("transport_address")
+    private String transportAddress;
+
+    @TableField("un_public_key")
+    private String unPublicKey;
+
+    @TableField("dn_public_key")
+    private String dnPublicKey;
+
+    @TableField("dn_private_key")
+    private String dnPrivateKey;
+
+    @TableField(value = "token",exist = false)
+    private String token;
+
+    @TableField("poll_interval_sec")
+    private Integer pollIntervalSec;
+
+    @TableField("outbound_enabled")
+    private Boolean outboundEnabled;
+
+    @TableField("poll_enabled")
+    private Boolean pollEnabled;
+
+    @TableField("crypto_enabled")
+    private Boolean cryptoEnabled;
+
+    @TableField("price_down_coeff")
+    private String priceDownCoeff;
+
+    @TableField("auto_ack_clearing")
+    private Boolean autoAckClearing;
+
+    @TableField("auto_register_on_startup")
+    private Boolean autoRegisterOnStartup;
+
+    @TableField("create_by")
+    private String createBy;
+
+    @TableField("create_time")
+    private LocalDateTime createTime;
+
+    @TableField("update_by")
+    private String updateBy;
+
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+}

+ 0 - 47
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnBootstrapRunner.java

@@ -1,47 +0,0 @@
-package com.usky.vpp.job;
-
-import com.usky.vpp.config.VppUnProperties;
-import com.usky.vpp.service.VppUnIntegrationService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.ApplicationArguments;
-import org.springframework.boot.ApplicationRunner;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-
-/**
- * 启动时自动向 UN 注册(可选)
- */
-@Component
-@ConditionalOnProperty(name = "vpp.un.auto-register-on-startup", havingValue = "true")
-public class VppUnBootstrapRunner implements ApplicationRunner {
-
-    private static final Logger log = LoggerFactory.getLogger(VppUnBootstrapRunner.class);
-
-    @Autowired
-    private VppUnProperties properties;
-
-    @Autowired
-    private VppUnIntegrationService integrationService;
-
-    @Override
-    public void run(ApplicationArguments args) {
-        if (!properties.isOutboundActive()) {
-            log.warn("auto-register-on-startup 已开启但 outbound 未激活,跳过");
-            return;
-        }
-        try {
-            integrationService.refreshToken();
-            if (!StringUtils.hasText(properties.getRegistrationId())) {
-                integrationService.register();
-                log.info("启动注册 UN 完成");
-            } else {
-                log.info("已有 registrationID={},跳过启动注册", properties.getRegistrationId());
-            }
-        } catch (Exception ex) {
-            log.warn("启动注册 UN 失败: {}", ex.getMessage());
-        }
-    }
-}

+ 0 - 36
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnPollScheduler.java

@@ -1,36 +0,0 @@
-package com.usky.vpp.job;
-
-import com.usky.vpp.config.VppUnProperties;
-import com.usky.vpp.service.VppUnIntegrationService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-/**
- * 定时 Poll UN 拉取事件/反馈
- */
-@Component
-public class VppUnPollScheduler {
-
-    private static final Logger log = LoggerFactory.getLogger(VppUnPollScheduler.class);
-
-    @Autowired
-    private VppUnProperties properties;
-
-    @Autowired
-    private VppUnIntegrationService integrationService;
-
-    //@Scheduled(fixedDelayString = "#{${vpp.un.poll-interval-sec:10} * 1000}")
-    public void pollUn() {
-        if (!properties.isPollActive()) {
-            return;
-        }
-        try {
-            integrationService.pollOnce();
-        } catch (Exception ex) {
-            log.warn("UN Poll 失败: {}", ex.getMessage());
-        }
-    }
-}

+ 39 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnPropertiesLoader.java

@@ -0,0 +1,39 @@
+package com.usky.vpp.job;
+
+import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.config.VppUnPropertiesRegistry;
+import com.usky.vpp.service.VppUnRegisterTaskService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * 服务启动时从 vpp_tenant_config 加载 UN 配置至内存,并按需自动注册 UN
+ */
+@Component
+@Order(100)
+public class VppUnPropertiesLoader implements ApplicationRunner {
+
+    @Autowired
+    private VppUnPropertiesRegistry propertiesRegistry;
+
+    @Autowired
+    private VppUnRegisterTaskService unRegisterTaskService;
+
+    @Override
+    public void run(ApplicationArguments args) {
+        propertiesRegistry.refreshAll();
+        autoRegisterOnStartup();
+    }
+
+    private void autoRegisterOnStartup() {
+        for (VppUnProperties properties : propertiesRegistry.getAll()) {
+            if (!Boolean.TRUE.equals(properties.getAutoRegisterOnStartup()) || !properties.isOutboundActive()) {
+                continue;
+            }
+            unRegisterTaskService.registerUn(properties.getTenantId());
+        }
+    }
+}

+ 12 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppResponseDeviationPriceMapper.java

@@ -0,0 +1,12 @@
+package com.usky.vpp.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.vpp.domain.VppResponseDeviationPrice;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 响应量偏差价格调整表 Mapper。
+ */
+@Repository
+public interface VppResponseDeviationPriceMapper extends CrudMapper<VppResponseDeviationPrice> {
+}

+ 10 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppTenantConfigMapper.java

@@ -0,0 +1,10 @@
+package com.usky.vpp.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.vpp.domain.VppTenantConfig;
+
+/**
+ * vpp_tenant_config Mapper
+ */
+public interface VppTenantConfigMapper extends CrudMapper<VppTenantConfig> {
+}

+ 1 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppAliyunSmsService.java

@@ -10,7 +10,7 @@ public interface VppAliyunSmsService {
      *
      * @param phone        手机号
      * @param templateCode 模板 CODE
-     * @param templateParam JSON 模板变量,如 {"meet":"夏季削峰","time":"2026-07-08 14:00:00","room":"某某公司"}
+     * @param templateParam JSON 模板变量,如 {"name":"某某公司","responsetype":"日前","time":"2026-07-08 14:00:00~2026-07-08 15:00:00","number":"100","operationtype":"参与削峰"}
      */
     void sendTemplateSms(String phone, String templateCode, String templateParam);
 }

+ 13 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppBaselineService.java

@@ -19,10 +19,22 @@ public interface VppBaselineService {
      */
     SiteBaselineVO getSiteBaseline(Long siteId, String responseStartTime);
 
+    /**
+     * 查询站点基线负荷曲线(可指定参考天数、实测截止时间)。
+     *
+     * @param siteId            站点 ID
+     * @param responseStartTime 响应开始时间
+     * @param baselineDays      参考天数,仅 3 或 5;为空则按工作日/非工作日自动选择
+     * @param actualEndTime     当日实测查询截止时间;为空则截止到响应开始
+     */
+    SiteBaselineVO getSiteBaseline(Long siteId, String responseStartTime,
+                                   Integer baselineDays, String actualEndTime);
+
     /**
      * 查询站点响应申报容量。
      *
-     * <p>响应时段内同时间段历史负荷均值 × 修正系数 K × 执行时段小时数 / 2。</p>
+     * <p>响应时段内同时间段历史负荷均值 × 执行时段小时数 / 2。</p>
+     * <p>典型历史日选取:工作日 5 天 / 非工作日 3 天;剔除响应当日、历史响应日及响应日 &gt;= 今日。</p>
      *
      * @param siteId            站点 ID
      * @param responseStartTime 响应开始时间,格式 yyyy-MM-dd HH:mm:ss

+ 18 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractService.java

@@ -1,6 +1,8 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.VppContractExpireStatVO;
+import com.usky.vpp.service.vo.VppContractOverviewVO;
 import com.usky.vpp.service.vo.VppContractRequestVO;
 import com.usky.vpp.service.vo.VppContractResponseVO;
 
@@ -35,12 +37,16 @@ public interface VppContractService {
      * @param templateId     模板ID
      * @param contractType   合同类型
      * @param customerId     客户ID
+     * @param customerName   客户名称(模糊匹配)
+     * @param signDateStart  签订时间起始
+     * @param signDateEnd    签订时间截止
      * @param contractStatus 合同状态
      * @param pageNum        页码
      * @param pageSize       页大小
      */
     CommonPage<VppContractResponseVO> page(Long id, LocalDate signDateStart, LocalDate signDateEnd,
                                            Long templateId, Integer contractType, Long customerId,
+                                           String customerName, LocalDate signDateRangeStart, LocalDate signDateRangeEnd,
                                            Integer contractStatus, Integer pageNum, Integer pageSize);
 
     /**
@@ -52,4 +58,16 @@ public interface VppContractService {
      * 下载合同文件
      */
     void download(Long id, HttpServletResponse response);
+
+    /**
+     * 按客户最新合同统计已到期 / 即将到期(近三个月)数量
+     * <p>租户隔离;每个客户仅取创建时间最新的一份合同,再按到期日分类统计数量。
+     * 列表请走分页接口,按 contractStatus=5(已到期)/ 7(即将到期)查询。</p>
+     */
+    VppContractExpireStatVO expireStat();
+
+    /**
+     * 合同概况:甲方、乙方、签订日期、合同金额、起止时间、生效状态
+     */
+    VppContractOverviewVO overview(Long id);
 }

+ 15 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrMonitorService.java

@@ -3,6 +3,7 @@ package com.usky.vpp.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
+import com.usky.vpp.service.vo.DrMonitorSiteResourceVO;
 import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 
 import java.util.List;
@@ -14,22 +15,31 @@ import java.util.Map;
 public interface VppDrMonitorService {
 
     /**
-     * 顶部 KPI
+     * 顶部 KPI:按时间区间内执行中/已结束事件关联邀约统计
      *
      * @param startDate 开始日期 yyyy-MM-dd
      * @param endDate   结束日期 yyyy-MM-dd
-     * @param siteIds   站点 ID(必填,可多个);先查站点下 is_support_peak=1 资源点再平均
+     * @param siteIds   站点 ID(必填,可多个)
      */
     DrMonitorSummaryVO getSummary(String startDate, String endDate, List<Long> siteIds);
 
     /**
-     * 响应记录分页(按事件表字段;仅可调峰资源点参与过的事件
+     * 响应记录分页(查询口径与 summary 一致,按邀约展开
      */
     CommonPage<DrMonitorRecordVO> pageRecords(String startDate, String endDate,
                                               List<Long> siteIds, Map<String, Object> params);
 
     /**
-     * 折线图:基线/实际负荷按可调峰资源点加权平均;申报/实际响应按资源点平均
+     * 折线图:按邀约主键取站点与响应时间,复用 getSiteBaseline 全天曲线,并带上实际响应容量
+     *
+     * @param invitationId 邀约表主键 id
+     */
+    DrMonitorCurveVO getCurve(Long invitationId);
+
+    /**
+     * 按响应事件查询邀约站点及其可调峰资源点,以站点分组返回。
+     *
+     * @param eventId 响应事件主键 id(vpp_dr_event.id)
      */
-    DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds);
+    List<DrMonitorSiteResourceVO> listEventSitePeakResources(Long eventId);
 }

+ 6 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java

@@ -7,6 +7,7 @@ import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.service.vo.DrClearingRequest;
 import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
+import com.usky.vpp.service.vo.DrEventStatusRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrParticipateRequest;
 import com.usky.vpp.service.vo.DrStrategyRequest;
@@ -27,6 +28,11 @@ public interface VppDrService {
 
     void updateEvent(Long id, DrEventRequest request);
 
+    /**
+     * 修改事件状态(独立接口,不走编辑事件)。校验租户后仅允许 0待参与 → 1已申报。
+     */
+    void updateEventStatus(DrEventStatusRequest request);
+
     void deleteEvent(Long id);
 
     Object assessCapability(Long eventId);

+ 25 - 9
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java

@@ -1,6 +1,8 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.DrSubsidyPredictionCalculateRequest;
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
 
 /**
@@ -10,20 +12,34 @@ public interface VppDrSubsidyPredictionService {
 
     /**
      * 分页查询补贴金额预测
-     *
-     * @param siteName   站点名称(模糊匹配)
-     * @param startTime  响应开始时间起始
-     * @param endTime    响应开始时间截止
-     * @param current    页码
-     * @param size       页大小
      */
-    CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
+    CommonPage<DrSubsidyPredictionVO> page(String siteName, String customerName,
+                                           String startTime, String endTime,
                                            Integer current, Integer size);
 
+    /**
+     * 试算补贴预测(不落库)。
+     * <p>入参为邀约编号 + 电网实际响应容量;时长/平台响应量/申报出清量/补贴标准等从邀约与事件查询后计算。</p>
+     */
+    DrSubsidyPredictionVO calculate(DrSubsidyPredictionCalculateRequest request);
+
+    /**
+     * 新增补贴预测:按前端传参直接入库,服务端不再重算。
+     */
+    Boolean create(DrSubsidyPredictionRequest request);
+
+    /**
+     * 修改补贴预测:按前端传参直接入库,服务端不再重算。
+     */
+    Boolean update(DrSubsidyPredictionRequest request);
+
+    /**
+     * 删除补贴预测
+     */
+    Boolean delete(Long id);
+
     /**
      * 根据事件ID生成/更新补贴预测数据
-     *
-     * @param drEventId 需求响应事件ID
      */
     void generateByEventId(Long drEventId);
 }

+ 1 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceBoardService.java

@@ -12,7 +12,7 @@ import java.util.Map;
 public interface VppResourceBoardService {
 
     /**
-     * 资源看板列表(模拟实时运行数据)
+     * 资源看板列表(TSDB 实时运行数据)
      */
     CommonPage<ResourceBoardItemVO> pageBoard(Map<String, Object> params);
 

+ 2 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceOverviewService.java

@@ -18,7 +18,7 @@ public interface VppResourceOverviewService {
     List<ResourceTypeStatVO> getTypeStats();
 
     /**
-     * 今日电能概况(总发电量、总用电量、绿电消纳比例,模拟数据
+     * 今日电能概况(总发电量、总用电量、绿电消纳比例,TSDB 今日 eday/epp 汇总
      */
     EnergyTodayVO getEnergyToday();
 
@@ -28,7 +28,7 @@ public interface VppResourceOverviewService {
     ResourceOverviewRealtimeCurveVO getRealtimeCurves(String startTime, String endTime);
 
     /**
-     * 实时告警统计(紧急/重要/一般,含跳转路径,模拟数据
+     * 实时告警统计(紧急/重要/一般,含跳转告警列表路径)
      */
     List<AlarmLevelStatVO> getAlarmStats();
 }

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff