fanghuisheng 2 nedēļas atpakaļ
vecāks
revīzija
be2c19ae31
17 mainītis faili ar 2142 papildinājumiem un 0 dzēšanām
  1. BIN
      service-eg/service-eg-biz/data/face_db.sqlite
  2. 141 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/config/SmartFaceConfig.java
  3. 591 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/SmartFaceController.java
  4. 54 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/SmartFaceDetectionService.java
  5. 165 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/SmartFaceRecognitionService.java
  6. 161 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/SmartFaceDetectionServiceImpl.java
  7. 571 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/SmartFaceRecognitionServiceImpl.java
  8. BIN
      service-eg/service-eg-biz/src/main/resources/models/model_ir_se50.pt
  9. BIN
      service-eg/service-eg-biz/src/main/resources/models/mtcnn/onet_script.pt
  10. BIN
      service-eg/service-eg-biz/src/main/resources/models/mtcnn/pnet_script.pt
  11. BIN
      service-eg/service-eg-biz/src/main/resources/models/mtcnn/rnet_script.pt
  12. 199 0
      service-eg/service-eg-biz/src/main/resources/smartface-config-example.yml
  13. 9 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsProjectController.java
  14. 6 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectService.java
  15. 34 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectServiceImpl.java
  16. 118 0
      service-ems/service-ems-biz/src/main/resources/application-dev.yml
  17. 93 0
      service-meeting/service-meeting-biz/src/main/resources/application-dev.yml

BIN
service-eg/service-eg-biz/data/face_db.sqlite


+ 141 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/config/SmartFaceConfig.java

@@ -0,0 +1,141 @@
+package com.usky.eg.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * SmartJavaAI人脸识别配置类
+ * 
+ * @author SmartJavaAI Integration
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "smartface")
+public class SmartFaceConfig {
+
+    /**
+     * 是否启用SmartJavaAI人脸识别
+     */
+    private boolean enabled = true;
+
+    /**
+     * 是否在启动时加载模型(如果为false,则延迟到第一次使用时加载)
+     */
+    private boolean lazyLoad = false;
+
+    /**
+     * 人脸检测模型配置
+     */
+    private FaceDetectionConfig detection = new FaceDetectionConfig();
+
+    /**
+     * 人脸识别模型配置
+     */
+    private FaceRecognitionConfig recognition = new FaceRecognitionConfig();
+
+    /**
+     * 向量数据库配置
+     */
+    private VectorDbConfig vectorDb = new VectorDbConfig();
+
+    /**
+     * 人脸检测配置
+     */
+    @Data
+    public static class FaceDetectionConfig {
+        /**
+         * 模型类型: MTCNN, RETINA_FACE, YOLOV5_FACE_320, YOLOV5_FACE_640, SEETA_FACE6_MODEL
+         */
+        private String modelType = "MTCNN";
+
+        /**
+         * 模型路径
+         */
+        private String modelPath;
+
+        /**
+         * 置信度阈值
+         */
+        private float confidenceThreshold = 0.5f;
+
+        /**
+         * NMS阈值
+         */
+        private float nmsThreshold = 0.4f;
+
+        /**
+         * 设备类型: CPU, GPU
+         */
+        private String device = "CPU";
+    }
+
+    /**
+     * 人脸识别配置
+     */
+    @Data
+    public static class FaceRecognitionConfig {
+        /**
+         * 模型类型: INSIGHT_FACE_IRSE50_MODEL, INSIGHT_FACE_MOBILE_FACENET_MODEL, FACE_NET_MODEL
+         */
+        private String modelType = "INSIGHT_FACE_IRSE50_MODEL";
+
+        /**
+         * 模型路径
+         */
+        private String modelPath;
+
+        /**
+         * 是否裁剪人脸
+         */
+        private boolean cropFace = true;
+
+        /**
+         * 是否启用人脸对齐
+         */
+        private boolean align = true;
+
+        /**
+         * 相似度阈值
+         */
+        private float similarityThreshold = 0.62f;
+
+        /**
+         * 设备类型: CPU, GPU
+         */
+        private String device = "CPU";
+    }
+
+    /**
+     * 向量数据库配置
+     */
+    @Data
+    public static class VectorDbConfig {
+        /**
+         * 数据库类型: SQLITE, MILVUS
+         */
+        private String type = "SQLITE";
+
+        /**
+         * SQLite数据库路径
+         */
+        private String sqlitePath = "./data/face_db.sqlite";
+
+        /**
+         * Milvus配置
+         */
+        private MilvusConfig milvus = new MilvusConfig();
+    }
+
+    /**
+     * Milvus配置
+     */
+    @Data
+    public static class MilvusConfig {
+        private String host = "127.0.0.1";
+        private int port = 19530;
+        private String username;
+        private String password;
+        private String collectionName = "face_collection";
+    }
+}

+ 591 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/SmartFaceController.java

@@ -0,0 +1,591 @@
+package com.usky.eg.controller.web;
+
+import ai.djl.modality.cv.Image;
+import cn.smartjavaai.common.cv.SmartImageFactory;
+import cn.smartjavaai.common.entity.DetectionInfo;
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.common.entity.R;
+import cn.smartjavaai.common.entity.face.FaceSearchResult;
+import cn.smartjavaai.face.entity.FaceRegisterInfo;
+import cn.smartjavaai.face.entity.FaceSearchParams;
+import cn.smartjavaai.face.vector.entity.FaceVector;
+import com.alibaba.fastjson.JSONObject;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.eg.domain.SysPerson;
+import com.usky.eg.mapper.SysPersonMapper;
+import com.usky.eg.service.SmartFaceDetectionService;
+import com.usky.eg.service.SmartFaceRecognitionService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.util.*;
+
+/**
+ * SmartJavaAI人脸识别控制器
+ * 
+ * @author SmartJavaAI Integration
+ */
+@Slf4j
+@Api(tags = "SmartJavaAI人脸识别接口")
+@RestController
+@RequestMapping("/smartface")
+@ConditionalOnProperty(prefix = "smartface", name = "enabled", havingValue = "true")
+public class SmartFaceController {
+
+    @Autowired
+    private SmartFaceDetectionService faceDetectionService;
+
+    @Autowired
+    private SmartFaceRecognitionService faceRecognitionService;
+
+    @Autowired
+    private SysPersonMapper sysPersonMapper;
+
+    // ==================== 人脸检测接口 ====================
+
+    @ApiOperation("人脸检测 - 上传图片")
+    @PostMapping("/detect")
+    public ApiResult<Map<String, Object>> detectFaces(
+            @ApiParam("图片文件") @RequestParam("file") MultipartFile file) {
+        try {
+            if (file.isEmpty()) {
+                return ApiResult.error("文件不能为空");
+            }
+
+            byte[] imageBytes = file.getBytes();
+            R<DetectionResponse> result = faceDetectionService.detectFaces(imageBytes);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            DetectionResponse response = result.getData();
+            Map<String, Object> data = new HashMap<>();
+            data.put("faceCount", response.getDetectionInfoList().size());
+            data.put("faces", response.getDetectionInfoList());
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("人脸检测失败", e);
+            return ApiResult.error("人脸检测失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("人脸检测 - 通过图片路径")
+    @PostMapping("/detect/path")
+    public ApiResult<Map<String, Object>> detectFacesByPath(
+            @ApiParam("图片路径") @RequestParam("imagePath") String imagePath) {
+        try {
+            R<DetectionResponse> result = faceDetectionService.detectFaces(imagePath);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            DetectionResponse response = result.getData();
+            Map<String, Object> data = new HashMap<>();
+            data.put("faceCount", response.getDetectionInfoList().size());
+            data.put("faces", response.getDetectionInfoList());
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("人脸检测失败", e);
+            return ApiResult.error("人脸检测失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 人脸特征提取接口 ====================
+
+    @ApiOperation("提取人脸特征 - 上传图片")
+    @PostMapping("/extract/feature")
+    public ApiResult<Map<String, Object>> extractFeature(
+            @ApiParam("图片文件") @RequestParam("file") MultipartFile file) {
+        try {
+            if (file.isEmpty()) {
+                return ApiResult.error("文件不能为空");
+            }
+
+            byte[] imageBytes = file.getBytes();
+            R<float[]> result = faceRecognitionService.extractTopFaceFeature(imageBytes);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("featureDimension", result.getData().length);
+            data.put("features", result.getData());
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("提取人脸特征失败", e);
+            return ApiResult.error("提取人脸特征失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("提取所有人脸特征 - 上传图片")
+    @PostMapping("/extract/features")
+    public ApiResult<Map<String, Object>> extractFeatures(
+            @ApiParam("图片文件") @RequestParam("file") MultipartFile file) {
+        try {
+            if (file.isEmpty()) {
+                return ApiResult.error("文件不能为空");
+            }
+
+            byte[] imageBytes = file.getBytes();
+            Image image = SmartImageFactory.getInstance().fromInputStream(
+                    new java.io.ByteArrayInputStream(imageBytes));
+            
+            R<DetectionResponse> result = faceRecognitionService.extractFeatures(image);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            DetectionResponse response = result.getData();
+            Map<String, Object> data = new HashMap<>();
+            data.put("faceCount", response.getDetectionInfoList().size());
+            
+            List<Map<String, Object>> faceList = new ArrayList<>();
+            for (DetectionInfo detectionInfo : response.getDetectionInfoList()) {
+                Map<String, Object> faceData = new HashMap<>();
+                faceData.put("bbox", detectionInfo.getDetectionRectangle());
+                faceData.put("confidence", detectionInfo.getScore());
+                // 注意:DetectionInfo 可能没有 feature 字段,需要单独提取
+                // faceData.put("features", detectionInfo.getFeature());
+                // faceData.put("featureDimension", detectionInfo.getFeature() != null ? detectionInfo.getFeature().length : 0);
+                faceList.add(faceData);
+            }
+            data.put("faces", faceList);
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("提取人脸特征失败", e);
+            return ApiResult.error("提取人脸特征失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 人脸比对接口(1:1) ====================
+
+    @ApiOperation("人脸比对(1:1)- 上传两张图片")
+    @PostMapping("/compare")
+    public ApiResult<Map<String, Object>> compareFaces(
+            @ApiParam("第一张图片") @RequestParam("file1") MultipartFile file1,
+            @ApiParam("第二张图片") @RequestParam("file2") MultipartFile file2,
+            @ApiParam("相似度阈值") @RequestParam(value = "threshold", defaultValue = "0.62") float threshold) {
+        try {
+            if (file1.isEmpty() || file2.isEmpty()) {
+                return ApiResult.error("图片文件不能为空");
+            }
+
+            Image image1 = SmartImageFactory.getInstance().fromInputStream(file1.getInputStream());
+            Image image2 = SmartImageFactory.getInstance().fromInputStream(file2.getInputStream());
+
+            R<Float> result = faceRecognitionService.compareFeatures(image1, image2);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            float similarity = result.getData();
+            Map<String, Object> data = new HashMap<>();
+            data.put("similarity", similarity);
+            data.put("threshold", threshold);
+            data.put("isMatch", similarity >= threshold);
+            data.put("confidenceLevel", getConfidenceLevel(similarity, threshold));
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("人脸比对失败", e);
+            return ApiResult.error("人脸比对失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("人脸比对(1:1)- 通过图片路径")
+    @PostMapping("/compare/path")
+    public ApiResult<Map<String, Object>> compareFacesByPath(
+            @ApiParam("第一张图片路径") @RequestParam("imagePath1") String imagePath1,
+            @ApiParam("第二张图片路径") @RequestParam("imagePath2") String imagePath2,
+            @ApiParam("相似度阈值") @RequestParam(value = "threshold", defaultValue = "0.62") float threshold) {
+        try {
+            R<Float> result = faceRecognitionService.compareFeatures(imagePath1, imagePath2);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            float similarity = result.getData();
+            Map<String, Object> data = new HashMap<>();
+            data.put("similarity", similarity);
+            data.put("threshold", threshold);
+            data.put("isMatch", similarity >= threshold);
+            data.put("confidenceLevel", getConfidenceLevel(similarity, threshold));
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("人脸比对失败", e);
+            return ApiResult.error("人脸比对失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 人脸比对(1:1)- base64图片 ====================
+
+    @ApiOperation("人脸比对(1:1)- base64图片")
+    @PostMapping("/compare/base64")
+    public ApiResult<Map<String, Object>> compareFacesByBase64(
+            @ApiParam("第一张图片base64(支持data:image/...;base64,前缀或纯base64)")
+            @RequestParam("imageBase641") String imageBase641,
+            @ApiParam("第二张图片base64(支持data:image/...;base64,前缀或纯base64)")
+            @RequestParam("imageBase642") String imageBase642,
+            @ApiParam("相似度阈值")
+            @RequestParam(value = "threshold", defaultValue = "0.62") float threshold) {
+        try {
+            if (imageBase641 == null || imageBase641.isEmpty() || imageBase642 == null || imageBase642.isEmpty()) {
+                return ApiResult.error("图片base64不能为空");
+            }
+
+            String base64Data1 = imageBase641.contains(",")
+                    ? imageBase641.substring(imageBase641.indexOf(",") + 1)
+                    : imageBase641;
+            String base64Data2 = imageBase642.contains(",")
+                    ? imageBase642.substring(imageBase642.indexOf(",") + 1)
+                    : imageBase642;
+
+            byte[] imageBytes1 = java.util.Base64.getDecoder().decode(base64Data1);
+            byte[] imageBytes2 = java.util.Base64.getDecoder().decode(base64Data2);
+
+            Image image1 = SmartImageFactory.getInstance().fromInputStream(
+                    new java.io.ByteArrayInputStream(imageBytes1));
+            Image image2 = SmartImageFactory.getInstance().fromInputStream(
+                    new java.io.ByteArrayInputStream(imageBytes2));
+
+            R<Float> result = faceRecognitionService.compareFeatures(image1, image2);
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            float similarity = result.getData();
+            Map<String, Object> data = new HashMap<>();
+            data.put("similarity", similarity);
+            data.put("threshold", threshold);
+            data.put("isMatch", similarity >= threshold);
+            data.put("confidenceLevel", getConfidenceLevel(similarity, threshold));
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("人脸比对失败", e);
+            return ApiResult.error("人脸比对失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 人脸注册接口 ====================
+
+    @ApiOperation("注册人脸 - 上传图片")
+    @PostMapping("/register")
+    public ApiResult<Map<String, Object>> registerFace(
+            @ApiParam("图片文件") @RequestParam("file") MultipartFile file,
+            @ApiParam("用户ID(可选)") @RequestParam(value = "userId", required = false) String userId,
+            @ApiParam("元数据(JSON格式)") @RequestParam(value = "metadata", required = false) String metadata) {
+        try {
+            if (file.isEmpty()) {
+                return ApiResult.error("文件不能为空");
+            }
+
+            Image image = SmartImageFactory.getInstance().fromInputStream(file.getInputStream());
+
+            FaceRegisterInfo registerInfo = new FaceRegisterInfo();
+            if (userId != null && !userId.isEmpty()) {
+                registerInfo.setId(userId);
+            }
+            if (metadata != null && !metadata.isEmpty()) {
+                registerInfo.setMetadata(metadata);
+            }
+
+            R<String> result = faceRecognitionService.registerFace(registerInfo, image);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("faceId", result.getData());
+            data.put("message", "人脸注册成功");
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("注册人脸失败", e);
+            return ApiResult.error("注册人脸失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 人脸搜索接口(1:N) ====================
+
+    @ApiOperation("搜索人脸(1:N)- 上传图片")
+    @PostMapping("/search")
+    public ApiResult<Map<String, Object>> searchFace(
+            @ApiParam("图片文件") @RequestParam("file") MultipartFile file,
+            @ApiParam("返回结果数量") @RequestParam(value = "topK", defaultValue = "5") int topK,
+            @ApiParam("相似度阈值") @RequestParam(value = "threshold", defaultValue = "0.62") float threshold) {
+        try {
+            if (file.isEmpty()) {
+                return ApiResult.error("文件不能为空");
+            }
+
+            // 等待人脸库加载完成
+            if (!faceRecognitionService.isLoadFaceCompleted()) {
+                return ApiResult.error("人脸库正在加载中,请稍后再试");
+            }
+
+            Image image = SmartImageFactory.getInstance().fromInputStream(file.getInputStream());
+
+            FaceSearchParams searchParams = new FaceSearchParams();
+            searchParams.setTopK(topK);
+            searchParams.setThreshold(threshold);
+
+            List<FaceSearchResult> searchResults = faceRecognitionService.searchFace(image, searchParams);
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("resultCount", searchResults.size());
+            data.put("results", searchResults);
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("搜索人脸失败", e);
+            return ApiResult.error("搜索人脸失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 人脸管理接口 ====================
+
+    @ApiOperation("删除人脸")
+    @DeleteMapping("/delete/{faceId}")
+    public ApiResult<String> deleteFace(
+            @ApiParam("人脸ID") @PathVariable("faceId") String faceId) {
+        try {
+            R<Boolean> result = faceRecognitionService.deleteFace(faceId);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            return ApiResult.success("人脸删除成功");
+        } catch (Exception e) {
+            log.error("删除人脸失败: {}", faceId, e);
+            return ApiResult.error("删除人脸失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取人脸信息")
+    @GetMapping("/info/{faceId}")
+    public ApiResult<FaceVector> getFaceInfo(
+            @ApiParam("人脸ID") @PathVariable("faceId") String faceId) {
+        try {
+            R<FaceVector> result = faceRecognitionService.getFaceById(faceId);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            return ApiResult.success(result.getData());
+        } catch (Exception e) {
+            log.error("获取人脸信息失败: {}", faceId, e);
+            return ApiResult.error("获取人脸信息失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取人脸列表")
+    @GetMapping("/list")
+    public ApiResult<Map<String, Object>> listFaces(
+            @ApiParam("页码") @RequestParam(value = "page", defaultValue = "1") int page,
+            @ApiParam("每页大小") @RequestParam(value = "pageSize", defaultValue = "10") int pageSize) {
+        try {
+            R<List<FaceVector>> result = faceRecognitionService.listFaces(page, pageSize);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("page", page);
+            data.put("pageSize", pageSize);
+            data.put("list", result.getData());
+            data.put("total", result.getData().size());
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("获取人脸列表失败", e);
+            return ApiResult.error("获取人脸列表失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("更新人脸")
+    @PutMapping("/update")
+    public ApiResult<String> updateFace(
+            @ApiParam("人脸ID") @RequestParam("faceId") String faceId,
+            @ApiParam("图片文件") @RequestParam("file") MultipartFile file,
+            @ApiParam("元数据(JSON格式)") @RequestParam(value = "metadata", required = false) String metadata) {
+        try {
+            if (file.isEmpty()) {
+                return ApiResult.error("文件不能为空");
+            }
+
+            Image image = SmartImageFactory.getInstance().fromInputStream(file.getInputStream());
+
+            FaceRegisterInfo registerInfo = new FaceRegisterInfo();
+            registerInfo.setId(faceId);
+            if (metadata != null && !metadata.isEmpty()) {
+                registerInfo.setMetadata(metadata);
+            }
+
+            R<Boolean> result = faceRecognitionService.updateFace(registerInfo, image);
+
+            if (!result.isSuccess()) {
+                return ApiResult.error(result.getMessage());
+            }
+
+            return ApiResult.success("人脸更新成功");
+        } catch (Exception e) {
+            log.error("更新人脸失败: {}", faceId, e);
+            return ApiResult.error("更新人脸失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("检查人脸库加载状态")
+    @GetMapping("/status")
+    public ApiResult<Map<String, Object>> checkStatus() {
+        try {
+            boolean isLoaded = faceRecognitionService.isLoadFaceCompleted();
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("isLoaded", isLoaded);
+            data.put("status", isLoaded ? "READY" : "LOADING");
+            data.put("message", isLoaded ? "人脸库已加载完成" : "人脸库正在加载中");
+
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("检查人脸库状态失败", e);
+            return ApiResult.error("检查人脸库状态失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 人脸比对(1:N)- 与sys_person表比对 ====================
+
+    @ApiOperation("人脸比对(1:N)- 通过base64图片与sys_person表人脸库比对")
+    @PostMapping("/compare/person")
+    public ApiResult<Map<String, Object>> compareFaceWithPersonLib(
+            @ApiParam("入参 JSON: {\"faceBase\":\"...\",\"threshold\":0.7},默认相似度阈值 0.7(约 70%)") @RequestBody Map<String, Object> body) {
+        try {
+            String faceBase = body != null ? (String) body.get("faceBase") : null;
+            Object thresholdObj = body != null ? body.get("threshold") : null;
+            float threshold = thresholdObj instanceof Number ? ((Number) thresholdObj).floatValue() : 0.70f;
+
+            if (faceBase == null || faceBase.isEmpty()) {
+                return comparePersonFailureResponse("faceBase不能为空", null);
+            }
+
+            int sep = faceBase.indexOf(',');
+            String targetPayload = sep >= 0 ? faceBase.substring(sep + 1) : faceBase;
+            byte[] imageBytes = Base64.getDecoder().decode(targetPayload);
+            Image targetImage = SmartImageFactory.getInstance().fromInputStream(new ByteArrayInputStream(imageBytes));
+
+            // 提取目标人脸特征(兼作是否有人脸;不用独立 detection,避免模型路径未配时初始化失败)
+            R<float[]> featureResult = faceRecognitionService.extractTopFaceFeature(targetImage);
+            if (!featureResult.isSuccess() || featureResult.getData() == null) {
+                return comparePersonFailureResponse("未检测到人脸,请上传包含清晰人脸的图片",
+                        Collections.singletonMap("hasFace", false));
+            }
+            float[] targetFeatures = featureResult.getData();
+
+            List<SysPerson> personList = sysPersonMapper.selectAllValidFace();
+            if (personList == null || personList.isEmpty()) {
+                return comparePersonFailureResponse("人脸库为空,请先录入人员人脸信息", null);
+            }
+
+            float maxSimilarity = 0f;
+            SysPerson matchedPerson = null;
+            for (SysPerson person : personList) {
+                try {
+                    String personFaceBase = person.getFaceBase();
+                    if (personFaceBase == null || personFaceBase.isEmpty()) {
+                        continue;
+                    }
+                    sep = personFaceBase.indexOf(',');
+                    String personPayload = sep >= 0 ? personFaceBase.substring(sep + 1) : personFaceBase;
+                    byte[] personImageBytes = Base64.getDecoder().decode(personPayload);
+                    R<float[]> personFeatureResult = faceRecognitionService.extractTopFaceFeature(personImageBytes);
+                    if (!personFeatureResult.isSuccess() || personFeatureResult.getData() == null) {
+                        continue;
+                    }
+                    float similarity = faceRecognitionService.calculateSimilarity(targetFeatures, personFeatureResult.getData());
+                    if (similarity > maxSimilarity) {
+                        maxSimilarity = similarity;
+                        matchedPerson = person;
+                    }
+                } catch (Exception e) {
+                    log.warn("比对人员ID={}的人脸时发生异常,已跳过: {}", person.getId(), e.getMessage());
+                }
+            }
+
+            Map<String, Object> data = new HashMap<>(8);
+            boolean matched = maxSimilarity >= threshold && matchedPerson != null;
+            data.put("success", matched);
+            data.put("hasFace", true);
+            data.put("faceCount", 1);
+            data.put("threshold", threshold);
+            data.put("maxSimilarity", maxSimilarity);
+            data.put("isMatch", matched);
+            if (matched) {
+                Map<String, Object> personInfo = new HashMap<>(8);
+                personInfo.put("id", matchedPerson.getId());
+                personInfo.put("fullName", matchedPerson.getFullName());
+                personInfo.put("faceName", matchedPerson.getFaceName());
+                personInfo.put("cardNum", matchedPerson.getCardNum());
+                data.put("matchedPerson", personInfo);
+            } else {
+                data.put("matchedPerson", null);
+                data.put("message", "未找到匹配的人员");
+            }
+            return ApiResult.success(data);
+        } catch (Exception e) {
+            log.error("人脸1:N比对失败", e);
+            return comparePersonFailureResponse("人脸比对失败: " + e.getMessage(), null);
+        }
+    }
+
+    // ==================== 辅助方法 ====================
+
+    /**
+     * 人脸与 sys_person 比对:业务失败时仍返回 HTTP 成功包装,data 内 success=false + message 供调用方判断。
+     */
+    private ApiResult<Map<String, Object>> comparePersonFailureResponse(String message, Map<String, Object> extra) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("success", false);
+        data.put("message", message);
+        if (extra != null && !extra.isEmpty()) {
+            data.putAll(extra);
+        }
+        return ApiResult.success(data);
+    }
+
+    /**
+     * 获取置信度等级
+     */
+    private String getConfidenceLevel(float similarity, float threshold) {
+        if (similarity >= threshold + 0.15f) {
+            return "HIGH";
+        } else if (similarity >= threshold) {
+            return "MEDIUM";
+        } else if (similarity >= threshold - 0.10f) {
+            return "LOW";
+        } else {
+            return "VERY_LOW";
+        }
+    }
+}

+ 54 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/SmartFaceDetectionService.java

@@ -0,0 +1,54 @@
+package com.usky.eg.service;
+
+import ai.djl.modality.cv.Image;
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.common.entity.R;
+
+/**
+ * SmartJavaAI人脸检测服务接口
+ * 
+ * @author SmartJavaAI Integration
+ */
+public interface SmartFaceDetectionService {
+
+    /**
+     * 检测图片中的人脸
+     * 
+     * @param image DJL Image对象
+     * @return 检测结果
+     */
+    R<DetectionResponse> detectFaces(Image image);
+
+    /**
+     * 检测图片中的人脸(通过文件路径)
+     * 
+     * @param imagePath 图片路径
+     * @return 检测结果
+     */
+    R<DetectionResponse> detectFaces(String imagePath);
+
+    /**
+     * 检测图片中的人脸(通过字节数组)
+     * 
+     * @param imageBytes 图片字节数组
+     * @return 检测结果
+     */
+    R<DetectionResponse> detectFaces(byte[] imageBytes);
+
+    /**
+     * 检测并绘制人脸框
+     * 
+     * @param image DJL Image对象
+     * @return 检测结果(包含绘制后的图片)
+     */
+    R<DetectionResponse> detectAndDraw(Image image);
+
+    /**
+     * 检测并绘制人脸框(通过文件路径)
+     * 
+     * @param imagePath 图片路径
+     * @param outputPath 输出路径
+     * @return 检测结果
+     */
+    R<DetectionResponse> detectAndDraw(String imagePath, String outputPath);
+}

+ 165 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/SmartFaceRecognitionService.java

@@ -0,0 +1,165 @@
+package com.usky.eg.service;
+
+import ai.djl.modality.cv.Image;
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.common.entity.R;
+import cn.smartjavaai.common.entity.face.FaceSearchResult;
+import cn.smartjavaai.face.entity.FaceRegisterInfo;
+import cn.smartjavaai.face.entity.FaceSearchParams;
+import cn.smartjavaai.face.vector.entity.FaceVector;
+
+import java.util.List;
+
+/**
+ * SmartJavaAI人脸识别服务接口
+ * 
+ * @author SmartJavaAI Integration
+ */
+public interface SmartFaceRecognitionService {
+
+    /**
+     * 提取人脸特征(提取所有人脸)
+     * 
+     * @param image DJL Image对象
+     * @return 特征提取结果
+     */
+    R<DetectionResponse> extractFeatures(Image image);
+
+    /**
+     * 提取人脸特征(提取置信度最高的人脸)
+     * 
+     * @param image DJL Image对象
+     * @return 特征数组
+     */
+    R<float[]> extractTopFaceFeature(Image image);
+
+    /**
+     * 提取人脸特征(通过文件路径)
+     * 
+     * @param imagePath 图片路径
+     * @return 特征数组
+     */
+    R<float[]> extractTopFaceFeature(String imagePath);
+
+    /**
+     * 提取人脸特征(通过字节数组)
+     * 
+     * @param imageBytes 图片字节数组
+     * @return 特征数组
+     */
+    R<float[]> extractTopFaceFeature(byte[] imageBytes);
+
+    /**
+     * 人脸比对(1:1)- 基于图像直接比对
+     * 
+     * @param image1 第一张图片
+     * @param image2 第二张图片
+     * @return 相似度
+     */
+    R<Float> compareFeatures(Image image1, Image image2);
+
+    /**
+     * 人脸比对(1:1)- 基于图像路径比对
+     * 
+     * @param imagePath1 第一张图片路径
+     * @param imagePath2 第二张图片路径
+     * @return 相似度
+     */
+    R<Float> compareFeatures(String imagePath1, String imagePath2);
+
+    /**
+     * 人脸比对(1:1)- 基于特征值比对
+     * 
+     * @param features1 第一个特征数组
+     * @param features2 第二个特征数组
+     * @return 相似度
+     */
+    float calculateSimilarity(float[] features1, float[] features2);
+
+    /**
+     * 注册人脸
+     * 
+     * @param registerInfo 注册信息
+     * @param image 人脸图片
+     * @return 注册ID
+     */
+    R<String> registerFace(FaceRegisterInfo registerInfo, Image image);
+
+    /**
+     * 注册人脸(通过特征值)
+     * 
+     * @param registerInfo 注册信息
+     * @param features 人脸特征
+     * @return 注册ID
+     */
+    R<String> registerFace(FaceRegisterInfo registerInfo, float[] features);
+
+    /**
+     * 更新人脸
+     * 
+     * @param registerInfo 注册信息(必须包含ID)
+     * @param image 人脸图片
+     * @return 是否成功
+     */
+    R<Boolean> updateFace(FaceRegisterInfo registerInfo, Image image);
+
+    /**
+     * 搜索人脸(1:N)
+     * 
+     * @param features 人脸特征
+     * @param searchParams 搜索参数
+     * @return 搜索结果列表
+     */
+    List<FaceSearchResult> searchFace(float[] features, FaceSearchParams searchParams);
+
+    /**
+     * 搜索人脸(1:N)- 通过图片
+     * 
+     * @param image 人脸图片
+     * @param searchParams 搜索参数
+     * @return 搜索结果列表
+     */
+    List<FaceSearchResult> searchFace(Image image, FaceSearchParams searchParams);
+
+    /**
+     * 删除人脸
+     * 
+     * @param faceId 人脸ID
+     * @return 是否成功
+     */
+    R<Boolean> deleteFace(String faceId);
+
+    /**
+     * 根据ID获取人脸信息
+     * 
+     * @param faceId 人脸ID
+     * @return 人脸信息
+     */
+    R<FaceVector> getFaceById(String faceId);
+
+    /**
+     * 分页获取人脸列表
+     * 
+     * @param page 页码
+     * @param pageSize 每页大小
+     * @return 人脸列表
+     */
+    R<List<FaceVector>> listFaces(int page, int pageSize);
+
+    /**
+     * 检查人脸库是否加载完成
+     * 
+     * @return 是否加载完成
+     */
+    boolean isLoadFaceCompleted();
+
+    /**
+     * 绘制搜索结果
+     * 
+     * @param image 原图
+     * @param searchParams 搜索参数
+     * @param displayField 显示字段名
+     * @return 绘制后的图片
+     */
+    Image drawSearchResult(Image image, FaceSearchParams searchParams, String displayField);
+}

+ 161 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/SmartFaceDetectionServiceImpl.java

@@ -0,0 +1,161 @@
+package com.usky.eg.service.impl;
+
+import ai.djl.modality.cv.Image;
+import cn.smartjavaai.common.cv.SmartImageFactory;
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.common.entity.R;
+import cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.face.config.FaceDetConfig;
+import cn.smartjavaai.face.constant.FaceDetectConstant;
+import cn.smartjavaai.face.enums.FaceDetModelEnum;
+import cn.smartjavaai.face.factory.FaceDetModelFactory;
+import cn.smartjavaai.face.model.facedect.FaceDetModel;
+import com.usky.eg.config.SmartFaceConfig;
+import com.usky.eg.service.SmartFaceDetectionService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+
+/**
+ * SmartJavaAI人脸检测服务实现类
+ * 
+ * @author SmartJavaAI Integration
+ */
+@Slf4j
+@Service
+@ConditionalOnProperty(prefix = "smartface", name = "enabled", havingValue = "true")
+public class SmartFaceDetectionServiceImpl implements SmartFaceDetectionService {
+
+    @Autowired
+    private SmartFaceConfig smartFaceConfig;
+
+    private FaceDetModel faceDetModel;
+
+    @PostConstruct
+    public void init() {
+        // 设置图片处理引擎为OpenCV
+        SmartImageFactory.setEngine(SmartImageFactory.Engine.OPENCV);
+        
+        // 如果配置为延迟加载,则跳过初始化
+        if (smartFaceConfig.isLazyLoad()) {
+            log.info("SmartJavaAI人脸检测模型配置为延迟加载,将在首次使用时初始化");
+            return;
+        }
+        
+        // 立即加载模型
+        loadModel();
+    }
+    
+    /**
+     * 加载人脸检测模型
+     */
+    private synchronized void loadModel() {
+        if (faceDetModel != null) {
+            return; // 已经加载过了
+        }
+        
+        try {
+            log.info("初始化SmartJavaAI人脸检测模型...");
+            
+            // 创建人脸检测配置
+            FaceDetConfig config = new FaceDetConfig();
+            
+            // 设置模型类型
+            SmartFaceConfig.FaceDetectionConfig detConfig = smartFaceConfig.getDetection();
+            config.setModelEnum(FaceDetModelEnum.valueOf(detConfig.getModelType()));
+            
+            // 设置模型路径
+            if (detConfig.getModelPath() != null && !detConfig.getModelPath().isEmpty()) {
+                config.setModelPath(detConfig.getModelPath());
+            } else {
+                log.warn("未配置人脸检测模型路径,将使用默认路径");
+            }
+            
+            // 设置置信度阈值
+            config.setConfidenceThreshold(detConfig.getConfidenceThreshold());
+            
+            // 设置NMS阈值
+            config.setNmsThresh(detConfig.getNmsThreshold());
+            
+            // 设置设备类型
+            config.setDevice(DeviceEnum.valueOf(detConfig.getDevice()));
+            
+            // 创建模型实例
+            faceDetModel = FaceDetModelFactory.getInstance().getModel(config);
+            
+            log.info("SmartJavaAI人脸检测模型初始化成功!模型类型: {}, 设备: {}", 
+                    detConfig.getModelType(), detConfig.getDevice());
+        } catch (Exception e) {
+            log.error("SmartJavaAI人脸检测模型初始化失败", e);
+            throw new RuntimeException("人脸检测模型初始化失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public R<DetectionResponse> detectFaces(Image image) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceDetModel == null) {
+                loadModel();
+            }
+            return faceDetModel.detect(image);
+        } catch (Exception e) {
+            log.error("人脸检测失败", e);
+            return R.fail(500, "人脸检测失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<DetectionResponse> detectFaces(String imagePath) {
+        try {
+            Image image = SmartImageFactory.getInstance().fromFile(imagePath);
+            return detectFaces(image);
+        } catch (Exception e) {
+            log.error("从文件路径检测人脸失败: {}", imagePath, e);
+            return R.fail(500, "从文件路径检测人脸失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<DetectionResponse> detectFaces(byte[] imageBytes) {
+        try {
+            Image image = SmartImageFactory.getInstance().fromInputStream(
+                    new java.io.ByteArrayInputStream(imageBytes));
+            return detectFaces(image);
+        } catch (Exception e) {
+            log.error("从字节数组检测人脸失败", e);
+            return R.fail(500, "从字节数组检测人脸失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<DetectionResponse> detectAndDraw(Image image) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceDetModel == null) {
+                loadModel();
+            }
+            return faceDetModel.detectAndDraw(image);
+        } catch (Exception e) {
+            log.error("检测并绘制人脸失败", e);
+            return R.fail(500, "检测并绘制人脸失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<DetectionResponse> detectAndDraw(String imagePath, String outputPath) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceDetModel == null) {
+                loadModel();
+            }
+            return faceDetModel.detectAndDraw(imagePath, outputPath);
+        } catch (Exception e) {
+            log.error("检测并绘制人脸失败: {} -> {}", imagePath, outputPath, e);
+            return R.fail(500, "检测并绘制人脸失败: " + e.getMessage());
+        }
+    }
+}

+ 571 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/SmartFaceRecognitionServiceImpl.java

@@ -0,0 +1,571 @@
+package com.usky.eg.service.impl;
+
+import ai.djl.modality.cv.Image;
+import cn.smartjavaai.common.cv.SmartImageFactory;
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.common.entity.R;
+import cn.smartjavaai.common.entity.face.FaceSearchResult;
+import cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.common.enums.SimilarityType;
+import cn.smartjavaai.face.config.FaceRecConfig;
+import cn.smartjavaai.face.entity.FaceRegisterInfo;
+import cn.smartjavaai.face.entity.FaceSearchParams;
+import cn.smartjavaai.face.enums.FaceRecModelEnum;
+import cn.smartjavaai.face.enums.IdStrategy;
+import cn.smartjavaai.face.factory.FaceRecModelFactory;
+import cn.smartjavaai.face.model.facedect.FaceDetModel;
+import cn.smartjavaai.face.model.facerec.FaceRecModel;
+import cn.smartjavaai.face.vector.config.MilvusConfig;
+import cn.smartjavaai.face.vector.config.SQLiteConfig;
+import cn.smartjavaai.face.vector.entity.FaceVector;
+import com.usky.eg.config.SmartFaceConfig;
+import com.usky.eg.service.SmartFaceDetectionService;
+import com.usky.eg.service.SmartFaceRecognitionService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+/**
+ * SmartJavaAI人脸识别服务实现类
+ * 
+ * @author SmartJavaAI Integration
+ */
+@Slf4j
+@Service
+@ConditionalOnProperty(prefix = "smartface", name = "enabled", havingValue = "true")
+public class SmartFaceRecognitionServiceImpl implements SmartFaceRecognitionService {
+
+    @Autowired
+    private SmartFaceConfig smartFaceConfig;
+
+    @Autowired
+    private SmartFaceDetectionService faceDetectionService;
+
+    private FaceRecModel faceRecModel;
+    
+    private String tempModelDir;
+
+    @PostConstruct
+    public void init() {
+        // 设置图片处理引擎为OpenCV
+        SmartImageFactory.setEngine(SmartImageFactory.Engine.OPENCV);
+        
+        // 如果配置为延迟加载,则跳过初始化
+        if (smartFaceConfig.isLazyLoad()) {
+            log.info("SmartJavaAI人脸识别模型配置为延迟加载,将在首次使用时初始化");
+            return;
+        }
+        
+        // 立即加载模型
+        loadModel();
+    }
+    
+    /**
+     * 加载人脸识别模型
+     */
+    private synchronized void loadModel() {
+        if (faceRecModel != null) {
+            return; // 已经加载过了
+        }
+        
+        try {
+            log.info("初始化SmartJavaAI人脸识别模型...");
+            
+            // 复制模型文件到临时目录(解决中文路径问题)
+            try {
+                tempModelDir = copyModelsToTempDir();
+            } catch (IOException e) {
+                log.error("复制模型文件到临时目录失败", e);
+                throw new RuntimeException("复制模型文件失败: " + e.getMessage(), e);
+            }
+            
+            // 创建人脸识别配置
+            FaceRecConfig config = new FaceRecConfig();
+            
+            // 设置模型类型
+            SmartFaceConfig.FaceRecognitionConfig recConfig = smartFaceConfig.getRecognition();
+            config.setModelEnum(FaceRecModelEnum.valueOf(recConfig.getModelType()));
+            
+            // 使用临时目录中的模型路径
+            if (tempModelDir != null) {
+                String recModelPath = Paths.get(tempModelDir, "model_ir_se50.pt").toString();
+                config.setModelPath(recModelPath);
+                log.info("使用临时目录中的识别模型: {}", recModelPath);
+            } else if (recConfig.getModelPath() != null && !recConfig.getModelPath().isEmpty()) {
+                config.setModelPath(recConfig.getModelPath());
+            } else {
+                log.warn("未配置人脸识别模型路径,将使用默认路径");
+            }
+            
+            // 设置裁剪和对齐参数
+            config.setCropFace(recConfig.isCropFace());
+            config.setAlign(recConfig.isAlign());
+            
+            // 设置设备类型
+            config.setDevice(DeviceEnum.valueOf(recConfig.getDevice()));
+            
+            // 设置人脸检测模型(从人脸检测服务获取)
+            // 注意:这里需要获取FaceDetModel实例,暂时使用独立的检测模型
+            config.setDetectModel(createFaceDetModel());
+            
+            // 配置向量数据库
+            SmartFaceConfig.VectorDbConfig vectorDbConfig = smartFaceConfig.getVectorDb();
+            if ("SQLITE".equalsIgnoreCase(vectorDbConfig.getType())) {
+                SQLiteConfig sqliteConfig = new SQLiteConfig();
+                sqliteConfig.setDbPath(vectorDbConfig.getSqlitePath());
+                sqliteConfig.setSimilarityType(SimilarityType.IP);
+                config.setVectorDBConfig(sqliteConfig);
+                log.info("使用SQLite向量数据库: {}", vectorDbConfig.getSqlitePath());
+            } else if ("MILVUS".equalsIgnoreCase(vectorDbConfig.getType())) {
+                MilvusConfig milvusConfig = new MilvusConfig();
+                SmartFaceConfig.MilvusConfig milvusCfg = vectorDbConfig.getMilvus();
+                milvusConfig.setHost(milvusCfg.getHost());
+                milvusConfig.setPort(milvusCfg.getPort());
+                milvusConfig.setUsername(milvusCfg.getUsername());
+                milvusConfig.setPassword(milvusCfg.getPassword());
+                milvusConfig.setCollectionName(milvusCfg.getCollectionName());
+                milvusConfig.setIdStrategy(IdStrategy.AUTO);
+                config.setVectorDBConfig(milvusConfig);
+                log.info("使用Milvus向量数据库: {}:{}", milvusCfg.getHost(), milvusCfg.getPort());
+            }
+            
+            // 创建模型实例
+            faceRecModel = FaceRecModelFactory.getInstance().getModel(config);
+            
+            log.info("SmartJavaAI人脸识别模型初始化成功!模型类型: {}, 设备: {}", 
+                    recConfig.getModelType(), recConfig.getDevice());
+        } catch (Exception e) {
+            log.error("SmartJavaAI人脸识别模型初始化失败", e);
+            throw new RuntimeException("人脸识别模型初始化失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 从classpath复制模型文件到临时目录(解决中文路径问题)
+     */
+    private String copyModelsToTempDir() throws IOException {
+        // 使用系统临时目录,避免中文路径问题
+        String tempDir = System.getProperty("java.io.tmpdir");
+        Path modelTempPath = Paths.get(tempDir, "smartface-models");
+        
+        // 创建临时目录
+        Files.createDirectories(modelTempPath);
+        
+        // 复制MTCNN模型文件
+        Path mtcnnDir = modelTempPath.resolve("mtcnn");
+        Files.createDirectories(mtcnnDir);
+        
+        String[] mtcnnFiles = {"pnet_script.pt", "rnet_script.pt", "onet_script.pt"};
+        for (String fileName : mtcnnFiles) {
+            copyResourceToFile("models/mtcnn/" + fileName, mtcnnDir.resolve(fileName));
+        }
+        
+        // 复制人脸识别模型文件
+        copyResourceToFile("models/model_ir_se50.pt", modelTempPath.resolve("model_ir_se50.pt"));
+        
+        log.info("模型文件已复制到临时目录: {}", modelTempPath.toString());
+        return modelTempPath.toString();
+    }
+    
+    /**
+     * 从classpath复制资源文件到指定路径
+     */
+    private void copyResourceToFile(String resourcePath, Path targetPath) throws IOException {
+        // 如果文件已存在且大小正确,跳过复制
+        if (Files.exists(targetPath)) {
+            try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath)) {
+                if (is != null && Files.size(targetPath) == is.available()) {
+                    log.debug("文件已存在,跳过复制: {}", targetPath);
+                    return;
+                }
+            } catch (Exception e) {
+                // 忽略检查错误,继续复制
+            }
+        }
+        
+        try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath)) {
+            if (is == null) {
+                throw new IOException("无法找到资源文件: " + resourcePath);
+            }
+            Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING);
+            log.debug("已复制资源文件: {} -> {}", resourcePath, targetPath);
+        }
+    }
+
+    /**
+     * 创建人脸检测模型(用于人脸识别)
+     */
+    private FaceDetModel createFaceDetModel() {
+        try {
+            cn.smartjavaai.face.config.FaceDetConfig config = new cn.smartjavaai.face.config.FaceDetConfig();
+            SmartFaceConfig.FaceDetectionConfig detConfig = smartFaceConfig.getDetection();
+            
+            config.setModelEnum(cn.smartjavaai.face.enums.FaceDetModelEnum.valueOf(detConfig.getModelType()));
+            
+            // 使用临时目录中的模型路径
+            if (tempModelDir != null) {
+                String mtcnnPath = Paths.get(tempModelDir, "mtcnn").toString();
+                config.setModelPath(mtcnnPath);
+                log.info("使用临时目录中的MTCNN模型: {}", mtcnnPath);
+            } else if (detConfig.getModelPath() != null && !detConfig.getModelPath().isEmpty()) {
+                config.setModelPath(detConfig.getModelPath());
+            }
+            
+            config.setConfidenceThreshold(detConfig.getConfidenceThreshold());
+            config.setNmsThresh(detConfig.getNmsThreshold());
+            config.setDevice(DeviceEnum.valueOf(detConfig.getDevice()));
+            
+            return cn.smartjavaai.face.factory.FaceDetModelFactory.getInstance().getModel(config);
+        } catch (Exception e) {
+            log.error("创建人脸检测模型失败", e);
+            throw new RuntimeException("创建人脸检测模型失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public R<DetectionResponse> extractFeatures(Image image) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.extractFeatures(image);
+        } catch (Exception e) {
+            log.error("提取人脸特征失败", e);
+            return R.fail(500, "提取人脸特征失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<float[]> extractTopFaceFeature(Image image) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.extractTopFaceFeature(image);
+        } catch (Exception e) {
+            log.error("提取人脸特征失败", e);
+            return R.fail(500, "提取人脸特征失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<float[]> extractTopFaceFeature(String imagePath) {
+        try {
+            Image image = loadImageFromPathOrUrl(imagePath);
+            return extractTopFaceFeature(image);
+        } catch (Exception e) {
+            log.error("从文件路径提取人脸特征失败: {}", imagePath, e);
+            return R.fail(500, "从文件路径提取人脸特征失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<float[]> extractTopFaceFeature(byte[] imageBytes) {
+        try {
+            Image image = SmartImageFactory.getInstance().fromInputStream(
+                    new java.io.ByteArrayInputStream(imageBytes));
+            return extractTopFaceFeature(image);
+        } catch (Exception e) {
+            log.error("从字节数组提取人脸特征失败", e);
+            return R.fail(500, "从字节数组提取人脸特征失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<Float> compareFeatures(Image image1, Image image2) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.featureComparison(image1, image2);
+        } catch (Exception e) {
+            log.error("人脸比对失败", e);
+            return R.fail(500, "人脸比对失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<Float> compareFeatures(String imagePath1, String imagePath2) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            
+            // 判断是否为 URL,如果是则下载后处理
+            Image image1 = loadImageFromPathOrUrl(imagePath1);
+            Image image2 = loadImageFromPathOrUrl(imagePath2);
+            
+            return faceRecModel.featureComparison(image1, image2);
+        } catch (Exception e) {
+            log.error("人脸比对失败: {} vs {}", imagePath1, imagePath2, e);
+            return R.fail(500, "人脸比对失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 从路径或URL加载图片
+     * 支持本地文件路径和HTTP/HTTPS URL
+     */
+    private Image loadImageFromPathOrUrl(String path) throws Exception {
+        if (path == null || path.isEmpty()) {
+            throw new IllegalArgumentException("图片路径不能为空");
+        }
+        
+        // 判断是否为 URL
+        if (path.startsWith("http://") || path.startsWith("https://")) {
+            log.debug("从URL加载图片: {}", path);
+            try {
+                // 对URL进行编码处理,支持中文和特殊字符
+                String encodedUrl = encodeUrl(path);
+                log.debug("编码后的URL: {}", encodedUrl);
+                
+                java.net.URL url = new java.net.URL(encodedUrl);
+                java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
+                connection.setRequestMethod("GET");
+                connection.setConnectTimeout(10000);
+                connection.setReadTimeout(10000);
+                // 设置User-Agent,避免某些服务器拒绝请求
+                connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+                
+                int responseCode = connection.getResponseCode();
+                if (responseCode != 200) {
+                    throw new RuntimeException("HTTP请求失败,响应码: " + responseCode);
+                }
+                
+                try (InputStream is = connection.getInputStream()) {
+                    return SmartImageFactory.getInstance().fromInputStream(is);
+                }
+            } catch (Exception e) {
+                log.error("从URL加载图片失败: {}", path, e);
+                throw new RuntimeException("从URL加载图片失败: " + e.getMessage(), e);
+            }
+        } else {
+            // 本地文件路径
+            log.debug("从本地文件加载图片: {}", path);
+            File file = new File(path);
+            if (!file.exists()) {
+                throw new RuntimeException("文件不存在: " + path);
+            }
+            return SmartImageFactory.getInstance().fromFile(path);
+        }
+    }
+    
+    /**
+     * 对URL进行编码,支持中文和特殊字符
+     */
+    private String encodeUrl(String url) {
+        try {
+            // 分离协议、主机和路径
+            int protocolEnd = url.indexOf("://");
+            if (protocolEnd == -1) {
+                return url;
+            }
+            
+            String protocol = url.substring(0, protocolEnd + 3);
+            String remaining = url.substring(protocolEnd + 3);
+            
+            int pathStart = remaining.indexOf('/');
+            if (pathStart == -1) {
+                return url; // 没有路径部分
+            }
+            
+            String hostPort = remaining.substring(0, pathStart);
+            String path = remaining.substring(pathStart);
+            
+            // 对路径部分进行编码
+            String[] pathParts = path.split("/");
+            StringBuilder encodedPath = new StringBuilder();
+            for (String part : pathParts) {
+                if (!part.isEmpty()) {
+                    encodedPath.append("/").append(java.net.URLEncoder.encode(part, "UTF-8")
+                            .replace("+", "%20")  // 空格用%20而不是+
+                            .replace("%2F", "/")  // 不编码斜杠
+                            .replace("%3A", ":")  // 不编码冒号
+                    );
+                }
+            }
+            
+            return protocol + hostPort + encodedPath.toString();
+        } catch (Exception e) {
+            log.warn("URL编码失败,使用原始URL: {}", url, e);
+            return url;
+        }
+    }
+
+    @Override
+    public float calculateSimilarity(float[] features1, float[] features2) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.calculSimilar(features1, features2);
+        } catch (Exception e) {
+            log.error("计算相似度失败", e);
+            throw new RuntimeException("计算相似度失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public R<String> registerFace(FaceRegisterInfo registerInfo, Image image) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.register(registerInfo, image);
+        } catch (Exception e) {
+            log.error("注册人脸失败", e);
+            return R.fail(500, "注册人脸失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<String> registerFace(FaceRegisterInfo registerInfo, float[] features) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.register(registerInfo, features);
+        } catch (Exception e) {
+            log.error("注册人脸失败", e);
+            return R.fail(500, "注册人脸失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<Boolean> updateFace(FaceRegisterInfo registerInfo, Image image) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            faceRecModel.upsertFace(registerInfo, image);
+            return R.ok(true);
+        } catch (Exception e) {
+            log.error("更新人脸失败", e);
+            return R.fail(500, "更新人脸失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public List<FaceSearchResult> searchFace(float[] features, FaceSearchParams searchParams) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.search(features, searchParams);
+        } catch (Exception e) {
+            log.error("搜索人脸失败", e);
+            throw new RuntimeException("搜索人脸失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public List<FaceSearchResult> searchFace(Image image, FaceSearchParams searchParams) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            
+            // 提取人脸特征
+            R<float[]> featureResult = extractTopFaceFeature(image);
+            if (!featureResult.isSuccess()) {
+                throw new RuntimeException("提取人脸特征失败: " + featureResult.getMessage());
+            }
+            
+            return searchFace(featureResult.getData(), searchParams);
+        } catch (Exception e) {
+            log.error("搜索人脸失败", e);
+            throw new RuntimeException("搜索人脸失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public R<Boolean> deleteFace(String faceId) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            faceRecModel.removeRegister(faceId);
+            return R.ok(true);
+        } catch (Exception e) {
+            log.error("删除人脸失败: {}", faceId, e);
+            return R.fail(500, "删除人脸失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<FaceVector> getFaceById(String faceId) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.getFaceInfoById(faceId);
+        } catch (Exception e) {
+            log.error("获取人脸信息失败: {}", faceId, e);
+            return R.fail(500, "获取人脸信息失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<List<FaceVector>> listFaces(int page, int pageSize) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.listFaces(page, pageSize);
+        } catch (Exception e) {
+            log.error("获取人脸列表失败", e);
+            return R.fail(500, "获取人脸列表失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean isLoadFaceCompleted() {
+        try {
+            if (faceRecModel == null) {
+                return false;
+            }
+            return faceRecModel.isLoadFaceCompleted();
+        } catch (Exception e) {
+            log.error("检查人脸库加载状态失败", e);
+            return false;
+        }
+    }
+
+    @Override
+    public Image drawSearchResult(Image image, FaceSearchParams searchParams, String displayField) {
+        try {
+            // 如果模型未加载,尝试加载
+            if (faceRecModel == null) {
+                loadModel();
+            }
+            return faceRecModel.drawSearchResult(image, searchParams, displayField);
+        } catch (Exception e) {
+            log.error("绘制搜索结果失败", e);
+            throw new RuntimeException("绘制搜索结果失败: " + e.getMessage(), e);
+        }
+    }
+}

BIN
service-eg/service-eg-biz/src/main/resources/models/model_ir_se50.pt


BIN
service-eg/service-eg-biz/src/main/resources/models/mtcnn/onet_script.pt


BIN
service-eg/service-eg-biz/src/main/resources/models/mtcnn/pnet_script.pt


BIN
service-eg/service-eg-biz/src/main/resources/models/mtcnn/rnet_script.pt


+ 199 - 0
service-eg/service-eg-biz/src/main/resources/smartface-config-example.yml

@@ -0,0 +1,199 @@
+# SmartJavaAI人脸识别配置示例
+
+# ============================================
+# SmartJavaAI人脸识别配置
+# ============================================
+smartface:
+  # 是否启用人脸识别功能
+  enabled: true
+  
+  # 人脸检测配置
+  detection:
+    # 模型类型选择:
+    # - MTCNN: 均衡型,准确度和速度兼顾(推荐)
+    # - RETINA_FACE: 高精度型,准确度最高但速度较慢
+    # - YOLOV5_FACE_320: 高速型,速度快但准确度略低
+    # - YOLOV5_FACE_640: 均衡型,比320更准确但速度稍慢
+    # - SEETA_FACE6_MODEL: 国产模型,不支持macOS
+    model-type: MTCNN
+    
+    # 模型路径(请下载模型并配置本地绝对路径)
+    # 下载地址: https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234
+    # Windows示例: D:/models/face_detection/mtcnn
+    # Linux示例: /opt/models/face_detection/mtcnn
+    model-path: /path/to/models/face_detection/mtcnn
+    
+    # 置信度阈值(0-1之间)
+    # 值越大越严格,可能漏检;值越小越宽松,可能误检
+    # 推荐值: 0.5
+    confidence-threshold: 0.5
+    
+    # NMS阈值(Non-Maximum Suppression,用于去除重复检测框)
+    # 当两个框的重叠度超过该值时,只保留一个
+    # 推荐值: 0.4
+    nms-threshold: 0.4
+    
+    # 设备类型: CPU 或 GPU
+    # 如果有NVIDIA GPU且安装了CUDA,可以设置为GPU以加速推理
+    device: CPU
+  
+  # 人脸识别配置
+  recognition:
+    # 模型类型选择:
+    # - INSIGHT_FACE_IRSE50_MODEL: 高精度模型,512维特征,准确度最高(推荐)
+    # - INSIGHT_FACE_MOBILE_FACENET_MODEL: 轻量级模型,128维特征,速度快
+    # - FACE_NET_MODEL: FaceNet模型,128维特征
+    model-type: INSIGHT_FACE_IRSE50_MODEL
+    
+    # 模型路径(请下载模型并配置本地绝对路径)
+    # 下载地址: https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234
+    # Windows示例: D:/models/face_recognition/model_ir_se50.pt
+    # Linux示例: /opt/models/face_recognition/model_ir_se50.pt
+    model-path: /path/to/models/face_recognition/model_ir_se50.pt
+    
+    # 是否裁剪人脸
+    # true: 自动检测并裁剪人脸区域(推荐)
+    # false: 使用整张图片(仅当图片已经是裁剪好的人脸时使用)
+    crop-face: true
+    
+    # 是否启用人脸对齐
+    # true: 对人脸进行对齐处理,提高准确度但降低速度(推荐)
+    # false: 不进行对齐,速度更快但准确度略低
+    align: true
+    
+    # 相似度阈值(不同模型阈值不同)
+    # InsightFace-IRSE50: 推荐 0.62
+    # InsightFace-MobileFaceNet: 推荐 0.60
+    # FaceNet: 推荐 0.70
+    similarity-threshold: 0.62
+    
+    # 设备类型: CPU 或 GPU
+    device: CPU
+  
+  # 向量数据库配置
+  vector-db:
+    # 数据库类型: SQLITE 或 MILVUS
+    # SQLITE: 轻量级,适合小规模应用(<10万人脸)
+    # MILVUS: 高性能向量数据库,适合大规模应用(>10万人脸)
+    type: SQLITE
+    
+    # SQLite数据库路径(当type=SQLITE时使用)
+    # 相对路径: 相对于应用启动目录
+    # 绝对路径: 完整路径
+    sqlite-path: ./data/face_db.sqlite
+    
+    # Milvus配置(当type=MILVUS时使用)
+    milvus:
+      # Milvus服务器地址
+      host: 127.0.0.1
+      # Milvus服务器端口
+      port: 19530
+      # 用户名(可选,如果Milvus启用了认证)
+      username: 
+      # 密码(可选,如果Milvus启用了认证)
+      password: 
+      # 集合名称(类似于数据库表名)
+      collection-name: face_collection
+
+# ============================================
+# 推荐配置方案
+# ============================================
+
+# 方案一:均衡型(推荐用于生产环境)
+# smartface:
+#   enabled: true
+#   detection:
+#     model-type: MTCNN
+#     model-path: /opt/models/face_detection/mtcnn
+#     confidence-threshold: 0.5
+#     nms-threshold: 0.4
+#     device: CPU
+#   recognition:
+#     model-type: INSIGHT_FACE_IRSE50_MODEL
+#     model-path: /opt/models/face_recognition/model_ir_se50.pt
+#     crop-face: true
+#     align: true
+#     similarity-threshold: 0.62
+#     device: CPU
+#   vector-db:
+#     type: SQLITE
+#     sqlite-path: ./data/face_db.sqlite
+
+# 方案二:高精度型(推荐用于安防、金融等高要求场景)
+# smartface:
+#   enabled: true
+#   detection:
+#     model-type: RETINA_FACE
+#     model-path: /opt/models/face_detection/retinaface.pt
+#     confidence-threshold: 0.6
+#     nms-threshold: 0.4
+#     device: GPU
+#   recognition:
+#     model-type: INSIGHT_FACE_IRSE50_MODEL
+#     model-path: /opt/models/face_recognition/model_ir_se50.pt
+#     crop-face: true
+#     align: true
+#     similarity-threshold: 0.65
+#     device: GPU
+#   vector-db:
+#     type: MILVUS
+#     milvus:
+#       host: 127.0.0.1
+#       port: 19530
+#       collection-name: face_collection
+
+# 方案三:高速型(推荐用于实时视频流、考勤打卡等场景)
+# smartface:
+#   enabled: true
+#   detection:
+#     model-type: YOLOV5_FACE_320
+#     model-path: /opt/models/face_detection/yolov5face-n-0.5-320x320.onnx
+#     confidence-threshold: 0.5
+#     nms-threshold: 0.4
+#     device: CPU
+#   recognition:
+#     model-type: INSIGHT_FACE_MOBILE_FACENET_MODEL
+#     model-path: /opt/models/face_recognition/model_mobilefacenet.pt
+#     crop-face: true
+#     align: false
+#     similarity-threshold: 0.60
+#     device: CPU
+#   vector-db:
+#     type: SQLITE
+#     sqlite-path: ./data/face_db.sqlite
+
+# ============================================
+# 性能调优建议
+# ============================================
+
+# 1. CPU环境(无GPU)
+#    - 使用轻量级模型: YOLOV5_FACE_320 + MOBILE_FACENET
+#    - 关闭人脸对齐: align: false
+#    - 使用SQLite数据库
+
+# 2. GPU环境(有NVIDIA GPU)
+#    - 设置device为GPU
+#    - 可以使用高精度模型: RETINA_FACE + IRSE50
+#    - 开启人脸对齐: align: true
+
+# 3. 大规模应用(>10万人脸)
+#    - 使用Milvus向量数据库
+#    - 考虑分布式部署
+#    - 使用GPU加速
+
+# 4. 实时应用(视频流)
+#    - 使用高速模型: YOLOV5_FACE + MOBILE_FACENET 。0
+#    - 关闭人脸对齐
+#    - 降低图片分辨率
+
+# ============================================
+# 注意事项
+# ============================================
+
+# 1. 模型文件必须下载到本地,不支持在线下载
+# 2. 首次启动会加载模型,可能需要10-30秒
+# 3. 确保模型路径有读取权限
+# 4. SQLite数据库文件会自动创建,确保目录有写入权限
+# 5. 如果使用Milvus,需要先启动Milvus服务
+# 6. 不同模型的相似度阈值不同,需要根据实际情况调整
+# 7. 建议在测试环境先验证配置,再部署到生产环境

+ 9 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsProjectController.java

@@ -2,6 +2,7 @@ package com.usky.ems.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.DmpProduct;
 import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.service.EmsEnergyItemCodeService;
 import com.usky.ems.service.EmsProjectService;
@@ -60,6 +61,14 @@ public class EmsProjectController {
         return ApiResult.success(emsProjectService.listAreaDeviceItemCodes(spaceId));
     }
 
+    /**
+     * 按能耗类型查询已绑定产品列表(ems_product_energy_type)
+     */
+    @GetMapping("/product-energy-type/list")
+    public ApiResult<List<DmpProduct>> listProductsByEnergyType(@RequestParam Integer energyType) {
+        return ApiResult.success(emsProjectService.listProductsByEnergyType(energyType));
+    }
+
     /**
      * 产品关联的能源类型(ems_product_energy_type)
      */

+ 6 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectService.java

@@ -1,6 +1,7 @@
 package com.usky.ems.service;
 
 import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.DmpProduct;
 import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.service.vo.EmsDeviceItemCodeSaveRequest;
 import com.usky.ems.service.vo.EmsProductEnergyTypeSaveRequest;
@@ -25,6 +26,11 @@ public interface EmsProjectService {
      */
     void saveProductEnergyTypes(EmsProductEnergyTypeSaveRequest request);
 
+    /**
+     * 按能耗类型查询已绑定产品列表(ems_product_energy_type)
+     */
+    List<DmpProduct> listProductsByEnergyType(Integer energyType);
+
     /**
      * 保存设备关联的能源分项:先删除当前租户下该分项编码的记录,再按设备列表插入
      */

+ 34 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectServiceImpl.java

@@ -7,6 +7,7 @@ import com.usky.common.security.utils.SecurityUtils;
 import com.usky.ems.domain.BaseSpace;
 import com.usky.ems.domain.BaseSpaceGateway;
 import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.DmpProduct;
 import com.usky.ems.domain.EmsDeviceItemCode;
 import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.domain.EmsProductEnergyType;
@@ -14,6 +15,7 @@ import com.usky.ems.domain.EmsProject;
 import com.usky.ems.domain.EmsProjectDeviceSystem;
 import com.usky.ems.mapper.BaseSpaceGatewayMapper;
 import com.usky.ems.mapper.DmpDeviceMapper;
+import com.usky.ems.mapper.DmpProductMapper;
 import com.usky.ems.mapper.EmsDeviceItemCodeMapper;
 import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
 import com.usky.ems.mapper.EmsProductEnergyTypeMapper;
@@ -64,6 +66,8 @@ public class EmsProjectServiceImpl implements EmsProjectService {
     @Autowired
     private DmpDeviceMapper dmpDeviceMapper;
     @Autowired
+    private DmpProductMapper dmpProductMapper;
+    @Autowired
     private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
 
     @Override
@@ -160,6 +164,36 @@ public class EmsProjectServiceImpl implements EmsProjectService {
         }
     }
 
+    @Override
+    public List<DmpProduct> listProductsByEnergyType(Integer energyType) {
+        if (energyType == null) {
+            throw new BusinessException("能耗类型不能为空");
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        List<EmsProductEnergyType> bindings = emsProductEnergyTypeMapper.selectList(
+                Wrappers.<EmsProductEnergyType>lambdaQuery()
+                        .eq(EmsProductEnergyType::getTenantId, tenantId)
+                        .eq(EmsProductEnergyType::getEnergyType, energyType));
+        if (bindings.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Integer> productIds = bindings.stream()
+                .map(EmsProductEnergyType::getProductId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .map(Long::intValue)
+                .collect(Collectors.toList());
+        if (productIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return dmpProductMapper.selectList(
+                Wrappers.<DmpProduct>lambdaQuery()
+                        .in(DmpProduct::getId, productIds)
+                        .eq(DmpProduct::getTenantId, tenantId)
+                        .eq(DmpProduct::getDeleteFlag, 0)
+                        .orderByAsc(DmpProduct::getId));
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void saveDeviceItemCodes(EmsDeviceItemCodeSaveRequest request) {

+ 118 - 0
service-ems/service-ems-biz/src/main/resources/application-dev.yml

@@ -0,0 +1,118 @@
+mybatis:
+  refresh:
+    delay-seconds: 10
+    enabled: true
+    sleep-seconds: 20
+mybatis-plus:
+  configuration:
+    defaultStatementTimeout: 30
+    lazy-loading-enabled: true
+    map-underscore-to-camel-case: true
+  global-config:
+    db-config:
+      id-type: auto
+    mapperRegistryCache: true
+  mapper-locations: classpath*:mapper/**/*.xml
+server:
+  compression:
+    enabled: true
+    mime-types: application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain
+spring:
+  autoconfigure:
+    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
+  cache:
+    ehcache:
+      config: classpath:ehcache.xml
+      enabled: false
+    redis:
+      enabled: true
+  datasource:
+    druid:
+      stat-view-servlet:
+        enabled: true
+        login-password: '@dmin1234'
+        login-username: admin
+        reset-enable: true
+        url-pattern: /druid/*
+    dynamic:
+      datasource:
+        master:
+          password: yt123456
+          url: jdbc:mysql://usky-cloud-mysql:3306/usky-cloud?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowMultiQueries=true
+          username: root
+        taos:
+          driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
+          url: jdbc:TAOS-WS://47.110.48.75:6041/uskydata?charset=UTF-8
+          username: root
+          password: Usky@2025Yt
+          hikari:
+            maximum-pool-size: 50
+            connection-timeout: 60000
+            connection-test-query: SELECT 1
+      druid:
+        initial-size: 5
+        min-idle: 5
+        maxActive: 20
+        maxWait: 60000
+        timeBetweenEvictionRunsMillis: 60000
+        minEvictableIdleTimeMillis: 300000
+        validationQuery: SELECT 1 FROM DUAL
+        testWhileIdle: true
+        testOnBorrow: false
+        testOnReturn: false
+        poolPreparedStatements: true
+        maxPoolPreparedStatementPerConnectionSize: 20
+        filters: stat,slf4j
+        connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
+      primary: master
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    default-property-inclusion: always
+    deserialization:
+      fail-on-unknown-properties: false
+    parser:
+      allow-single-quotes: true
+      allow-unquoted-control-chars: true
+    serialization:
+      fail-on-empty-beans: false
+    time-zone: GMT+8 
+  tenant:
+    enable: false
+  servlet:
+    multipart:
+      max-file-size: 10MB
+      max-request-size: 15MB
+  redis:
+    host: usky-cloud-redis
+    password: 123456
+    port: 6379
+    timeout: 10000  
+
+temp:
+  basedir: C:/Users/pc/Desktop/
+mqtt:
+  completionTimeout: 5000
+  enabled: true
+  keep-alive-interval: 60
+  password: public
+  sub-topics: data-collector,511-XFFJ/+/+/control
+  url: tcp://192.168.10.165:1883
+  username: admin
+# 能耗服务配置
+energy:
+  # 服务端配置
+  server:
+    host: 183.192.66.5
+    port: 9006
+  # 认证配置
+  auth:
+    key: 529f3d2bf15d4d5d
+  # 建筑信息
+  building:
+    id: PT310107BZ4054
+  # 网关配置(多个网关用逗号分隔)
+  gateway:
+    ids: 1,2
+  # 定时任务配置
+  push:
+    cron: "0 */10 * * * ?"  # 每10分钟执行一次

+ 93 - 0
service-meeting/service-meeting-biz/src/main/resources/application-dev.yml

@@ -0,0 +1,93 @@
+mybatis:
+  refresh:
+    delay-seconds: 10
+    enabled: true
+    sleep-seconds: 20
+mybatis-plus:
+  configuration:
+    defaultStatementTimeout: 30
+    lazy-loading-enabled: true
+    map-underscore-to-camel-case: true
+  global-config:
+    db-config:
+      id-type: assign_id
+    mapperRegistryCache: true
+  mapper-locations: classpath*:mapper/**/*.xml
+server:
+  compression:
+    enabled: true
+    mime-types: application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain
+tencentcloudapi:
+  # 你的 secretId
+  secretId: AKID60hCWbS9c3FF3e2B6pjZDfx2xgFbkiW8
+  # 你的 secretKey
+  secretKey: UlpfHez96rPzOaZNzOacT8aryudAWhgS
+  # 请求官方地址 不变
+  endpoint: iai.tencentcloudapi.com
+  #地域 可不变
+  region: ap-shanghai
+spring:
+  autoconfigure:
+    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
+  cache:
+    ehcache:
+      config: classpath:ehcache.xml
+      enabled: false
+    redis:
+      enabled: true
+  datasource:
+    druid:
+      stat-view-servlet:
+        enabled: true
+        login-password: '@dmin1234'
+        login-username: admin
+        reset-enable: true
+        url-pattern: /druid/*
+    dynamic:
+      datasource:
+        master:
+          password: yt123456
+          url: jdbc:mysql://usky-cloud-mysql:3306/usky-cloud?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowMultiQueries=true
+          username: root
+      druid:
+        initial-size: 5
+        min-idle: 5
+        maxActive: 20
+        maxWait: 60000
+        timeBetweenEvictionRunsMillis: 60000
+        minEvictableIdleTimeMillis: 300000
+        validationQuery: SELECT 1 FROM DUAL
+        testWhileIdle: true
+        testOnBorrow: false
+        testOnReturn: false
+        poolPreparedStatements: true
+        maxPoolPreparedStatementPerConnectionSize: 20
+        filters: stat,slf4j
+        connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
+      primary: master
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    default-property-inclusion: always
+    deserialization:
+      fail-on-unknown-properties: false
+    parser:
+      allow-single-quotes: true
+      allow-unquoted-control-chars: true
+    serialization:
+      fail-on-empty-beans: false
+    time-zone: GMT+8 
+  redis:
+    host: usky-cloud-redis
+    password: 123456
+    port: 6379
+    timeout: 10000
+  tenant:
+    enable: false
+  servlet:
+    multipart:
+      max-file-size: 10MB
+      max-request-size: 15MB  
+
+temp:
+  basedir: C:/Users/pc/Desktop/
+