Forráskód Böngészése

Merge branch 'master' into vpp-zyj

james 1 hete
szülő
commit
f28efed70d
87 módosított fájl, 2258 hozzáadás és 962 törlés
  1. 1 0
      service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmNoticeResultServiceImpl.java
  2. 82 0
      service-eg/service-eg-biz/pom.xml
  3. 10 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbConfiguration.java
  4. 75 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbProperties.java
  5. 17 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/api/ServiceEgTaskApi.java
  6. 97 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceAdbController.java
  7. 0 86
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFace.java
  8. 0 32
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFaceDevice.java
  9. 0 7
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceMapper.java
  10. 0 18
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceDeviceMapper.java
  11. 0 18
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceMapper.java
  12. 6 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java
  13. 67 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttOutboundConfig.java
  14. 81 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttProperties.java
  15. 20 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceAdbService.java
  16. 19 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceRemoteUpgradeMqttService.java
  17. 325 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceAdbServiceImpl.java
  18. 173 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceRemoteUpgradeMqttServiceImpl.java
  19. 34 7
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.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. 2 1
      service-fire/service-fire-biz/src/main/java/com/usky/fire/service/config/SyncPatrolInspectionEvent.java
  34. 1 1
      service-fire/service-fire-biz/src/main/java/com/usky/fire/service/impl/PatrolInspectionEventServiceImpl.java
  35. 1 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/config/DingTalkAndMessage.java
  36. 54 49
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/BaseAlarmServiceImpl.java
  37. 2 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/PmWorkReportServiceImpl.java
  38. 0 10
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/FaceLog.java
  39. 0 50
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/aspect/FaceAspect.java
  40. 23 2
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingDeviceController.java
  41. 0 182
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceController.java
  42. 0 60
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceVefLogController.java
  43. 6 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevice.java
  44. 25 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevicePersonBind.java
  45. 0 75
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingFaceVefLog.java
  46. 48 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/SysPerson.java
  47. 29 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingDevicePersonBindMapper.java
  48. 0 6
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceMapper.java
  49. 0 16
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceVefLogMapper.java
  50. 21 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/SysPersonMapper.java
  51. 0 19
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/repository/MeetingInfoRepository.java
  52. 12 1
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingDeviceService.java
  53. 0 16
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingFaceVefLogService.java
  54. 192 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingDeviceServiceImpl.java
  55. 0 20
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFaceVefLogServiceImpl.java
  56. 0 1
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFileServiceImpl.java
  57. 23 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonQueryVO.java
  58. 27 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonVO.java
  59. 18 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindPersonRequest.java
  60. 34 0
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingDevicePersonBindMapper.xml
  61. 0 15
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceMapper.xml
  62. 0 16
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceVefLogMapper.xml
  63. 18 0
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/SysPersonMapper.xml
  64. 3 0
      service-oa/service-oa-biz/src/main/java/com/usky/oa/service/impl/OaApprovalServiceImpl.java
  65. 1 0
      service-oa/service-oa-biz/src/main/java/com/usky/oa/service/impl/OaFbdDocumentServiceImpl.java
  66. 2 1
      service-oa/service-oa-biz/src/main/java/com/usky/oa/service/utils/OaSendMessageCenter.java
  67. 1 0
      service-pm/service-pm-biz/src/main/java/com/usky/pm/service/config/DingTalkAndMessage.java
  68. 2 0
      service-pm/service-pm-biz/src/main/java/com/usky/pm/service/impl/PmWorkReportServiceImpl.java
  69. 19 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java
  70. 19 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppBiddingConfig.java
  71. 1 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  72. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppAliyunSmsService.java
  73. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractService.java
  74. 18 74
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBiddingConfigServiceImpl.java
  75. 65 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java
  76. 15 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractTemplateServiceImpl.java
  77. 26 53
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java
  78. 101 19
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java
  79. 12 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/BiddingConfigRequest.java
  80. 12 7
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/BiddingConfigVO.java
  81. 20 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractExpireStatVO.java
  82. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractResponseVO.java
  83. 11 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppContractStatusHelper.java
  84. 13 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppFieldValidator.java
  85. 39 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseMonitorHelper.java
  86. 69 13
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppSmsTemplateHelper.java
  87. 2 1
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

+ 1 - 0
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmNoticeResultServiceImpl.java

@@ -209,6 +209,7 @@ public class BaseAlarmNoticeResultServiceImpl extends AbstractCrudService<BaseAl
         mceRequestVO.setUserIds(Collections.singletonList(sysUser.getUserId()));
         mceRequestVO.setUserName(sysUser.getUserName());
         mceRequestVO.setId(baseAlarm.getId());
+        mceRequestVO.setTenantId(baseAlarm.getTenantId());
         return mceRequestVO;
     }
 

+ 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) {

+ 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>

+ 2 - 1
service-fire/service-fire-biz/src/main/java/com/usky/fire/service/config/SyncPatrolInspectionEvent.java

@@ -17,7 +17,7 @@ public class SyncPatrolInspectionEvent {
     private RemoteDictService remoteDictService;
 
     @Async("asyncServiceExecutor")
-    public void sendAsyncMessage(Integer eventId,String eventName,Integer eventType){
+    public void sendAsyncMessage(Integer tenantId,Integer eventId,String eventName,Integer eventType){
         String eventTypeName = remoteDictService.selectNameById("patrol_event_type",
                 eventType.toString());
         JsonObject jsonObject = new JsonObject();
@@ -26,6 +26,7 @@ public class SyncPatrolInspectionEvent {
         jsonObject.addProperty("infoType",2);
         jsonObject.addProperty("userName", SecurityUtils.getUsername());
         jsonObject.addProperty("id",eventId);
+        jsonObject.addProperty("tenantId",tenantId);
         remoteMceService.addMce(jsonObject.toString());
     }
 }

+ 1 - 1
service-fire/service-fire-biz/src/main/java/com/usky/fire/service/impl/PatrolInspectionEventServiceImpl.java

@@ -134,7 +134,7 @@ public class PatrolInspectionEventServiceImpl extends AbstractCrudService<Patrol
 
         this.save(patrolInspectionEvent);
 
-//        syncPatrolInspectionEvent.sendAsyncMessage(patrolInspectionEvent.getId(),patrolInspectionEvent.getEventName(),patrolInspectionEvent.getEventType());
+//        syncPatrolInspectionEvent.sendAsyncMessage(SecurityUtils.getTenantId(),patrolInspectionEvent.getId(),patrolInspectionEvent.getEventName(),patrolInspectionEvent.getEventType());
 
     }
 

+ 1 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/config/DingTalkAndMessage.java

@@ -330,6 +330,7 @@ public class DingTalkAndMessage {
         jsonObject.put("id", newReport.getId());
         jsonObject.put("infoTypeName", INFO_TITLE);
         jsonObject.put("userName", newReport.getCreateBy());
+        jsonObject.put("tenantId", newReport.getTenantId());
         if (!userId.isEmpty()) {
             jsonObject.put("userIds", userId);
         }

+ 54 - 49
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/BaseAlarmServiceImpl.java

@@ -205,6 +205,7 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
         }
 
         IPage<BaseAlarm> page = new Page<>(baseAlarmListVO.getCurrent(), baseAlarmListVO.getSize());
+        List<BaseAlarm> records = new ArrayList<>();
         if(CollectionUtils.isNotEmpty(devIdList)){
             LambdaQueryWrapper<BaseAlarm> queryWrapper = Wrappers.lambdaQuery();
             queryWrapper.in(BaseAlarm::getDeviceId,devIdList)
@@ -227,65 +228,27 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
             }else {
                 queryWrapper.orderByDesc(BaseAlarm::getId);
             }
-            page = this.page(page,queryWrapper);
-//        List<BaseAlarm> records = this.list(queryWrapper);
-            if(page.getRecords().size()>0){
-                List<String> devList = new ArrayList<>();
-                for(int i=0;i<page.getRecords().size();i++){
-                    devList.add(page.getRecords().get(i).getDeviceId());
-                }
-
-                if(devList.size() > 0){
-                    LambdaQueryWrapper<BaseFacilityDevice> lambdaQuery1 = Wrappers.lambdaQuery();
-                    lambdaQuery1.in(BaseFacilityDevice::getDeviceId,devList);
-                    List<BaseFacilityDevice> records1 = baseFacilityDeviceService.list(lambdaQuery1);
-                    if(CollectionUtils.isNotEmpty(deviceInfo)){
-                        for(int i=0;i<page.getRecords().size();i++){
-                            for(int j=0;j<deviceInfo.size();j++){
-                                if(page.getRecords().get(i).getDeviceId().equals(deviceInfo.get(j).getDeviceId())){
-                                    page.getRecords().get(i).setDeviceName(deviceInfo.get(j).getDeviceName());
-                                    page.getRecords().get(i).setDeviceType(deviceInfo.get(j).getDeviceType());
-                                    break;
-                                }
-                            }
-                            for(int k=0;k<records1.size();k++){
-                                if(page.getRecords().get(i).getDeviceId().equals(records1.get(k).getDeviceId())){
-                                    page.getRecords().get(i).setFacilityId(records1.get(k).getFacilityId());
-                                    break;
-                                }
-                            }
-                        }
-                    }
-                }
-
-                List<BaseAlarmType> alarmTypeList = baseMapper.getAlarmTypeList();
-                if(CollectionUtils.isNotEmpty(alarmTypeList)){
-                    for(int i=0;i<page.getRecords().size();i++){
-                        for(int j=0;j<alarmTypeList.size();j++){
-                            if(page.getRecords().get(i).getAlarmType().equals(alarmTypeList.get(j).getTypeCode())){
-                                page.getRecords().get(i).setTypeImg(alarmTypeList.get(j).getTypeImg());
-                                break;
-                            }
-                        }
-                    }
-                }
+//            page = this.page(page,queryWrapper);
+            records = this.list(queryWrapper);
+            if(records.size() == 0){
+                return new CommonPage<>(new ArrayList<>(),0,0,0);
 
             }
         }
 
         if(StringUtils.isNotBlank(baseAlarmListVO.getDeviceName())){
-            list = page.getRecords().stream().filter(e -> baseAlarmListVO.getDeviceName().equals(e.getDeviceName())).collect(Collectors.toList());
+            list = records.stream().filter(e -> baseAlarmListVO.getDeviceName().equals(e.getDeviceName())).collect(Collectors.toList());
         }else {
-            if(page.getRecords().size()>0){
-                for(int k=0;k<page.getRecords().size();k++){
-                    list.add(page.getRecords().get(k));
+            if(records.size()>0){
+                for(int k=0;k<records.size();k++){
+                    list.add(records.get(k));
                 }
             }
 
         }
-        int total = 0;
+
         if(list.size()>0){
-            total = list.size();
+            page.setTotal(list.size());
         }
         int current = 0;
         Integer pageNum = baseAlarmListVO.getCurrent();
@@ -297,6 +260,46 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
         queryWrapper5.eq("tenant_id",SecurityUtils.getTenantId());
         List<DmpDeviceType> deviceTypeList = dmpDeviceTypeMapper.selectList(queryWrapper5);
         List<BaseAlarm> info = list.stream().sorted(Comparator.comparing(BaseAlarm::getId,Comparator.reverseOrder())).skip(current).limit(pageSize).collect(Collectors.toList());
+        List<String> devList = new ArrayList<>();
+        for(int i=0;i<info.size();i++){
+            devList.add(info.get(i).getDeviceId());
+        }
+
+        if(devList.size() > 0){
+            LambdaQueryWrapper<BaseFacilityDevice> lambdaQuery1 = Wrappers.lambdaQuery();
+            lambdaQuery1.in(BaseFacilityDevice::getDeviceId,devList);
+            List<BaseFacilityDevice> records1 = baseFacilityDeviceService.list(lambdaQuery1);
+            if(CollectionUtils.isNotEmpty(deviceInfo)){
+                for(int i=0;i<info.size();i++){
+                    for(int j=0;j<deviceInfo.size();j++){
+                        if(info.get(i).getDeviceId().equals(deviceInfo.get(j).getDeviceId())){
+                            info.get(i).setDeviceName(deviceInfo.get(j).getDeviceName());
+                            info.get(i).setDeviceType(deviceInfo.get(j).getDeviceType());
+                            break;
+                        }
+                    }
+                    for(int k=0;k<records1.size();k++){
+                        if(info.get(i).getDeviceId().equals(records1.get(k).getDeviceId())){
+                            info.get(i).setFacilityId(records1.get(k).getFacilityId());
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        List<BaseAlarmType> alarmTypeList = baseMapper.getAlarmTypeList();
+        if(CollectionUtils.isNotEmpty(alarmTypeList)){
+            for(int i=0;i<info.size();i++){
+                for(int j=0;j<alarmTypeList.size();j++){
+                    if(info.get(i).getAlarmType().equals(alarmTypeList.get(j).getTypeCode())){
+                        info.get(i).setTypeImg(alarmTypeList.get(j).getTypeImg());
+                        break;
+                    }
+                }
+            }
+        }
+
         LocalDateTime toDateTime = LocalDateTime.now();
         for(int l=0;l<info.size();l++){
             LocalDateTime fromDateTime = info.get(l).getAlarmTime();
@@ -318,8 +321,10 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
                     info.get(l).setDeviceTypeName(deviceTypeList.get(m).getTypeName());
                 }
             }
+
         }
-        return new CommonPage<BaseAlarm>(info,total,pageSize,pageNum);
+        page.setRecords(info);
+        return new CommonPage<BaseAlarm>(page.getRecords(),page.getTotal(),pageSize,pageNum);
 
     }
 

+ 2 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/PmWorkReportServiceImpl.java

@@ -421,6 +421,7 @@ public class PmWorkReportServiceImpl extends AbstractCrudService<PmWorkReportMap
                     rp.setReportStatus(1);
                     pmWorkReportMapper.updateById(rp);
                     if (StringUtils.isNotBlank(ccTo)) {
+                        rp.setTenantId(tenantId);
                         dingTalkAndMessage.sendAsyncMessage(rp);
                         receiveMessages(ccTo, rid);
                     }
@@ -776,6 +777,7 @@ public class PmWorkReportServiceImpl extends AbstractCrudService<PmWorkReportMap
         jsonObject.put("infoTypeName", INFO_TITLE);
         jsonObject.put("userName", "报告提醒通知");
         jsonObject.put("userIds", userIds);
+        jsonObject.put("tenantId", tenantId);
 
         // 推送消息中心
         try {

+ 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>

+ 3 - 0
service-oa/service-oa-biz/src/main/java/com/usky/oa/service/impl/OaApprovalServiceImpl.java

@@ -238,6 +238,7 @@ public class OaApprovalServiceImpl extends AbstractCrudService<OaApprovalMapper,
 
                             // 发送消息通知下一步审批人
                             oaSendMessageCenter.sendAsyncMessage(
+                                    tenantId,
                                     oaApproval.getCreateBy(),
                                     oaApproval.getApprovalUid(),
                                     approval.getId(),
@@ -253,6 +254,7 @@ public class OaApprovalServiceImpl extends AbstractCrudService<OaApprovalMapper,
             else {
                 Long proposerId = Long.valueOf(oaApproval.getProposer());
                 oaSendMessageCenter.sendAsyncMessage(
+                        tenantId,
                         oaApproval.getCreateBy(),
                         oaApproval.getApprovalUid(),
                         oaApproval.getId(),
@@ -265,6 +267,7 @@ public class OaApprovalServiceImpl extends AbstractCrudService<OaApprovalMapper,
             Long proposerId = Long.valueOf(oaApproval.getProposer());
             // 发送消息通知申请人审核不通过
             oaSendMessageCenter.sendAsyncMessage(
+                    tenantId,
                     oaApproval.getCreateBy(),
                     oaApproval.getApprovalUid(),
                     oaApproval.getId(),

+ 1 - 0
service-oa/service-oa-biz/src/main/java/com/usky/oa/service/impl/OaFbdDocumentServiceImpl.java

@@ -179,6 +179,7 @@ public class OaFbdDocumentServiceImpl extends AbstractCrudService<OaFbdDocumentM
             oaApprovalMapper.insert(approval);
 
             oaSendMessageCenter.sendAsyncMessage(
+                    oaFbdDocument.getTenantId(),
                     oaFbdDocument.getCreateBy(),
                     approval.getApprovalUid(),
                     oaFbdDocument.getId(),

+ 2 - 1
service-oa/service-oa-biz/src/main/java/com/usky/oa/service/utils/OaSendMessageCenter.java

@@ -57,7 +57,7 @@ public class OaSendMessageCenter {
      * @param sign 申请单类型
      */
     @Async
-    public void sendAsyncMessage(String username, Long approvalUid, Integer id, List<Long> receivers, Integer isPass, String sign) {
+    public void sendAsyncMessage(Integer tenantId, String username, Long approvalUid, Integer id, List<Long> receivers, Integer isPass, String sign) {
 
         log.info(username + "的申请开始发送消息中心-----------------------------------");
 
@@ -99,6 +99,7 @@ public class OaSendMessageCenter {
         mceRequestVO.setInfoTypeName(INFO_TITLE);
         mceRequestVO.setUserName(username);
         mceRequestVO.setUserIds(receivers);
+        mceRequestVO.setTenantId(tenantId);
         try {
             // 推送消息中心
             //ApiResult<Void> voidApiResult = remoteMceService.addMce(jsonObject.toString());

+ 1 - 0
service-pm/service-pm-biz/src/main/java/com/usky/pm/service/config/DingTalkAndMessage.java

@@ -330,6 +330,7 @@ public class DingTalkAndMessage {
         mceRequestVO.setInfoTypeName(INFO_TITLE);
         mceRequestVO.setUserName(username);
         mceRequestVO.setRealName(nickName.getNickName());
+        mceRequestVO.setTenantId(newReport.getTenantId());
 
         if (!userId.isEmpty()) {
             mceRequestVO.setUserIds(userId);

+ 2 - 0
service-pm/service-pm-biz/src/main/java/com/usky/pm/service/impl/PmWorkReportServiceImpl.java

@@ -443,6 +443,7 @@ public class PmWorkReportServiceImpl extends AbstractCrudService<PmWorkReportMap
                         if (pmWorkReport.getSendDingTalk().equals(1)) {
                             dingTalkAndMessage.sendDingTalkDailyReport(rp, contents);
                         }
+                        rp.setTenantId(tenantId);
                         dingTalkAndMessage.sendAsyncMessage(rp);
                         receiveMessages(ccTo, rid);
                     }
@@ -808,6 +809,7 @@ public class PmWorkReportServiceImpl extends AbstractCrudService<PmWorkReportMap
         mceRequestVO.setId(0);
         mceRequestVO.setInfoTypeName(INFO_TITLE);
         mceRequestVO.setUserName("报告管理系统");
+        mceRequestVO.setTenantId(tenantId);
         if (!userIds.isEmpty()) {
             mceRequestVO.setUserIds(userIds);
         }

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

@@ -4,6 +4,7 @@ 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.VppContractRequestVO;
 import com.usky.vpp.service.vo.VppContractResponseVO;
 import com.usky.vpp.service.vo.VppContractTemplateRequestVO;
@@ -111,7 +112,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 +127,24 @@ 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());
     }
 
     /**

+ 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;
 

+ 1 - 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;

+ 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);
 }

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

@@ -1,6 +1,7 @@
 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.VppContractRequestVO;
 import com.usky.vpp.service.vo.VppContractResponseVO;
 
@@ -35,12 +36,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 +57,11 @@ public interface VppContractService {
      * 下载合同文件
      */
     void download(Long id, HttpServletResponse response);
+
+    /**
+     * 按客户最新合同统计已到期 / 即将到期(近三个月)数量
+     * <p>租户隔离;每个客户仅取创建时间最新的一份合同,再按到期日分类统计数量。
+     * 列表请走分页接口,按 contractStatus=5(已到期)/ 7(即将到期)查询。</p>
+     */
+    VppContractExpireStatVO expireStat();
 }

+ 18 - 74
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBiddingConfigServiceImpl.java

@@ -6,28 +6,22 @@ import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.vpp.domain.VppBiddingConfig;
-import com.usky.vpp.domain.VppSite;
 import com.usky.vpp.mapper.VppBiddingConfigMapper;
-import com.usky.vpp.mapper.VppSiteMapper;
 import com.usky.vpp.service.VppBiddingConfigService;
 import com.usky.vpp.service.vo.BiddingConfigRequest;
-import com.usky.vpp.service.vo.BiddingConfigSiteVO;
 import com.usky.vpp.service.vo.BiddingConfigVO;
 import com.usky.vpp.util.VppAuditHelper;
 import com.usky.vpp.util.VppBiddingConfigHelper;
 import com.usky.vpp.util.VppPageHelper;
-import com.usky.vpp.util.VppReportHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
 import java.time.LocalDateTime;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.stream.Collectors;
 
 @Service
@@ -35,8 +29,6 @@ public class VppBiddingConfigServiceImpl implements VppBiddingConfigService {
 
     @Autowired
     private VppBiddingConfigMapper biddingConfigMapper;
-    @Autowired
-    private VppSiteMapper siteMapper;
 
     @Override
     public CommonPage<BiddingConfigVO> page(String configName,
@@ -65,24 +57,23 @@ public class VppBiddingConfigServiceImpl implements VppBiddingConfigService {
 
         Page<VppBiddingConfig> result = biddingConfigMapper.selectPage(page, wrapper);
         List<BiddingConfigVO> records = result.getRecords().stream()
-                .map(config -> toVo(config, false))
+                .map(this::toVo)
                 .collect(Collectors.toList());
         return new CommonPage<>(records, result.getTotal(), result.getCurrent(), result.getSize());
     }
 
     @Override
     public BiddingConfigVO get(Long id) {
-        return toVo(requireConfig(id), true);
+        return toVo(requireConfig(id));
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long create(BiddingConfigRequest request) {
         validateRequest(request);
-        List<Long> siteIds = validateAndNormalizeSiteIds(request.getSiteIds(), resolveCurrentTenantId());
 
         VppBiddingConfig config = new VppBiddingConfig();
-        fillConfigFromRequest(config, request, siteIds);
+        fillConfigFromRequest(config, request);
         VppAuditHelper.fillCreate(config);
         biddingConfigMapper.insert(config);
         return config.getId();
@@ -93,9 +84,8 @@ public class VppBiddingConfigServiceImpl implements VppBiddingConfigService {
     public void update(Long id, BiddingConfigRequest request) {
         VppBiddingConfig config = requireConfig(id);
         validateRequest(request);
-        List<Long> siteIds = validateAndNormalizeSiteIds(request.getSiteIds(), resolveCurrentTenantId());
 
-        fillConfigFromRequest(config, request, siteIds);
+        fillConfigFromRequest(config, request);
         VppAuditHelper.fillUpdate(config);
         biddingConfigMapper.updateById(config);
     }
@@ -117,8 +107,7 @@ public class VppBiddingConfigServiceImpl implements VppBiddingConfigService {
     }
 
     private void fillConfigFromRequest(VppBiddingConfig config,
-                                       BiddingConfigRequest request,
-                                       List<Long> siteIds) {
+                                       BiddingConfigRequest request) {
         config.setConfigName(request.getConfigName().trim());
         config.setEffectiveStart(request.getEffectiveStart());
         config.setEffectiveEnd(request.getEffectiveEnd());
@@ -126,13 +115,13 @@ public class VppBiddingConfigServiceImpl implements VppBiddingConfigService {
         config.setAttachmentName(request.getAttachmentName());
         config.setBiddingMode(request.getBiddingMode());
         config.setTypicalCoefficient(request.getTypicalCoefficient());
-        config.setAdvanceCloseHours(request.getAdvanceCloseHours());
+        config.setNoticeMinHours(request.getNoticeMinHours());
+        config.setNoticeMaxHours(request.getNoticeMaxHours());
         config.setMaxPrice(request.getMaxPrice());
         config.setMinPrice(request.getMinPrice());
-        config.setSiteIds(VppReportHelper.toJson(siteIds));
     }
 
-    private BiddingConfigVO toVo(VppBiddingConfig config, boolean withSites) {
+    private BiddingConfigVO toVo(VppBiddingConfig config) {
         BiddingConfigVO vo = new BiddingConfigVO();
         vo.setId(config.getId());
         vo.setConfigName(config.getConfigName());
@@ -143,37 +132,16 @@ public class VppBiddingConfigServiceImpl implements VppBiddingConfigService {
         vo.setAttachmentUrl(config.getAttachmentUrl());
         vo.setAttachmentName(config.getAttachmentName());
         vo.setBiddingMode(config.getBiddingMode());
-        vo.setBiddingModeName(VppBiddingConfigHelper.resolveBiddingModeName(config.getBiddingMode()));
+        // vo.setBiddingModeName(VppBiddingConfigHelper.resolveBiddingModeName(config.getBiddingMode()));
         vo.setTypicalCoefficient(config.getTypicalCoefficient());
-        vo.setAdvanceCloseHours(config.getAdvanceCloseHours());
-        vo.setAdvanceCloseHoursLabel(
-                VppBiddingConfigHelper.resolveAdvanceCloseHoursLabel(config.getAdvanceCloseHours()));
+        vo.setNoticeMinHours(config.getNoticeMinHours());
+        vo.setNoticeMaxHours(config.getNoticeMaxHours());
         vo.setMaxPrice(config.getMaxPrice());
         vo.setMinPrice(config.getMinPrice());
-        List<Long> siteIds = VppReportHelper.parseSiteIds(config.getSiteIds());
-        vo.setSiteIds(siteIds);
         vo.setUpdateTime(config.getUpdateTime());
-        if (withSites) {
-            vo.setSites(loadSiteBriefs(siteIds));
-        }
         return vo;
     }
 
-    private List<BiddingConfigSiteVO> loadSiteBriefs(List<Long> siteIds) {
-        if (siteIds == null || siteIds.isEmpty()) {
-            return Collections.emptyList();
-        }
-        Map<Long, String> siteNameMap = siteMapper.selectBatchIds(siteIds).stream()
-                .filter(site -> !VppAuditHelper.isDeleted(site.getDeleteFlag()))
-                .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (left, right) -> left));
-        return siteIds.stream().map(siteId -> {
-            BiddingConfigSiteVO item = new BiddingConfigSiteVO();
-            item.setSiteId(siteId);
-            item.setSiteName(siteNameMap.get(siteId));
-            return item;
-        }).collect(Collectors.toList());
-    }
-
     private void validateRequest(BiddingConfigRequest request) {
         if (request == null) {
             throw new BusinessException("请求体不能为空");
@@ -195,8 +163,13 @@ public class VppBiddingConfigServiceImpl implements VppBiddingConfigService {
         if (request.getTypicalCoefficient() == null) {
             throw new BusinessException("典型系数不能为空");
         }
-        if (request.getAdvanceCloseHours() == null || request.getAdvanceCloseHours() <= 0) {
-            throw new BusinessException("提前截标时间须为正整数(小时)");
+        if (request.getNoticeMinHours() != null
+                && request.getNoticeMinHours().compareTo(new java.math.BigDecimal("0.5")) < 0) {
+            throw new BusinessException("提前截标最小通知时间不能小于0.5小时!");
+        }
+        if (request.getNoticeMaxHours() != null
+                && request.getNoticeMaxHours().compareTo(new java.math.BigDecimal("0.5")) < 0) {
+            throw new BusinessException("提前截标最大通知时间不能小于0.5小时!");
         }
         if (request.getMaxPrice() == null) {
             throw new BusinessException("最高价格不能为空");
@@ -205,35 +178,6 @@ public class VppBiddingConfigServiceImpl implements VppBiddingConfigService {
             throw new BusinessException("最低价格不能为空");
         }
         VppBiddingConfigHelper.validatePriceRange(request.getMinPrice(), request.getMaxPrice());
-        if (request.getSiteIds() == null || request.getSiteIds().isEmpty()) {
-            throw new BusinessException("请至少选择一个关联站点");
-        }
-    }
-
-    private List<Long> validateAndNormalizeSiteIds(List<Long> siteIds, Integer tenantId) {
-        if (siteIds == null || siteIds.isEmpty()) {
-            throw new BusinessException("请至少选择一个关联站点");
-        }
-        List<Long> distinct = siteIds.stream()
-                .filter(Objects::nonNull)
-                .distinct()
-                .collect(Collectors.toList());
-        if (distinct.isEmpty()) {
-            throw new BusinessException("请至少选择一个关联站点");
-        }
-        Map<Long, VppSite> siteMap = siteMapper.selectBatchIds(distinct).stream()
-                .filter(site -> !VppAuditHelper.isDeleted(site.getDeleteFlag()))
-                .collect(Collectors.toMap(VppSite::getId, site -> site, (left, right) -> left));
-        for (Long siteId : distinct) {
-            VppSite site = siteMap.get(siteId);
-            if (site == null) {
-                throw new BusinessException("站点不存在或已删除: " + siteId);
-            }
-            if (tenantId != null && site.getTenantId() != null && !tenantId.equals(site.getTenantId())) {
-                throw new BusinessException("站点不属于当前租户: " + siteId);
-            }
-        }
-        return distinct;
     }
 
     private Integer resolveCurrentTenantId() {

+ 65 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
 import com.usky.vpp.domain.VppContract;
 import com.usky.vpp.domain.VppContractTemplate;
 import com.usky.vpp.domain.VppCustomer;
@@ -12,6 +13,7 @@ import com.usky.vpp.mapper.VppContractMapper;
 import com.usky.vpp.mapper.VppContractTemplateMapper;
 import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.service.VppContractService;
+import com.usky.vpp.service.vo.VppContractExpireStatVO;
 import com.usky.vpp.service.vo.VppContractRequestVO;
 import com.usky.vpp.service.vo.VppContractResponseVO;
 import com.usky.vpp.util.VppAuditHelper;
@@ -32,13 +34,12 @@ import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
-import java.time.LocalDateTime;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -162,17 +163,35 @@ public class VppContractServiceImpl implements VppContractService {
     @Override
     public 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) {
         IPage<VppContract> page = new Page<>(pageNum, pageSize);
 
+        // 客户名称模糊匹配:先反查符合条件的客户ID
+        List<Long> customerIdsByName = null;
+        if (StringUtils.isNotBlank(customerName)) {
+            LambdaQueryWrapper<VppCustomer> customerWrapper = new LambdaQueryWrapper<>();
+            customerWrapper.like(VppCustomer::getCustomerName, customerName)
+                    .eq(VppCustomer::getDeleteFlag, 0)
+                    .select(VppCustomer::getId);
+            List<VppCustomer> matchedCustomers = vppCustomerMapper.selectList(customerWrapper);
+            customerIdsByName = matchedCustomers.stream().map(VppCustomer::getId).collect(Collectors.toList());
+            if (customerIdsByName.isEmpty()) {
+                return new CommonPage<>(Collections.emptyList(), 0L, pageSize, pageNum);
+            }
+        }
+
         LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(VppContract::getDeleteFlag, 0)
                 .eq(id != null, VppContract::getId, id)
                 .ge(signDateStart != null, VppContract::getSignDate, signDateStart)
                 .le(signDateEnd != null, VppContract::getSignDate, signDateEnd)
+                .ge(signDateRangeStart != null, VppContract::getSignDate, signDateRangeStart)
+                .le(signDateRangeEnd != null, VppContract::getSignDate, signDateRangeEnd)
                 .eq(templateId != null, VppContract::getTemplateId, templateId)
                 .eq(contractType != null, VppContract::getContractType, contractType)
-                .eq(customerId != null, VppContract::getCustomerId, customerId);
+                .eq(customerId != null, VppContract::getCustomerId, customerId)
+                .in(customerIdsByName != null, VppContract::getCustomerId, customerIdsByName);
         VppContractStatusHelper.applyStatusFilter(queryWrapper, contractStatus);
         queryWrapper.orderByDesc(VppContract::getCreateTime);
         page = vppContractMapper.selectPage(page, queryWrapper);
@@ -244,6 +263,49 @@ public class VppContractServiceImpl implements VppContractService {
         return i > 0;
     }
 
+    @Override
+    public VppContractExpireStatVO expireStat() {
+        VppContractExpireStatVO result = new VppContractExpireStatVO();
+        Integer tenantId = SecurityUtils.getTenantId();
+
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getDeleteFlag, 0)
+                .eq(tenantId != null, VppContract::getTenantId, tenantId)
+                .in(VppContract::getContractStatus, VppContractStatusHelper.PENDING,
+                        VppContractStatusHelper.EFFECTIVE, VppContractStatusHelper.EXPIRED)
+                .isNotNull(VppContract::getCustomerId)
+                .isNotNull(VppContract::getExpireDate)
+                .orderByDesc(VppContract::getCreateTime);
+        List<VppContract> allContracts = vppContractMapper.selectList(queryWrapper);
+        if (allContracts == null || allContracts.isEmpty()) {
+            return result;
+        }
+
+        // 每个客户仅保留创建时间最新的一份合同
+        Map<Long, VppContract> latestByCustomer = new LinkedHashMap<>();
+        for (VppContract contract : allContracts) {
+            latestByCustomer.putIfAbsent(contract.getCustomerId(), contract);
+        }
+
+        LocalDate today = LocalDate.now();
+        LocalDate upcomingDeadline = today.plusMonths(3);
+        long expiredCount = 0L;
+        long upcomingExpireCount = 0L;
+        for (VppContract contract : latestByCustomer.values()) {
+            LocalDate expireDate = contract.getExpireDate();
+            if (today.isAfter(expireDate)) {
+                expiredCount++;
+            } else if (!expireDate.isAfter(upcomingDeadline)) {
+                // today <= expireDate <= today+3个月
+                upcomingExpireCount++;
+            }
+        }
+
+        result.setExpiredCount(expiredCount);
+        result.setUpcomingExpireCount(upcomingExpireCount);
+        return result;
+    }
+
     @Override
     public void download(Long id, HttpServletResponse response) {
         LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();

+ 15 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractTemplateServiceImpl.java

@@ -6,7 +6,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppContract;
 import com.usky.vpp.domain.VppContractTemplate;
+import com.usky.vpp.mapper.VppContractMapper;
 import com.usky.vpp.mapper.VppContractTemplateMapper;
 import com.usky.vpp.service.VppContractTemplateService;
 import com.usky.vpp.service.vo.VppContractTemplateRequestVO;
@@ -41,6 +43,8 @@ public class VppContractTemplateServiceImpl implements VppContractTemplateServic
 
     @Autowired
     private VppContractTemplateMapper vppContractTemplateMapper;
+    @Autowired
+    private VppContractMapper vppContractMapper;
 
     private final RestTemplate restTemplate = new RestTemplate();
 
@@ -185,11 +189,21 @@ public class VppContractTemplateServiceImpl implements VppContractTemplateServic
     public Boolean delete(Long id) {
         LambdaQueryWrapper<VppContractTemplate> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(VppContractTemplate::getId, id)
+                .eq(VppContractTemplate::getDeleteFlag, 0)
                 .eq(VppContractTemplate::getTenantId, SecurityUtils.getTenantId());
         VppContractTemplate template = vppContractTemplateMapper.selectOne(queryWrapper);
 
         if (template == null) {
-            throw new BusinessException("合同模板不存在!");
+            throw new BusinessException("合同模板不存在或已被删除!");
+        }
+
+        // 模板下存在关联合同时不允许删除
+        LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
+        contractWrapper.eq(VppContract::getTemplateId, id)
+                .eq(VppContract::getDeleteFlag, 0);
+        Integer relatedCount = vppContractMapper.selectCount(contractWrapper);
+        if (relatedCount != null && relatedCount > 0) {
+            throw new BusinessException("该合同模板下存在关联合同,无法删除!");
         }
 
         template.setDeleteFlag(1);

+ 26 - 53
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java

@@ -678,6 +678,15 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     public Boolean deleteCustomer(Long id) {
         VppCustomer customer = findCustomerById(id);
 
+        // 客户下存在合同(不论状态)则不允许删除
+        LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
+        contractWrapper.eq(VppContract::getCustomerId, id)
+                .eq(VppContract::getDeleteFlag, 0);
+        Integer contractCount = contractMapper.selectCount(contractWrapper);
+        if (contractCount != null && contractCount > 0) {
+            throw new BusinessException("该客户下存在合同,无法删除!");
+        }
+
         VppAuditHelper.fillSoftDelete(customer);
         int result = customerMapper.updateById(customer);
         return result > 0;
@@ -736,23 +745,12 @@ public class VppCustomerServiceImpl implements VppCustomerService {
             customers = Collections.emptyList();
         }
 
-        // 批量查询关联合同
+        // 批量查询关联主联系人
         final Set<Long> customerIds = customers.stream()
                 .map(VppCustomer::getId)
                 .filter(Objects::nonNull)
                 .collect(Collectors.toSet());
-
-        final Map<Long, List<VppContract>> contractMap;
-        if (!customerIds.isEmpty()) {
-            LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
-            contractWrapper.in(VppContract::getCustomerId, customerIds)
-                    .eq(VppContract::getDeleteFlag, 0);
-            List<VppContract> allContracts = contractMapper.selectList(contractWrapper);
-            contractMap = allContracts.stream()
-                    .collect(Collectors.groupingBy(VppContract::getCustomerId));
-        } else {
-            contractMap = Collections.emptyMap();
-        }
+        final Map<Long, VppCustomerContact> contactMap = loadPrimaryContactMap(customerIds);
 
         // 生成 Excel
         try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
@@ -768,13 +766,8 @@ public class VppCustomerServiceImpl implements VppCustomerService {
 
             // 表头
             String[] headers = {
-                    "序号", "电力户号", "客户名称", "客户类型", "供电公司",
-                    "签约容量(kW)", "信用状态", "签约状态",
-                    "省", "市", "区/县", "地址", "用电地址",
-                    "是否VPP资源", "VPP分类", "需求响应资源分类",
-                    "VPP代理编码", "所属行业", "所属产业", "运行容量(kW)",
-                    "生产经营开始", "生产经营结束", "合同编号列表",
-                    "营业执照", "签约日期", "备注", "创建时间"
+                    "序号", "客户名称", "客户类型", "联系人", "联系电话",
+                    "供电单位", "用电容量", "信用状况", "企业地址", "营业执照"
             };
             org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
             for (int i = 0; i < headers.length; i++) {
@@ -787,50 +780,30 @@ public class VppCustomerServiceImpl implements VppCustomerService {
             int rowIdx = 1;
             for (VppCustomer c : customers) {
                 org.apache.poi.xssf.usermodel.XSSFRow row = sheet.createRow(rowIdx++);
-                List<VppContract> contracts = contractMap.getOrDefault(c.getId(), Collections.emptyList());
-                String contractNos = contracts.stream()
-                        .map(VppContract::getContractNo)
-                        .collect(Collectors.joining("; "));
+                VppCustomerContact contact = contactMap.get(c.getId());
 
                 setCell(row, 0, rowIdx - 1);                       // 序号
-                setCell(row, 1, c.getAccountNo());
-                setCell(row, 2, c.getCustomerName());
-                setCell(row, 3, typeLabel(c.getCustomerType(), "虚拟电厂", "售电客户"));
-                setCell(row, 4, c.getPowerCompany());
-                setCell(row, 5, numberStr(c.getContractCapacity()));
-                setCell(row, 6, typeLabel(c.getCreditStatus(), "正常", "关注", "不良"));
-                setCell(row, 7, typeLabel(c.getLifecycleStatus(), "待签约", "已签约", "履约中", "待续约", "已续约", "已解约"));
-                setCell(row, 8, c.getProvince());
-                setCell(row, 9, c.getCity());
-                setCell(row, 10, c.getDistrict());
-                setCell(row, 11, c.getAddress());
-                setCell(row, 12, c.getPowerAddress());
-                setCell(row, 13, c.getIsVppResource() != null && c.getIsVppResource() == 1 ? "是" : "否");
-                setCell(row, 14, c.getVppCategory());
-                setCell(row, 15, c.getDrResourceType());
-                setCell(row, 16, c.getVppProxyCode());
-                setCell(row, 17, c.getIndustry());
-                setCell(row, 18, c.getIndustrySector());
-                setCell(row, 19, numberStr(c.getRunningCapacity()));
-                setCell(row, 20, timeStr(c.getProductionStartDate()));
-                setCell(row, 21, timeStr(c.getProductionEndDate()));
-                setCell(row, 22, contractNos);
-                insertLicenseImage(workbook, sheet, row, 23, c.getBusinessLicenseUrl());
-                setCell(row, 24, dateStr(c.getSigningDate()));
-                setCell(row, 25, c.getRemark());
-                setCell(row, 26, dateTimeStr(c.getCreateTime()));
+                setCell(row, 1, c.getCustomerName());                // 客户名称
+                setCell(row, 2, typeLabel(c.getCustomerType(), "虚拟电厂", "售电客户")); // 客户类型
+                setCell(row, 3, contact != null ? contact.getContactName() : "");   // 联系人
+                setCell(row, 4, contact != null ? contact.getContactPhone() : "");  // 联系电话
+                setCell(row, 5, c.getPowerCompany());                // 供电单位
+                setCell(row, 6, numberStr(c.getContractCapacity())); // 用电容量
+                setCell(row, 7, typeLabel(c.getCreditStatus(), "正常", "关注", "不良")); // 信用状况
+                setCell(row, 8, c.getAddress());                     // 企业地址
+                insertLicenseImage(workbook, sheet, row, 9, c.getBusinessLicenseUrl()); // 营业执照
             }
 
             // 设置营业执照列宽度(固定宽度以适配图片)
-            sheet.setColumnWidth(23, 28 * 256); // 约 28 字符宽
+            sheet.setColumnWidth(9, 28 * 256); // 约 28 字符宽
 
             // 自动调整列宽
             for (int i = 0; i < headers.length; i++) {
-                if (i == 23) {
+                if (i == 9) {
                     continue; // 营业执照列已手动设置宽度
                 }
                 sheet.autoSizeColumn(i);
-                // 限制最大宽度,避免营业执照URL列过宽
+                // 限制最大宽度
                 int width = sheet.getColumnWidth(i);
                 if (width > 15000) {
                     sheet.setColumnWidth(i, 15000);

+ 101 - 19
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java

@@ -26,6 +26,7 @@ import com.usky.vpp.service.vo.ResponseLoadPointVO;
 import com.usky.vpp.service.vo.ResponseLoadStatRowVO;
 import com.usky.vpp.service.vo.ResponseMonitorTreeNodeVO;
 import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppBaselineHelper;
 import com.usky.vpp.util.VppCapabilityEvalHelper;
 import com.usky.vpp.util.VppResponseMonitorHelper;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -33,8 +34,10 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
@@ -42,6 +45,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -54,6 +58,7 @@ import java.util.stream.Collectors;
 public class VppResponseMonitorServiceImpl implements VppResponseMonitorService {
 
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final int EVENT_STATUS_ENDED = 3;
 
     @Autowired
     private VppSiteMapper siteMapper;
@@ -206,26 +211,86 @@ public class VppResponseMonitorServiceImpl implements VppResponseMonitorService
                                         BigDecimal declaredCapacityKw,
                                         DrContext drContext) {
         List<String> deviceUuids = resolveDeviceUuids(resources, devices);
-        if (!deviceUuids.isEmpty()) {
-            LocalDateTime dayStart = statDate.atStartOfDay();
-            LocalDateTime dayEnd = statDate.atTime(23, 59, 59);
-            LocalDate baselineDate = statDate.minusDays(VppResponseMonitorHelper.BASELINE_DAYS_AGO);
-            List<String> powerMetrics = new ArrayList<>(VppTsdbConstants.ACTIVE_POWER_METRICS);
-            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> actualData =
-                    tsdbQueryService.queryDeviceMetricHistory(deviceUuids, dayStart, dayEnd, powerMetrics);
-            if (VppCapabilityEvalHelper.hasTsdbPowerData(actualData, powerMetrics)) {
-                Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> baselineData =
-                        tsdbQueryService.queryDeviceMetricHistory(deviceUuids,
-                                baselineDate.atStartOfDay(), baselineDate.atTime(23, 59, 59), powerMetrics);
-                List<ResponseLoadPointVO> points = VppResponseMonitorHelper.buildCurveFromTsdb(
-                        actualData, baselineData, statDate, powerMetrics, declaredCapacityKw,
-                        drContext.executionByTime);
-                return new CurveBuildResult(points, "tsdb");
-            }
+        if (deviceUuids.isEmpty()) {
+            return new CurveBuildResult(
+                    VppResponseMonitorHelper.buildMockCurve(resources, devices, siteId, statDate, declaredCapacityKw),
+                    "mock");
         }
-        return new CurveBuildResult(
-                VppResponseMonitorHelper.buildMockCurve(resources, devices, siteId, statDate, declaredCapacityKw),
-                "mock");
+
+        // 确定响应时段(有事件用事件时段,无事件用全天)
+        LocalDateTime responseStart;
+        LocalDateTime responseEnd;
+        if (drContext.event != null && drContext.event.getStartTime() != null) {
+            responseStart = drContext.event.getStartTime();
+            responseEnd = drContext.event.getEndTime() != null
+                    ? drContext.event.getEndTime() : statDate.atTime(23, 59, 59);
+        } else {
+            responseStart = statDate.atStartOfDay();
+            responseEnd = statDate.atTime(23, 59, 59);
+        }
+
+        // 选取典型历史日(与基线管理一致:法定节假日 + 调休,工作日5天 / 非工作日3天)
+        Set<LocalDate> excludeDates = loadResponseHistoryDates();
+        boolean workday = VppBaselineHelper.isWorkdayResponse(statDate);
+        int requiredCount = workday
+                ? VppBaselineHelper.WEEKDAY_REFERENCE_COUNT
+                : VppBaselineHelper.NON_WEEKDAY_REFERENCE_COUNT;
+        List<LocalDate> referenceDates = VppBaselineHelper.selectReferenceDates(
+                statDate, workday, excludeDates, requiredCount);
+
+        int interval = VppResponseMonitorHelper.BUCKET_MINUTES;
+
+        // 查询原始基线(全天,多个典型历史日同时段均值)
+        Map<String, BigDecimal> originalBaseline = tsdbQueryService.queryBaselineAvgByTimePoint(
+                deviceUuids, referenceDates,
+                LocalTime.MIN, LocalTime.of(23, 59, 59),
+                VppBaselineHelper.POWER_METRICS, interval, null);
+        if (originalBaseline.isEmpty()) {
+            return new CurveBuildResult(
+                    VppResponseMonitorHelper.buildMockCurve(resources, devices, siteId, statDate, declaredCapacityKw),
+                    "mock");
+        }
+
+        // 今日实际负荷(全天)
+        Map<String, BigDecimal> todayActual = VppBaselineHelper.buildActualLoadByTimePoint(
+                tsdbQueryService.queryDeviceMetricHistory(
+                        deviceUuids, statDate.atStartOfDay(), statDate.atTime(23, 59, 59),
+                        VppBaselineHelper.POWER_METRICS),
+                interval);
+
+        // 修正系数 K:响应开始前2小时今日实测 / 同时段原始基线,限制 [0.7, 1.2]
+        LocalDateTime correctionWindowStart = responseStart.minusHours(VppBaselineHelper.CORRECTION_HOURS);
+        Map<String, BigDecimal> correctionBaseline = tsdbQueryService.queryBaselineAvgByTimePoint(
+                deviceUuids, referenceDates,
+                correctionWindowStart.toLocalTime(), responseStart.toLocalTime(),
+                VppBaselineHelper.POWER_METRICS, interval, null);
+        BigDecimal correctionFactorK = VppBaselineHelper.calculateCorrectionFactor(
+                responseStart, todayActual,
+                correctionBaseline.isEmpty() ? originalBaseline : correctionBaseline);
+
+        // 预测基线 = 原始基线 × 修正系数 K
+        Map<String, BigDecimal> predictedBaseline = new TreeMap<>();
+        for (Map.Entry<String, BigDecimal> entry : originalBaseline.entrySet()) {
+            predictedBaseline.put(entry.getKey(),
+                    entry.getValue().multiply(correctionFactorK).setScale(2, RoundingMode.HALF_UP));
+        }
+
+        // TSDB 申报容量(仅当有事件时计算)
+        BigDecimal tsdbDeclaredKw = BigDecimal.ZERO;
+        if (drContext.event != null) {
+            Map<String, BigDecimal> responseWindowBaseline = tsdbQueryService.queryBaselineAvgByTimePoint(
+                    deviceUuids, referenceDates,
+                    responseStart.toLocalTime(), responseEnd.toLocalTime(),
+                    VppBaselineHelper.POWER_METRICS, interval, null);
+            tsdbDeclaredKw = VppBaselineHelper.calculateDeclaredCapacityKw(
+                    responseWindowBaseline, responseStart, responseEnd, correctionFactorK);
+        }
+        BigDecimal finalDeclared = tsdbDeclaredKw.compareTo(BigDecimal.ZERO) > 0
+                ? tsdbDeclaredKw : declaredCapacityKw;
+
+        List<ResponseLoadPointVO> points = VppResponseMonitorHelper.buildCurveWithBaseline(
+                predictedBaseline, todayActual, statDate, finalDeclared, drContext.executionByTime, interval);
+        return new CurveBuildResult(points, "tsdb");
     }
 
     private DrContext resolveDrContext(Long siteId, LocalDate statDate, Long eventId) {
@@ -299,6 +364,23 @@ public class VppResponseMonitorServiceImpl implements VppResponseMonitorService
         return time.withMinute(minute).withSecond(0).withNano(0);
     }
 
+    /**
+     * 查询历史已结束的 DR 事件日期,用于基线测算时排除。
+     */
+    private Set<LocalDate> loadResponseHistoryDates() {
+        List<VppDrEvent> events = eventMapper.selectList(
+                new LambdaQueryWrapper<VppDrEvent>()
+                        .eq(VppDrEvent::getEventStatus, EVENT_STATUS_ENDED)
+                        .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        Set<LocalDate> dates = new HashSet<>();
+        for (VppDrEvent event : events) {
+            if (event.getStartTime() != null) {
+                dates.add(event.getStartTime().toLocalDate());
+            }
+        }
+        return dates;
+    }
+
     private ResponseMonitorTreeNodeVO toSiteNode(VppSite site, Long parentId) {
         ResponseMonitorTreeNodeVO node = new ResponseMonitorTreeNodeVO();
         node.setNodeType("SITE");

+ 12 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/BiddingConfigRequest.java

@@ -1,11 +1,11 @@
 package com.usky.vpp.service.vo;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.util.List;
 
 /**
  * 竞价配置创建/更新请求
@@ -23,9 +23,17 @@ public class BiddingConfigRequest {
     /** 1分时段竞价 2整体竞价 */
     private Integer biddingMode;
     private BigDecimal typicalCoefficient;
-    /** 提前截标时间(小时),如 24 表示 >24 小时 */
-    private Integer advanceCloseHours;
+
+    /**
+     * 提前通知最小小时数(下限,不含本值,如 8 表示 >8)
+     */
+    private BigDecimal noticeMinHours;
+
+    /**
+     * 提前通知最大小时数(上限,含本值,如 24 表示 ≤24)
+     */
+    private BigDecimal noticeMaxHours;
+
     private BigDecimal maxPrice;
     private BigDecimal minPrice;
-    private List<Long> siteIds;
 }

+ 12 - 7
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/BiddingConfigVO.java

@@ -1,11 +1,11 @@
 package com.usky.vpp.service.vo;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.util.List;
 
 /**
  * 竞价配置详情/列表项
@@ -23,16 +23,21 @@ public class BiddingConfigVO {
     private String effectivePeriod;
     private String attachmentUrl;
     private String attachmentName;
+
     private Integer biddingMode;
-    private String biddingModeName;
+    // private String biddingModeName;
     private BigDecimal typicalCoefficient;
-    private Integer advanceCloseHours;
-    /** 提前截标时间展示,如 >24 */
-    private String advanceCloseHoursLabel;
+    /**
+     * 提前通知最小小时数(下限,不含本值,如 8 表示 >8)
+     */
+    private BigDecimal noticeMinHours;
+
+    /**
+     * 提前通知最大小时数(上限,含本值,如 24 表示 ≤24)
+     */
+    private BigDecimal noticeMaxHours;
     private BigDecimal maxPrice;
     private BigDecimal minPrice;
-    private List<Long> siteIds;
-    private List<BiddingConfigSiteVO> sites;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime updateTime;
 }

+ 20 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractExpireStatVO.java

@@ -0,0 +1,20 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+/**
+ * 合同到期统计:按客户最新合同的到期时间统计已到期 / 即将到期数量
+ */
+@Data
+public class VppContractExpireStatVO {
+
+    /**
+     * 已到期数量
+     */
+    private Long expiredCount = 0L;
+
+    /**
+     * 即将到期数量(近三个月)
+     */
+    private Long upcomingExpireCount = 0L;
+}

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

@@ -53,7 +53,7 @@ public class VppContractResponseVO {
     private String contractName;
 
     /**
-     * 合同状态
+     * 合同状态(0草稿 1审核中 2待生效 3已生效 4已驳回 5已到期 6已终止)
      */
     private Integer contractStatus;
 

+ 11 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppContractStatusHelper.java

@@ -20,6 +20,8 @@ public final class VppContractStatusHelper {
     public static final int REJECTED = 4;
     public static final int EXPIRED = 5;
     public static final int TERMINATED = 6;
+    /** 查询专用:即将到期(近三个月内过期,非落库状态) */
+    public static final int UPCOMING_EXPIRE = 7;
 
     private static final Set<Integer> MANUAL_STATUSES = new HashSet<>(Arrays.asList(
             DRAFT, AUDITING, REJECTED, TERMINATED
@@ -61,7 +63,8 @@ public final class VppContractStatusHelper {
     }
 
     /**
-     * 分页筛选:将生命周期状态(待生效/已生效/已到期)转换为日期条件。
+     * 分页筛选:将生命周期状态(待生效/已生效/已到期/即将到期)转换为日期条件。
+     * <p>其中 {@link #UPCOMING_EXPIRE} 仅用于查询,不会写入数据库。</p>
      */
     public static void applyStatusFilter(LambdaQueryWrapper<VppContract> wrapper, Integer contractStatus) {
         if (contractStatus == null) {
@@ -87,6 +90,13 @@ public final class VppContractStatusHelper {
                     .lt(VppContract::getExpireDate, today));
             return;
         }
+        if (contractStatus == UPCOMING_EXPIRE) {
+            LocalDate upcomingDeadline = today.plusMonths(3);
+            wrapper.and(w -> w.in(VppContract::getContractStatus, PENDING, EFFECTIVE, EXPIRED)
+                    .ge(VppContract::getExpireDate, today)
+                    .le(VppContract::getExpireDate, upcomingDeadline));
+            return;
+        }
         wrapper.eq(VppContract::getContractStatus, contractStatus);
     }
 }

+ 13 - 10
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppFieldValidator.java

@@ -33,6 +33,9 @@ public final class VppFieldValidator {
 
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
+    /** JSON 字符串最大长度:64KB,防止大 JSON DoS 攻击 */
+    private static final int JSON_MAX_LENGTH = 65536;
+
     private VppFieldValidator() {
         // 工具类禁止实例化
     }
@@ -60,27 +63,27 @@ public final class VppFieldValidator {
     /**
      * 校验 JSON 格式字符串的合法性:
      * <ul>
-     *   <li>null:视为合法,字段允许为空</li>
-     *   <li>空字符串:不允许,抛出异常</li>
-     *   <li>非空时:必须能解析为合法的 JSON 对象或数组</li>
+     *   <li>null 或空字符串:视为合法,返回 null</li>
+     *   <li>非空时:长度 ≤ 64KB,且必须为合法的 JSON 对象({...})</li>
+     *   <li>数组([...])、原始值(字符串/数字/布尔/null)均不通过</li>
      * </ul>
      *
      * @param json      待校验的 JSON 字符串
      * @param fieldName 字段名称(用于错误提示)
-     * @return 规范化后的值:null → null;合法 JSON → 原值
-     * @throws BusinessException 空字符串或 JSON 格式不合法时抛出
+     * @return 规范化后的值:null → null;合法 JSON 对象 → 原值
+     * @throws BusinessException 超出长度限制或 JSON 格式不合法时抛出
      */
     public static String validateJson(String json, String fieldName) {
-        if (json == null) {
+        if (json == null || json.trim().isEmpty()) {
             return null;
         }
-        if (json.trim().isEmpty()) {
-            throw new BusinessException(fieldName + "不能为空字符串!");
+        if (json.length() > JSON_MAX_LENGTH) {
+            throw new BusinessException(fieldName + "内容过大,最大支持64KB!");
         }
         try {
             JsonNode node = OBJECT_MAPPER.readTree(json);
-            if (!node.isObject() && !node.isArray()) {
-                throw new BusinessException(fieldName + "格式错误:仅支持JSON对象({})或数组([])格式!");
+            if (!node.isObject()) {
+                throw new BusinessException(fieldName + "格式错误:必须是JSON对象({})格式,不允许数组或原始值!");
             }
         } catch (BusinessException e) {
             throw e;

+ 39 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseMonitorHelper.java

@@ -102,6 +102,44 @@ public final class VppResponseMonitorHelper {
         return points;
     }
 
+    /**
+     * 基于预计算的预测基线和今日实际,构建曲线点(替代直接操作 TSDB 原始时序)。
+     */
+    public static List<ResponseLoadPointVO> buildCurveWithBaseline(
+            Map<String, BigDecimal> predictedBaseline,
+            Map<String, BigDecimal> todayActual,
+            LocalDate date,
+            BigDecimal declaredCapacityKw,
+            Map<LocalDateTime, BigDecimal> executionActualByTime,
+            int intervalMinutes) {
+        List<ResponseLoadPointVO> points = new ArrayList<>();
+        for (int minute = 0; minute < 1440; minute += intervalMinutes) {
+            LocalTime time = LocalTime.of(minute / 60, minute % 60);
+            String timeKey = time.format(TIME_FMT);
+            LocalDateTime bucketStart = date.atTime(time);
+
+            BigDecimal baseline = predictedBaseline.getOrDefault(timeKey, BigDecimal.ZERO);
+            BigDecimal actual = todayActual.getOrDefault(timeKey, BigDecimal.ZERO);
+
+            BigDecimal responseLoad = executionActualByTime != null
+                    ? executionActualByTime.getOrDefault(bucketStart, null)
+                    : null;
+            if (responseLoad == null) {
+                responseLoad = baseline.subtract(actual).max(BigDecimal.ZERO);
+            }
+
+            ResponseLoadPointVO point = new ResponseLoadPointVO();
+            point.setTime(timeKey);
+            point.setDatetime(bucketStart.format(DATETIME_FMT));
+            point.setBaselineLoadKw(scale(baseline));
+            point.setActualLoadKw(scale(actual));
+            point.setDeclaredCapacityKw(declaredCapacityKw != null ? scale(declaredCapacityKw) : BigDecimal.ZERO);
+            point.setActualResponseLoadKw(scale(responseLoad));
+            points.add(point);
+        }
+        return points;
+    }
+
     public static List<ResponseLoadStatRowVO> buildStatistics(List<ResponseLoadPointVO> points, LocalDate date) {
         if (points == null || points.isEmpty()) {
             return Collections.emptyList();
@@ -170,9 +208,7 @@ public final class VppResponseMonitorHelper {
             if (kw == null) {
                 continue;
             }
-            if (!actual) {
-                kw = kw.multiply(BigDecimal.valueOf(1.02 + pseudo(point.getTime()) * 0.05));
-            }
+            // baselineLoadKw 已是修正后的预测基线(原始基线 × K),无需伪随机调整
             allKw.add(kw);
             String hourKey = point.getTime().substring(0, 2) + ":00";
             hourlyBuckets.computeIfAbsent(hourKey, k -> new ArrayList<>()).add(kw);

+ 69 - 13
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppSmsTemplateHelper.java

@@ -5,12 +5,17 @@ import com.usky.vpp.domain.VppCustomerContact;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
 
+import java.math.BigDecimal;
 import java.time.format.DateTimeFormatter;
 
 /**
  * 邀约短信模板参数构建
  * <p>
- * 阿里云模板变量需与控制台一致,默认使用:meet、time、room(SMS_465362899)
+ * 阿里云模板变量需与控制台一致:name、responsetype、time、number、operationtype
+ * <pre>
+ * 尊敬的${name},您已通过电力需求响应审核。 响应类型:${responsetype} 响应时间:${time}
+ * 邀约规模: ${number}kW 请在规定时段内${operationtype}
+ * </pre>
  */
 public final class VppSmsTemplateHelper {
 
@@ -23,21 +28,72 @@ public final class VppSmsTemplateHelper {
                                             VppDrEvent event,
                                             VppCustomer customer,
                                             VppCustomerContact contact) {
-        String meet = event != null && event.getEventName() != null
-                ? event.getEventName()
-                : (event != null ? event.getEventId() : "");
-        String time = "";
-        if (event != null && event.getStartTime() != null) {
-            time = TIME_FORMAT.format(event.getStartTime());
-        } else if (invitation.getReplyDeadline() != null) {
-            time = TIME_FORMAT.format(invitation.getReplyDeadline());
-        }
-        String room = customer != null && customer.getCustomerName() != null
+        String name = customer != null && customer.getCustomerName() != null
                 ? customer.getCustomerName()
                 : "";
-        return "{\"meet\":\"" + escapeJson(meet)
+
+        Integer responseType = invitation != null && invitation.getResponseType() != null
+                ? invitation.getResponseType()
+                : (event != null ? event.getResponseType() : null);
+        String responsetype = VppCapabilityEvalHelper.responseTypeLabel(responseType);
+
+        String time = buildResponseTime(invitation, event);
+
+        BigDecimal capacity = invitation != null && invitation.getDemandCapacityKw() != null
+                ? invitation.getDemandCapacityKw()
+                : (event != null ? event.getTargetCapacityKw() : null);
+        String number = formatNumber(capacity);
+
+        Integer eventType = invitation != null && invitation.getTransactionType() != null
+                ? invitation.getTransactionType()
+                : (event != null ? event.getEventType() : null);
+        String operationtype = buildOperationType(eventType);
+
+        return "{\"name\":\"" + escapeJson(name)
+                + "\",\"responsetype\":\"" + escapeJson(responsetype)
                 + "\",\"time\":\"" + escapeJson(time)
-                + "\",\"room\":\"" + escapeJson(room) + "\"}";
+                + "\",\"number\":\"" + escapeJson(number)
+                + "\",\"operationtype\":\"" + escapeJson(operationtype) + "\"}";
+    }
+
+    private static String buildResponseTime(VppDrInvitation invitation, VppDrEvent event) {
+        if (event != null && event.getStartTime() != null) {
+            if (event.getEndTime() != null) {
+                return TIME_FORMAT.format(event.getStartTime()) + "~" + TIME_FORMAT.format(event.getEndTime());
+            }
+            return TIME_FORMAT.format(event.getStartTime());
+        }
+        if (invitation != null && invitation.getExecuteStartDate() != null) {
+            if (invitation.getExecuteEndDate() != null
+                    && !invitation.getExecuteStartDate().equals(invitation.getExecuteEndDate())) {
+                return invitation.getExecuteStartDate() + "~" + invitation.getExecuteEndDate();
+            }
+            return invitation.getExecuteStartDate().toString();
+        }
+        if (invitation != null && invitation.getReplyDeadline() != null) {
+            return TIME_FORMAT.format(invitation.getReplyDeadline());
+        }
+        return "";
+    }
+
+    private static String buildOperationType(Integer eventType) {
+        if (eventType == null) {
+            return "完成响应";
+        }
+        if (eventType == 1) {
+            return "参与削峰";
+        }
+        if (eventType == 2) {
+            return "参与填谷";
+        }
+        return "完成响应";
+    }
+
+    private static String formatNumber(BigDecimal value) {
+        if (value == null) {
+            return "0";
+        }
+        return value.stripTrailingZeros().toPlainString();
     }
 
     private static String escapeJson(String value) {

+ 2 - 1
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -643,7 +643,8 @@ CREATE TABLE `vpp_bidding_config` (
     `attachment_name` VARCHAR(200) NULL COMMENT '附件原始文件名',
     `bidding_mode` TINYINT NOT NULL COMMENT '1分时段竞价 2整体竞价',
     `typical_coefficient` DECIMAL(6,4) NOT NULL COMMENT '典型系数',
-    `advance_close_hours` INT NOT NULL COMMENT '提前截标时间(小时),如24表示>24小时',
+    `notice_min_hours` decimal(5,1) DEFAULT NULL COMMENT '提前通知最小小时数(下限,不含本值,如 8 表示 >8)',
+    `notice_max_hours` decimal(5,1) DEFAULT NULL COMMENT '提前通知最大小时数(上限,含本值,如 24 表示 ≤24)',
     `max_price` DECIMAL(12,4) NOT NULL COMMENT '最高价格(元)',
     `min_price` DECIMAL(12,4) NOT NULL COMMENT '最低价格(元)',
     `site_ids` JSON NULL COMMENT '关联站点ID列表',