Browse Source

解决冲突

james 2 days ago
parent
commit
de4a29ca3e
75 changed files with 1875 additions and 827 deletions
  1. 4 3
      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. 8 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/SiteController.java
  70. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppBaselineService.java
  71. 26 26
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBaselineServiceImpl.java
  72. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java
  73. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DeclaredCapacityPointVO.java
  74. 2 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteDeclaredCapacityVO.java
  75. 30 22
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppBaselineHelper.java

+ 4 - 3
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmNoticeResultServiceImpl.java

@@ -50,10 +50,10 @@ public class BaseAlarmNoticeResultServiceImpl extends AbstractCrudService<BaseAl
     private RemoteMceService remoteMceService;
 
     private static final String SMS_SIGN_NAME = "上海永天科技股份有限公司";
-    private static final String SMS_TEMPLATE_CODE = "SMS_463791105";
+    private static final String SMS_TEMPLATE_CODE = "SMS_465362899";
     private static final String REGION_ID = "cn-beijing";
-    private static final String ACCESS_KEY_ID = "LTAI5tH3VvRL5BUkovCokHJX";
-    private static final String ACCESS_KEY_SECRET = "SaaWUouNqvcA0C746gcNOH9m6SRYN4";
+    private static final String ACCESS_KEY_ID = "LTAI5t7GgkVKSdAbPWAzUq8U";
+    private static final String ACCESS_KEY_SECRET = "9zQ3e8GPTSlcfF1ev0JaBjlpvvSG6N";
 
     private static final String INFO_TYPE = "4";
     private static final String INFO_TITLE = "设备告警";
@@ -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);
         }

+ 8 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/SiteController.java

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

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

@@ -33,7 +33,8 @@ public interface VppBaselineService {
     /**
      * 查询站点响应申报容量。
      *
-     * <p>响应时段内同时间段历史负荷均值 × 修正系数 K × 执行时段小时数 / 2。</p>
+     * <p>响应时段内同时间段历史负荷均值 × 执行时段小时数 / 2。</p>
+     * <p>典型历史日选取:工作日 5 天 / 非工作日 3 天;剔除响应当日、历史响应日及响应日 &gt;= 今日。</p>
      *
      * @param siteId            站点 ID
      * @param responseStartTime 响应开始时间,格式 yyyy-MM-dd HH:mm:ss

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

@@ -196,8 +196,9 @@ public class VppBaselineServiceImpl implements VppBaselineService {
     /**
      * 查询站点响应申报容量和预估响应容量。
      *
-     * <p>公式:响应时段内同时间段历史负荷均值 × 修正系数 K × 执行时段小时数 / 2。</p>
-     * <p>典型历史日、修正系数 K 计算规则与基线管理一致;历史均值仅取响应开始至结束时段。</p>
+     * <p>公式:响应时段内同时间段历史负荷均值 × 执行时段小时数 / 2。</p>
+     * <p>典型历史日选取规则(工作日 5 天 / 非工作日 3 天),剔除响应当日、历史响应日(vpp_dr_event 已结束)
+     * 及响应日 &gt;= 今日。</p>
      */
     @Override
     public SiteDeclaredCapacityVO getSiteDeclaredCapacity(Long siteId,
@@ -220,7 +221,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
             return result;
         }
 
-        Set<LocalDate> excludeDates = loadResponseHistoryDates();
+        Set<LocalDate> excludeDates = loadDeclaredCapacityExcludeDates();
         boolean workday = VppBaselineHelper.isWorkdayResponse(responseDate);
         result.setResponseDayType(VppWorkdayHelper.resolveDayType(responseDate));
         result.setResponseDayTypeLabel(VppWorkdayHelper.resolveDayTypeLabel(responseDate));
@@ -228,27 +229,18 @@ public class VppBaselineServiceImpl implements VppBaselineService {
                 ? VppBaselineHelper.WEEKDAY_REFERENCE_COUNT
                 : VppBaselineHelper.NON_WEEKDAY_REFERENCE_COUNT;
         List<LocalDate> referenceDates = VppBaselineHelper.selectReferenceDates(
-                responseDate, workday, excludeDates, requiredCount);
+                responseDate, workday, excludeDates, requiredCount, LocalDate.now());
         result.setReferenceDates(referenceDates.stream()
                 .map(VppBaselineHelper::formatDate)
                 .collect(Collectors.toList()));
+        if (referenceDates.isEmpty()) {
+            return result;
+        }
 
-        LocalDateTime correctionWindowStart = startTime.minusHours(VppBaselineHelper.CORRECTION_HOURS);
-        Map<String, BigDecimal> correctionBaselineByTime = vppTsdbQueryService.queryBaselineAvgByTimePoint(
-                deviceUuids,
-                referenceDates,
-                correctionWindowStart.toLocalTime(),
-                startTime.toLocalTime(),
-                VppBaselineHelper.POWER_METRICS,
-                VppBaselineHelper.INTERVAL_MINUTES,
-                null);
-        // 查询至响应结束,供修正系数 K(响应前)与预估响应容量(响应时段内)共用
         Map<String, BigDecimal> todayActualByTime = VppBaselineHelper.buildActualLoadByTimePoint(
                 vppTsdbQueryService.queryDeviceMetricHistory(
                         deviceUuids, responseDate.atStartOfDay(), endTime, VppBaselineHelper.POWER_METRICS),
                 VppBaselineHelper.INTERVAL_MINUTES);
-        BigDecimal correctionFactorK = VppBaselineHelper.calculateCorrectionFactor(
-                startTime, todayActualByTime, correctionBaselineByTime);
 
         Map<String, BigDecimal> historicalAvgByTime = vppTsdbQueryService.queryBaselineAvgByTimePoint(
                 deviceUuids,
@@ -263,12 +255,12 @@ public class VppBaselineServiceImpl implements VppBaselineService {
             return result;
         }
 
-        result.setCorrectionFactorK(correctionFactorK);
         result.setDeclaredCapacityKw(VppBaselineHelper.calculateDeclaredCapacityKw(
-                historicalAvgByTime, startTime, endTime, correctionFactorK));
-        result.setEstimatedResponseCapacityKw(VppBaselineHelper.calculateEstimatedResponseCapacityKw(todayActualByTime, startTime, endTime));
+                historicalAvgByTime, startTime, endTime));
+        result.setEstimatedResponseCapacityKw(VppBaselineHelper.calculateEstimatedResponseCapacityKw(
+                todayActualByTime, startTime, endTime));
         result.setPoints(VppBaselineHelper.buildDeclaredCapacityPoints(
-                historicalAvgByTime, startTime, endTime, correctionFactorK));
+                historicalAvgByTime, startTime, endTime));
         result.setDataSource("tsdb");
         return result;
     }
@@ -279,15 +271,13 @@ public class VppBaselineServiceImpl implements VppBaselineService {
                                                                  LocalDate responseDate,
                                                                  LocalDateTime startTime,
                                                                  LocalDateTime endTime) {
-        BigDecimal correctionFactorK = VppBaselineHelper.mockCorrectionFactor(siteId, responseDate);
         result.setReferenceDates(new ArrayList<>());
         result.setResponseDayType(VppWorkdayHelper.resolveDayType(responseDate));
         result.setResponseDayTypeLabel(VppWorkdayHelper.resolveDayTypeLabel(responseDate));
-        result.setCorrectionFactorK(correctionFactorK);
         result.setDeclaredCapacityKw(VppBaselineHelper.mockDeclaredCapacityKw(
-                siteId, responseDate, startTime, endTime, correctionFactorK));
+                siteId, responseDate, startTime, endTime));
         result.setPoints(VppBaselineHelper.buildMockDeclaredCapacityPoints(
-                siteId, responseDate, startTime, endTime, correctionFactorK));
+                siteId, responseDate, startTime, endTime));
         result.setDataSource("mock");
         return result;
     }
@@ -350,17 +340,27 @@ public class VppBaselineServiceImpl implements VppBaselineService {
     }
 
     private Set<LocalDate> loadResponseHistoryDates() {
+        return new HashSet<>(loadCompleteResponseDates());
+    }
+
+    /** 申报容量参考日剔除:历史响应日(vpp_dr_event 已结束)。 */
+    private Set<LocalDate> loadDeclaredCapacityExcludeDates() {
+        return loadResponseHistoryDates();
+    }
+
+    /** 已结束需求响应事件的起始日期(历史完整响应日)。 */
+    private List<LocalDate> loadCompleteResponseDates() {
         List<VppDrEvent> events = drEventMapper.selectList(
                 new LambdaQueryWrapper<VppDrEvent>()
                         .eq(VppDrEvent::getEventStatus, EVENT_STATUS_ENDED)
                         .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED));
-        Set<LocalDate> dates = new HashSet<>();
+        Set<LocalDate> dates = new LinkedHashSet<>();
         for (VppDrEvent event : events) {
             if (event.getStartTime() != null) {
                 dates.add(event.getStartTime().toLocalDate());
             }
         }
-        return dates;
+        return new ArrayList<>(dates);
     }
 
     private VppSite resolveSite(Long siteId) {

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

@@ -284,7 +284,7 @@ public class VppResponseMonitorServiceImpl implements VppResponseMonitorService
                     responseStart.toLocalTime(), responseEnd.toLocalTime(),
                     VppBaselineHelper.POWER_METRICS, interval, null);
             tsdbDeclaredKw = VppBaselineHelper.calculateDeclaredCapacityKw(
-                    responseWindowBaseline, responseStart, responseEnd, correctionFactorK);
+                    responseWindowBaseline, responseStart, responseEnd);
         }
         BigDecimal finalDeclared = tsdbDeclaredKw.compareTo(BigDecimal.ZERO) > 0
                 ? tsdbDeclaredKw : declaredCapacityKw;

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

@@ -14,6 +14,6 @@ public class DeclaredCapacityPointVO {
     private String time;
     /** 同时间段历史负荷均值 kW */
     private BigDecimal historicalAvgKw;
-    /** 申报容量 kW = 历史均值 × K × 执行时段小时数 / 2 */
+    /** 申报容量 kW = 历史均值 × 执行时段小时数 / 2 */
     private BigDecimal declaredCapacityKw;
 }

+ 2 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteDeclaredCapacityVO.java

@@ -18,13 +18,11 @@ public class SiteDeclaredCapacityVO {
     private LocalDateTime responseStartTime;
     /** 响应结束时间 */
     private LocalDateTime responseEndTime;
-    /** 日内修正系数 K(0.7 ~ 1.2) */
-    private BigDecimal correctionFactorK;
-    /** 响应申报容量 kW(历史均值 × K × 执行时段小时数 / 2) */
+    /** 响应申报容量 kW(历史均值 × 执行时段小时数 / 2) */
     private BigDecimal declaredCapacityKw;
     /** 预估响应容量 kW(estimated_response_capacity_kw) */
     private BigDecimal estimatedResponseCapacityKw;
-    /** 典型历史日(yyyy-MM-dd) */
+    /** 参与计算的典型历史日(yyyy-MM-dd) */
     private List<String> referenceDates;
     /** 响应日类型:WORKDAY / NON_WORKDAY */
     private String responseDayType;

+ 30 - 22
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppBaselineHelper.java

@@ -67,19 +67,35 @@ public final class VppBaselineHelper {
 
     /**
      * 选取典型历史日:工作日取前 5 个工作日,非工作日取前 3 个非工作日;
-     * 仅剔除响应当日与历史响应日(不含节假日前后日期)
+     * 剔除 {@code excludeDates} 中的日期及响应当日
      */
     public static List<LocalDate> selectReferenceDates(LocalDate responseDate,
                                                        boolean workday,
                                                        Set<LocalDate> excludeDates,
                                                        int requiredCount) {
-        Set<LocalDate> excludes = new HashSet<>(excludeDates);
+        return selectReferenceDates(responseDate, workday, excludeDates, requiredCount, null);
+    }
+
+    /**
+     * 选取典型历史日,并可指定 {@code today}:候选参考日须严格早于 {@code today}(响应日 &gt;= 今日不参与选取)。
+     */
+    public static List<LocalDate> selectReferenceDates(LocalDate responseDate,
+                                                       boolean workday,
+                                                       Set<LocalDate> excludeDates,
+                                                       int requiredCount,
+                                                       LocalDate today) {
+        Set<LocalDate> excludes = new HashSet<>(excludeDates != null ? excludeDates : Collections.emptySet());
         excludes.add(responseDate);
 
         List<LocalDate> result = new ArrayList<>();
         LocalDate cursor = responseDate.minusDays(1);
         int guard = 0;
         while (result.size() < requiredCount && guard < 400) {
+            if (today != null && !cursor.isBefore(today)) {
+                cursor = cursor.minusDays(1);
+                guard++;
+                continue;
+            }
             if (excludes.contains(cursor)) {
                 cursor = cursor.minusDays(1);
                 guard++;
@@ -300,17 +316,16 @@ public final class VppBaselineHelper {
     }
 
     /**
-     * 响应申报容量 = 响应时段内历史负荷均值 × 修正系数 K × 执行时段小时数 / 2。
+     * 响应申报容量 = 响应时段内历史负荷均值 × 执行时段小时数 / 2。
      */
     public static BigDecimal calculateDeclaredCapacityKw(Map<String, BigDecimal> historicalAvgByTime,
-                                                       LocalDateTime windowStart,
-                                                       LocalDateTime windowEnd,
-                                                       BigDecimal correctionFactorK) {
+                                                         LocalDateTime windowStart,
+                                                         LocalDateTime windowEnd) {
         BigDecimal historicalAvg = averageValuesInWindow(historicalAvgByTime, windowStart, windowEnd);
         if (historicalAvg == null || historicalAvg.compareTo(BigDecimal.ZERO) <= 0) {
             return BigDecimal.ZERO;
         }
-        return applyDeclaredCapacityFormula(historicalAvg, correctionFactorK, windowStart, windowEnd);
+        return applyDeclaredCapacityFormula(historicalAvg, windowStart, windowEnd);
     }
 
     /**
@@ -327,19 +342,16 @@ public final class VppBaselineHelper {
     }
 
     private static BigDecimal applyDeclaredCapacityFormula(BigDecimal historicalAvg,
-                                                           BigDecimal correctionFactorK,
                                                            LocalDateTime windowStart,
                                                            LocalDateTime windowEnd) {
-        BigDecimal k = correctionFactorK != null ? correctionFactorK : K_DEFAULT;
         BigDecimal windowHours = responseWindowHours(windowStart, windowEnd);
-        return scale(historicalAvg.multiply(k).multiply(windowHours)
+        return scale(historicalAvg.multiply(windowHours)
                 .divide(DECLARED_CAPACITY_DIVISOR, 6, RoundingMode.HALF_UP));
     }
 
     public static List<DeclaredCapacityPointVO> buildDeclaredCapacityPoints(Map<String, BigDecimal> historicalAvgByTime,
-                                                                          LocalDateTime windowStart,
-                                                                          LocalDateTime windowEnd,
-                                                                          BigDecimal correctionFactorK) {
+                                                                              LocalDateTime windowStart,
+                                                                              LocalDateTime windowEnd) {
         List<DeclaredCapacityPointVO> points = new ArrayList<>();
         if (historicalAvgByTime == null || historicalAvgByTime.isEmpty()) {
             return points;
@@ -355,7 +367,7 @@ public final class VppBaselineHelper {
             point.setTime(entry.getKey());
             point.setHistoricalAvgKw(scale(historicalAvg));
             point.setDeclaredCapacityKw(applyDeclaredCapacityFormula(
-                    historicalAvg, correctionFactorK, windowStart, windowEnd));
+                    historicalAvg, windowStart, windowEnd));
             points.add(point);
         }
         return points;
@@ -365,25 +377,21 @@ public final class VppBaselineHelper {
     public static BigDecimal mockDeclaredCapacityKw(Long siteId,
                                                     LocalDate responseDate,
                                                     LocalDateTime windowStart,
-                                                    LocalDateTime windowEnd,
-                                                    BigDecimal correctionFactorK) {
+                                                    LocalDateTime windowEnd) {
         return calculateDeclaredCapacityKw(
                 buildMockHistoricalWindow(siteId, responseDate, windowStart, windowEnd),
                 windowStart,
-                windowEnd,
-                correctionFactorK);
+                windowEnd);
     }
 
     public static List<DeclaredCapacityPointVO> buildMockDeclaredCapacityPoints(Long siteId,
                                                                                 LocalDate responseDate,
                                                                                 LocalDateTime windowStart,
-                                                                                LocalDateTime windowEnd,
-                                                                                BigDecimal correctionFactorK) {
+                                                                                LocalDateTime windowEnd) {
         return buildDeclaredCapacityPoints(
                 buildMockHistoricalWindow(siteId, responseDate, windowStart, windowEnd),
                 windowStart,
-                windowEnd,
-                correctionFactorK);
+                windowEnd);
     }
 
     private static Map<String, BigDecimal> buildMockHistoricalWindow(Long siteId,