From fe988f32f08f0b6fd9d61a831de666f1dae6c100 Mon Sep 17 00:00:00 2001 From: buliping Date: Tue, 10 Nov 2026 17:00:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(train):=20=E5=AD=A6=E5=91=98=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=9B=9E=E5=A1=AB=E6=8A=A5=E5=88=B0=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BA=BA=E8=84=B8=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E4=B8=8E=E6=8A=A5=E5=88=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/TrainFaceApiController.java | 29 +++++++++++++++++- .../api/TrainRegisterApiController.java | 30 +++++++++++++++++-- .../src/main/resources/application.yml | 7 +++-- .../service/impl/TrainStudentServiceImpl.java | 29 ++++++++++++++++++ build_ui/src/views/train/report/index.vue | 2 +- 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainFaceApiController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainFaceApiController.java index 582b25007..4da2551b1 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainFaceApiController.java +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainFaceApiController.java @@ -4,12 +4,15 @@ import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; +import jakarta.annotation.PostConstruct; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; @@ -19,6 +22,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -69,13 +73,36 @@ public class TrainFaceApiController { @Value("${iot.face.face_api_key:}") private String apiKey; + /** 设备调用连接超时(毫秒):设备离线时快速失败,避免阻塞报到机请求 */ + @Value("${face.connect-timeout:3000}") + private int connectTimeoutMs; + + /** 设备调用读取超时(毫秒) */ + @Value("${face.read-timeout:3000}") + private int readTimeoutMs; + @Autowired private ITrainStudentService studentService; - private final RestTemplate restTemplate = new RestTemplate(); + private RestTemplate restTemplate; private static final Logger log = LoggerFactory.getLogger(TrainFaceApiController.class); + /** + * 初始化带超时的 RestTemplate。 + * 默认 RestTemplate 不设超时,设备不可达时会一直等待(实测约 21 秒由系统层超时才失败), + * 报到机属现场交互设备,必须快速失败并把失败信息回传给调用方。 + */ + @PostConstruct + public void initRestTemplate() + { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(Duration.ofMillis(connectTimeoutMs)); + factory.setReadTimeout(Duration.ofMillis(readTimeoutMs)); + this.restTemplate = new RestTemplate(factory); + log.info("人脸设备调用超时配置 connectTimeout={}ms readTimeout={}ms", connectTimeoutMs, readTimeoutMs); + } + /** * 4.3 POST /employees 新增员工人脸 * 按 employee_id(即学员 sId)查询 train_student,取头像转 Base64 data URL 后调用设备 /employees。 diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainRegisterApiController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainRegisterApiController.java index d60a1ed8b..21531eb58 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainRegisterApiController.java +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainRegisterApiController.java @@ -693,7 +693,6 @@ public class TrainRegisterApiController { String datePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); String fileName = UUID.randomUUID().toString().replace("-", "") + ".png"; - System.out.println("fileName = " + fileName); File dir = new File(RuoYiConfig.getProfile() + File.separator + "upload" + File.separator + datePath); if (!dir.exists() && !dir.mkdirs()) { @@ -708,9 +707,9 @@ public class TrainRegisterApiController { { return AjaxResult.error("图片写入失败:" + ex.getMessage()); } + log.debug("上传学员人脸 sId={} 落盘文件={}", sId, target.getAbsolutePath()); String avatarPath = Constants.RESOURCE_PREFIX + "/upload/" + datePath + "/" + fileName; - System.out.println("avatarPath = " + avatarPath); // 更新头像路径 TrainStudent student = new TrainStudent(); student.setsId(sId); @@ -727,6 +726,8 @@ public class TrainRegisterApiController { int rows = trainStudentMapper.updateTrainStudent(student); if (rows <= 0) { + // 学员不存在或未更新成功时不保留已落盘图片,避免产生无引用的孤儿文件 + deleteUploadedFile(target); return AjaxResult.error("学员不存在或更新失败"); } @@ -957,6 +958,7 @@ public class TrainRegisterApiController { { return AjaxResult.error("图片写入失败:" + ex.getMessage()); } + log.debug("上传教师人脸 teacherId={} 落盘文件={}", teacherId, target.getAbsolutePath()); String avatarPath = Constants.RESOURCE_PREFIX + "/upload/" + datePath + "/" + fileName; // 更新教师头像路径(教师表主键为 t_id) @@ -975,6 +977,8 @@ public class TrainRegisterApiController { int rows = teacherService.updateTrainTeacher(teacher); if (rows <= 0) { + // 教师不存在或未更新成功时不保留已落盘图片,避免产生无引用的孤儿文件 + deleteUploadedFile(target); return AjaxResult.error("教师不存在或更新失败"); } @@ -1106,6 +1110,28 @@ public class TrainRegisterApiController { return uploadStudentFace(target); } + /** + * 删除已落盘的上传文件。用于人脸上传后的业务校验失败场景, + * 避免在磁盘留下没有任何数据引用的孤儿图片。 + * + * @param file 待删除的文件,可为 null + */ + private void deleteUploadedFile(File file) + { + if (file == null || !file.exists()) + { + return; + } + try + { + Files.deleteIfExists(file.toPath()); + } + catch (IOException ex) + { + log.warn("删除孤儿上传文件失败 file={}", file.getAbsolutePath(), ex); + } + } + /** * 将 base64(可带 data URI 前缀)解码并转码为 PNG 字节。 */ diff --git a/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml b/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml index 7d18bc123..099ba1eca 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml +++ b/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml @@ -99,6 +99,9 @@ face: #api-base-url: http://192.168.1.230:9000 # 是否实际调用人脸识别设备:false=返回模拟响应,true=真实调用 enabled: true + # 设备调用超时(毫秒):设备离线时快速失败,避免报到机请求被长时间阻塞 + connect-timeout: 3000 + read-timeout: 3000 # 培训与门禁业务配置 train: @@ -186,8 +189,8 @@ iot: #- http://192.168.1.230:8802/bdws_api/Third/Api/Easen/getFaceFeatureData # 调用超时(毫秒) timeout: 10000 - # lock_endpoint: http://192.168.1.203:8080 - lock_endpoint: http://192.168.1.202:8085 + lock_endpoint: http://192.168.1.203:8080 + #lock_endpoint: http://192.168.1.202:8085 api_key: RiviwlSWGDVjY7ktsRsn718R6oDNmrQy avatar_profile: http://192.168.1.106:8096 face_api_key: To_eIlokUB6vf6HPSFugljLaAZh38AOI3al5xwb8rFU diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainStudentServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainStudentServiceImpl.java index 4a740fa14..9a42475a2 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainStudentServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainStudentServiceImpl.java @@ -27,6 +27,7 @@ import com.ruoyi.train.domain.TrainStudent; import com.ruoyi.train.domain.dto.StudentCreateDTO; import com.ruoyi.train.domain.dto.StudentUpdateDTO; import com.ruoyi.train.domain.query.StudentQuery; +import com.ruoyi.train.domain.vo.EnrollmentItemVO; import com.ruoyi.train.domain.vo.StudentDetailVO; import com.ruoyi.train.domain.vo.StudentExportVO; import com.ruoyi.train.domain.vo.StudentItemVO; @@ -118,9 +119,37 @@ public class TrainStudentServiceImpl implements ITrainStudentService detail.setRemark(student.getRemark()); detail.setCreateBy(student.getCreateBy()); detail.setCreateTime(student.getCreateTime()); + fillEnrollmentStatus(detail); return detail; } + /** + * 用参训档案回填报到状态。 + * + *

报到状态只存在于 train_enrollment,学员主档 train_student 不保存该字段。 + * 报到机上传人脸(/api/register/v1/student/face)更新的是 train_enrollment, + * 若详情接口不回填,就会出现“报到状态已变更、但学员详情仍显示空”的口径不一致。

+ * + *

取值口径与报到流程保持一致:按 create_time 降序取第一条参训记录。

+ * + * @param detail 学员详情对象 + */ + private void fillEnrollmentStatus(StudentItemVO detail) + { + if (trainEnrollmentMapper == null) + { + return; + } + List enrollments = trainEnrollmentMapper.selectEnrollmentsByStudentId(detail.getsId()); + if (enrollments == null || enrollments.isEmpty()) + { + return; + } + EnrollmentItemVO latest = enrollments.get(0); + detail.setEnrollmentStatus(latest.getEnrollmentStatus()); + detail.setJoinTime(latest.getJoinTime()); + } + @Override public String genPersonNo() { diff --git a/build_ui/src/views/train/report/index.vue b/build_ui/src/views/train/report/index.vue index abb0d2c93..57eded018 100644 --- a/build_ui/src/views/train/report/index.vue +++ b/build_ui/src/views/train/report/index.vue @@ -45,7 +45,7 @@