fix(train): 学员详情回填报到状态并优化人脸设备超时与报到接口提示

This commit is contained in:
buliping committed 2026-11-10 17:00:43 +08:00
1 parent f3041e7718
commit fe988f32f0
5 files changed
+91 -6

No files matched your search

@@ -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。
@@ -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 字节。
*/
@@ -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
@@ -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;
}
/**
* 用参训档案回填报到状态。
*
* <p>报到状态只存在于 train_enrollment,学员主档 train_student 不保存该字段。
* 报到机上传人脸(/api/register/v1/student/face)更新的是 train_enrollment,
* 若详情接口不回填,就会出现“报到状态已变更、但学员详情仍显示空”的口径不一致。</p>
*
* <p>取值口径与报到流程保持一致:按 create_time 降序取第一条参训记录。</p>
*
* @param detail 学员详情对象
*/
private void fillEnrollmentStatus(StudentItemVO detail)
{
if (trainEnrollmentMapper == null)
{
return;
}
List<EnrollmentItemVO> 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()
{
+1 -1
View File
@@ -45,7 +45,7 @@
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
</el-row>
<el-alert
title="报到状态取自参训记录表 train_enrollment 的 enrollment_status 字段(0 待报到 / 1 已报到 / 2 请假 / 3 放弃 / 4 已完成);「已报到」由报到确认流程产生,人工仅可标记请假、放弃与恢复参训。"
title="报到状态包含(0 待报到 / 1 已报到 / 2 请假 / 3 放弃 / 4 已完成);「已报到」由报到确认流程产生,人工仅可标记请假、放弃与恢复参训。"
type="info"
:closable="false"
show-icon