新增培训:实现参训人员P0能力
This commit is contained in:
1 parent
665ef040d8
commit
e340db872c
20 files changed
+3840
No files matched your search
+122
@@ -0,0 +1,122 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.train.domain.dto.EnrollmentAddDTO;
|
||||
import com.ruoyi.train.domain.dto.EnrollmentStatusUpdateDTO;
|
||||
import com.ruoyi.train.domain.query.EnrollmentQuery;
|
||||
import com.ruoyi.train.domain.vo.BatchImportResultVO;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentDetailVO;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentItemVO;
|
||||
import com.ruoyi.train.service.EnrollmentAppService;
|
||||
|
||||
/**
|
||||
* 参训人员全生命周期管理 Controller (API Contract Module B 第 3 部分 / ISSUE-03-01 / P0)。
|
||||
* 严格遵循规范:Controller 仅负责参数校验、鉴权拦截与统一 AjaxResult/TableDataInfo 返回。
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/train")
|
||||
public class TrainEnrollmentController extends BaseController
|
||||
{
|
||||
private final EnrollmentAppService enrollmentAppService;
|
||||
|
||||
@Autowired
|
||||
public TrainEnrollmentController(EnrollmentAppService enrollmentAppService)
|
||||
{
|
||||
this.enrollmentAppService = enrollmentAppService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看本期参训名单列表 (UC-06 / P0)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:enrollment:list')")
|
||||
@GetMapping("/plans/{planId}/enrollments")
|
||||
public TableDataInfo listEnrollments(@PathVariable String planId, EnrollmentQuery query)
|
||||
{
|
||||
startPage();
|
||||
List<EnrollmentItemVO> list = enrollmentAppService.listEnrollments(planId, query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单名参训人员 (UC-04 / P0)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:enrollment:add')")
|
||||
@Log(title = "添加参训学员", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/plans/{planId}/enrollments")
|
||||
public AjaxResult addEnrollment(@PathVariable String planId, @Validated @RequestBody EnrollmentAddDTO dto)
|
||||
{
|
||||
Long enrollmentId = enrollmentAppService.addEnrollment(planId, dto, SecurityUtils.getUsername());
|
||||
return success(enrollmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel 批量导入参训人员 (UC-05 / P0)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:enrollment:import')")
|
||||
@Log(title = "批量导入参训人员", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/plans/{planId}/enrollments/batch")
|
||||
public AjaxResult batchImportEnrollments(@PathVariable String planId,
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "autoCreateStudent", required = false, defaultValue = "false") Boolean autoCreateStudent)
|
||||
{
|
||||
BatchImportResultVO result = enrollmentAppService.batchImportEnrollments(planId, file, autoCreateStudent, SecurityUtils.getUsername());
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参训档案详情 (P0)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:enrollment:query')")
|
||||
@GetMapping("/enrollments/{enrollmentId}")
|
||||
public AjaxResult getDetail(@PathVariable Long enrollmentId)
|
||||
{
|
||||
EnrollmentDetailVO detail = enrollmentAppService.getEnrollmentDetail(enrollmentId);
|
||||
return success(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更参训状态 (UC-06 / P1 冻结范围)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:enrollment:edit')")
|
||||
@Log(title = "变更参训状态", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/enrollments/{enrollmentId}/status")
|
||||
public AjaxResult updateStatus(@PathVariable Long enrollmentId, @Validated @RequestBody EnrollmentStatusUpdateDTO dto)
|
||||
{
|
||||
enrollmentAppService.updateEnrollmentStatus(enrollmentId, dto, SecurityUtils.getUsername());
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除参训人员 (P1 冻结范围)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:enrollment:remove')")
|
||||
@Log(title = "移除参训人员", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/enrollments/{enrollmentId}")
|
||||
public AjaxResult removeEnrollment(@PathVariable Long enrollmentId)
|
||||
{
|
||||
enrollmentAppService.removeEnrollment(enrollmentId, SecurityUtils.getUsername());
|
||||
return success();
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.domain.dto.EnrollmentAddDTO;
|
||||
import com.ruoyi.train.domain.dto.EnrollmentStatusUpdateDTO;
|
||||
import com.ruoyi.train.domain.query.EnrollmentQuery;
|
||||
|
||||
/**
|
||||
* ISSUE-03-01 参训管理 Controller 契约与安全边界测试。
|
||||
*/
|
||||
public class EnrollmentApiTest
|
||||
{
|
||||
@Test
|
||||
public void shouldExposeExactFrozenEndpointsAndPermissions() throws Exception
|
||||
{
|
||||
// 验证类级别映射
|
||||
RequestMapping classMapping = TrainEnrollmentController.class.getAnnotation(RequestMapping.class);
|
||||
assertNotNull(classMapping);
|
||||
assertEquals("/train", classMapping.value()[0]);
|
||||
|
||||
// 1. GET /train/plans/{planId}/enrollments
|
||||
Method listMethod = TrainEnrollmentController.class.getMethod("listEnrollments", String.class, EnrollmentQuery.class);
|
||||
GetMapping listGet = listMethod.getAnnotation(GetMapping.class);
|
||||
assertNotNull(listGet);
|
||||
assertEquals("/plans/{planId}/enrollments", listGet.value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:enrollment:list')", listMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 2. POST /train/plans/{planId}/enrollments
|
||||
Method addMethod = TrainEnrollmentController.class.getMethod("addEnrollment", String.class, EnrollmentAddDTO.class);
|
||||
PostMapping addPost = addMethod.getAnnotation(PostMapping.class);
|
||||
assertNotNull(addPost);
|
||||
assertEquals("/plans/{planId}/enrollments", addPost.value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:enrollment:add')", addMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 3. POST /train/plans/{planId}/enrollments/batch
|
||||
Method batchMethod = TrainEnrollmentController.class.getMethod("batchImportEnrollments", String.class, MultipartFile.class, Boolean.class);
|
||||
PostMapping batchPost = batchMethod.getAnnotation(PostMapping.class);
|
||||
assertNotNull(batchPost);
|
||||
assertEquals("/plans/{planId}/enrollments/batch", batchPost.value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:enrollment:import')", batchMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 4. GET /train/enrollments/{enrollmentId}
|
||||
Method detailMethod = TrainEnrollmentController.class.getMethod("getDetail", Long.class);
|
||||
GetMapping detailGet = detailMethod.getAnnotation(GetMapping.class);
|
||||
assertNotNull(detailGet);
|
||||
assertEquals("/enrollments/{enrollmentId}", detailGet.value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:enrollment:query')", detailMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 5. PUT /train/enrollments/{enrollmentId}/status
|
||||
Method statusMethod = TrainEnrollmentController.class.getMethod("updateStatus", Long.class, EnrollmentStatusUpdateDTO.class);
|
||||
PutMapping statusPut = statusMethod.getAnnotation(PutMapping.class);
|
||||
assertNotNull(statusPut);
|
||||
assertEquals("/enrollments/{enrollmentId}/status", statusPut.value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:enrollment:edit')", statusMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 6. DELETE /train/enrollments/{enrollmentId}
|
||||
Method removeMethod = TrainEnrollmentController.class.getMethod("removeEnrollment", Long.class);
|
||||
DeleteMapping removeDelete = removeMethod.getAnnotation(DeleteMapping.class);
|
||||
assertNotNull(removeDelete);
|
||||
assertEquals("/enrollments/{enrollmentId}", removeDelete.value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:enrollment:remove')", removeMethod.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerMustNotExposePersistenceDomain()
|
||||
{
|
||||
for (Method method : TrainEnrollmentController.class.getDeclaredMethods())
|
||||
{
|
||||
assertFalse("Controller方法返回值不得直接暴露 TrainEnrollment 实体",
|
||||
method.getReturnType().equals(TrainEnrollment.class));
|
||||
for (Class<?> paramType : method.getParameterTypes())
|
||||
{
|
||||
assertFalse("Controller方法入参不得直接接收 TrainEnrollment 实体",
|
||||
paramType.equals(TrainEnrollment.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerMustStrictlyExposeOnlyTheFrozenEndpoints()
|
||||
{
|
||||
int endpointCount = 0;
|
||||
for (Method method : TrainEnrollmentController.class.getDeclaredMethods())
|
||||
{
|
||||
boolean isEndpoint = method.isAnnotationPresent(GetMapping.class)
|
||||
|| method.isAnnotationPresent(PostMapping.class)
|
||||
|| method.isAnnotationPresent(PutMapping.class)
|
||||
|| method.isAnnotationPresent(DeleteMapping.class);
|
||||
|
||||
if (isEndpoint)
|
||||
{
|
||||
endpointCount++;
|
||||
}
|
||||
}
|
||||
assertEquals("Controller 必须严格只有契约冻结的 6 个端点", 6, endpointCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ruoyi.train.constant;
|
||||
|
||||
/**
|
||||
* 参训人员管理错误码与常量规范 (API Contract V1.0.1 / P0 Issue Backlog)。
|
||||
*/
|
||||
public final class EnrollmentErrorCode
|
||||
{
|
||||
public static final int ENROLLMENT_DUPLICATED = 40030;
|
||||
public static final int RECRUIT_LIMIT_EXCEEDED = 40031;
|
||||
public static final int ENROLLMENT_HAS_BUSINESS_DATA = 40032;
|
||||
public static final int EXCEL_PARSE_ERROR = 40033;
|
||||
public static final int FILE_SIZE_EXCEEDED = 40034;
|
||||
public static final int ENROLLMENT_NOT_FOUND = 40430;
|
||||
|
||||
public static final String ENROLLMENT_DUPLICATED_MESSAGE = "该学员已在本班参训名单中,不可重复添加";
|
||||
public static final String RECRUIT_LIMIT_EXCEEDED_MESSAGE = "参训人数已超出本期计划招生上限";
|
||||
public static final String ENROLLMENT_HAS_BUSINESS_DATA_MESSAGE = "该学员已产生考勤或成绩数据,不可删除参训档案";
|
||||
public static final String EXCEL_PARSE_ERROR_MESSAGE = "Excel导入解析错误";
|
||||
public static final String FILE_SIZE_EXCEEDED_MESSAGE = "导入文件大小超出限制";
|
||||
public static final String ENROLLMENT_NOT_FOUND_MESSAGE = "参训档案不存在";
|
||||
public static final String ENROLLMENT_STATUS_INVALID_MESSAGE = "参训状态流转不合法";
|
||||
public static final String ENROLLMENT_SOURCE_REQUIRED_MESSAGE = "参训来源(sourceType)不能为空";
|
||||
public static final String ACCOMMODATION_REQUIRED_MESSAGE = "住宿需求(accommodationRequired)不能为空";
|
||||
|
||||
/** 参训状态常量定义 */
|
||||
public static final int STATUS_WAITING_REPORT = 0; // 0:待报到
|
||||
public static final int STATUS_REPORTED = 1; // 1:已报到
|
||||
public static final int STATUS_LEAVE = 2; // 2:请假
|
||||
public static final int STATUS_ABANDONED = 3; // 3:放弃
|
||||
public static final int STATUS_COMPLETED = 4; // 4:已完成
|
||||
|
||||
/** 住宿需求常量 */
|
||||
public static final int ACCOMMODATION_NOT_REQUIRED = 0; // 0:不住宿
|
||||
public static final int ACCOMMODATION_REQUIRED = 1; // 1:住宿
|
||||
|
||||
private EnrollmentErrorCode()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.ruoyi.train.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 参训学员登记表 (学员参加培训班全生命周期主表) train_enrollment
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
public class TrainEnrollment extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 参训档案ID (核心ID之一) */
|
||||
private Long enrollmentId;
|
||||
|
||||
/** 培训班ID */
|
||||
@Excel(name = "培训班ID")
|
||||
private String planId;
|
||||
|
||||
/** 学员长期主档ID引用 */
|
||||
@Excel(name = "学员长期主档ID引用")
|
||||
private String studentId;
|
||||
|
||||
/** 参训名单来源 (具体字典值由 train_enrollment_source 定义) */
|
||||
@Excel(name = "参训名单来源")
|
||||
private Integer sourceType;
|
||||
|
||||
/** 参训状态 (0:待报到, 1:已报到, 2:请假, 3:放弃, 4:已完成) */
|
||||
@Excel(name = "参训状态", readConverterExp = "0=待报到,1=已报到,2=请假,3=放弃,4=已完成")
|
||||
private Integer enrollmentStatus;
|
||||
|
||||
/** 是否需要住宿 (0:否, 1:是) */
|
||||
@Excel(name = "是否需要住宿", readConverterExp = "0=否,1=是")
|
||||
private Integer accommodationRequired;
|
||||
|
||||
/** 入班报到时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "入班报到时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date joinTime;
|
||||
|
||||
/** 培训完成/结课时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "培训完成/结课时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date completeTime;
|
||||
|
||||
public Long getEnrollmentId()
|
||||
{
|
||||
return enrollmentId;
|
||||
}
|
||||
|
||||
public void setEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
this.enrollmentId = enrollmentId;
|
||||
}
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public String getStudentId()
|
||||
{
|
||||
return studentId;
|
||||
}
|
||||
|
||||
public void setStudentId(String studentId)
|
||||
{
|
||||
this.studentId = studentId;
|
||||
}
|
||||
|
||||
public Integer getSourceType()
|
||||
{
|
||||
return sourceType;
|
||||
}
|
||||
|
||||
public void setSourceType(Integer sourceType)
|
||||
{
|
||||
this.sourceType = sourceType;
|
||||
}
|
||||
|
||||
public Integer getEnrollmentStatus()
|
||||
{
|
||||
return enrollmentStatus;
|
||||
}
|
||||
|
||||
public void setEnrollmentStatus(Integer enrollmentStatus)
|
||||
{
|
||||
this.enrollmentStatus = enrollmentStatus;
|
||||
}
|
||||
|
||||
public Integer getAccommodationRequired()
|
||||
{
|
||||
return accommodationRequired;
|
||||
}
|
||||
|
||||
public void setAccommodationRequired(Integer accommodationRequired)
|
||||
{
|
||||
this.accommodationRequired = accommodationRequired;
|
||||
}
|
||||
|
||||
public Date getJoinTime()
|
||||
{
|
||||
return joinTime;
|
||||
}
|
||||
|
||||
public void setJoinTime(Date joinTime)
|
||||
{
|
||||
this.joinTime = joinTime;
|
||||
}
|
||||
|
||||
public Date getCompleteTime()
|
||||
{
|
||||
return completeTime;
|
||||
}
|
||||
|
||||
public void setCompleteTime(Date completeTime)
|
||||
{
|
||||
this.completeTime = completeTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("enrollmentId", getEnrollmentId())
|
||||
.append("planId", getPlanId())
|
||||
.append("studentId", getStudentId())
|
||||
.append("sourceType", getSourceType())
|
||||
.append("enrollmentStatus", getEnrollmentStatus())
|
||||
.append("accommodationRequired", getAccommodationRequired())
|
||||
.append("joinTime", getJoinTime())
|
||||
.append("completeTime", getCompleteTime())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.append("remark", getRemark())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 添加参训学员请求 DTO (UC-04 / API Contract 3.2)。
|
||||
*/
|
||||
public class EnrollmentAddDTO
|
||||
{
|
||||
/** 学员长期主档ID */
|
||||
@NotBlank(message = "学员ID不能为空")
|
||||
@Size(max = 32, message = "学员ID长度不能超过32个字符")
|
||||
private String studentId;
|
||||
|
||||
/** 参训名单来源 (具体字典值由 train_enrollment_source 定义,如 1:管理员录入, 2:Excel批量导入, 3:铁路内部业务平台同步) */
|
||||
@NotNull(message = "参训来源(sourceType)不能为空")
|
||||
private Integer sourceType;
|
||||
|
||||
/** 是否需要住宿 (0:否, 1:是) */
|
||||
@NotNull(message = "住宿需求(accommodationRequired)不能为空")
|
||||
private Integer accommodationRequired;
|
||||
|
||||
/** 备注 */
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
/** 选派单位 (兼容部分调用传参,展示时以学员档案为准) */
|
||||
private String orgName;
|
||||
|
||||
public String getStudentId()
|
||||
{
|
||||
return studentId;
|
||||
}
|
||||
|
||||
public void setStudentId(String studentId)
|
||||
{
|
||||
this.studentId = studentId;
|
||||
}
|
||||
|
||||
public Integer getSourceType()
|
||||
{
|
||||
return sourceType;
|
||||
}
|
||||
|
||||
public void setSourceType(Integer sourceType)
|
||||
{
|
||||
this.sourceType = sourceType;
|
||||
}
|
||||
|
||||
public Integer getAccommodationRequired()
|
||||
{
|
||||
return accommodationRequired;
|
||||
}
|
||||
|
||||
public void setAccommodationRequired(Integer accommodationRequired)
|
||||
{
|
||||
this.accommodationRequired = accommodationRequired;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark)
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public String getOrgName()
|
||||
{
|
||||
return orgName;
|
||||
}
|
||||
|
||||
public void setOrgName(String orgName)
|
||||
{
|
||||
this.orgName = orgName;
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
/**
|
||||
* 参训学员创建 DTO 别名兼容类 (兼容 ISSUE-03-01 文档命名)。
|
||||
*/
|
||||
public class EnrollmentCreateDTO extends EnrollmentAddDTO
|
||||
{
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 变更参训状态请求 DTO (UC-06 / API Contract 3.5)。
|
||||
*/
|
||||
public class EnrollmentStatusUpdateDTO
|
||||
{
|
||||
/** 参训状态 (2:请假, 3:放弃) */
|
||||
@NotNull(message = "参训状态不能为空")
|
||||
private Integer enrollmentStatus;
|
||||
|
||||
/** 变更原因 / 备注 */
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
public Integer getEnrollmentStatus()
|
||||
{
|
||||
return enrollmentStatus;
|
||||
}
|
||||
|
||||
public void setEnrollmentStatus(Integer enrollmentStatus)
|
||||
{
|
||||
this.enrollmentStatus = enrollmentStatus;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark)
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ruoyi.train.domain.query;
|
||||
|
||||
/**
|
||||
* 参训学员列表查询参数 (UC-06 / API Contract 3.1)。
|
||||
*/
|
||||
public class EnrollmentQuery
|
||||
{
|
||||
/** 培训计划ID */
|
||||
private String planId;
|
||||
|
||||
/** 学员姓名 (模糊匹配) */
|
||||
private String studentName;
|
||||
|
||||
/** 参训状态 (0待报到 1已报到 2请假 3放弃 4已完成) */
|
||||
private Integer enrollmentStatus;
|
||||
|
||||
/** 住宿需求 (0不住宿 1住宿) */
|
||||
private Integer accommodationRequired;
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public String getStudentName()
|
||||
{
|
||||
return studentName;
|
||||
}
|
||||
|
||||
public void setStudentName(String studentName)
|
||||
{
|
||||
this.studentName = studentName;
|
||||
}
|
||||
|
||||
public Integer getEnrollmentStatus()
|
||||
{
|
||||
return enrollmentStatus;
|
||||
}
|
||||
|
||||
public void setEnrollmentStatus(Integer enrollmentStatus)
|
||||
{
|
||||
this.enrollmentStatus = enrollmentStatus;
|
||||
}
|
||||
|
||||
public Integer getAccommodationRequired()
|
||||
{
|
||||
return accommodationRequired;
|
||||
}
|
||||
|
||||
public void setAccommodationRequired(Integer accommodationRequired)
|
||||
{
|
||||
this.accommodationRequired = accommodationRequired;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 参训人员批量导入结果 VO (UC-05 / API Contract 3.3)。
|
||||
*/
|
||||
public class BatchImportResultVO
|
||||
{
|
||||
/** 总记录数 */
|
||||
private Integer totalCount = 0;
|
||||
|
||||
/** 导入成功数 */
|
||||
private Integer successCount = 0;
|
||||
|
||||
/** 导入失败数 */
|
||||
private Integer failureCount = 0;
|
||||
|
||||
/** 失败错误明细列表 (按行) */
|
||||
private List<String> errorList = new ArrayList<>();
|
||||
|
||||
public BatchImportResultVO()
|
||||
{
|
||||
}
|
||||
|
||||
public BatchImportResultVO(Integer totalCount, Integer successCount, Integer failureCount, List<String> errorList)
|
||||
{
|
||||
this.totalCount = totalCount;
|
||||
this.successCount = successCount;
|
||||
this.failureCount = failureCount;
|
||||
this.errorList = errorList;
|
||||
}
|
||||
|
||||
public Integer getTotalCount()
|
||||
{
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
public void setTotalCount(Integer totalCount)
|
||||
{
|
||||
this.totalCount = totalCount;
|
||||
}
|
||||
|
||||
public Integer getSuccessCount()
|
||||
{
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public void setSuccessCount(Integer successCount)
|
||||
{
|
||||
this.successCount = successCount;
|
||||
}
|
||||
|
||||
public Integer getFailureCount()
|
||||
{
|
||||
return failureCount;
|
||||
}
|
||||
|
||||
public void setFailureCount(Integer failureCount)
|
||||
{
|
||||
this.failureCount = failureCount;
|
||||
}
|
||||
|
||||
public List<String> getErrorList()
|
||||
{
|
||||
return errorList;
|
||||
}
|
||||
|
||||
public void setErrorList(List<String> errorList)
|
||||
{
|
||||
this.errorList = errorList;
|
||||
}
|
||||
}
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 学员参训档案全景 VO (API Contract V1.0.1 1.3 / P0)。
|
||||
*/
|
||||
public class EnrollmentDetailVO
|
||||
{
|
||||
/** 参训档案标识 */
|
||||
private Long enrollmentId;
|
||||
|
||||
/** 培训计划标识 */
|
||||
private String planId;
|
||||
|
||||
/** 培训计划名称 */
|
||||
private String planName;
|
||||
|
||||
/** 学员 ID */
|
||||
private String studentId;
|
||||
|
||||
/** 学员姓名 */
|
||||
private String studentName;
|
||||
|
||||
/** 性别 */
|
||||
private String gender;
|
||||
|
||||
/** 身份证脱敏展示 (210102********1234) */
|
||||
private String idCardMasked;
|
||||
|
||||
/** 选派单位 */
|
||||
private String orgName;
|
||||
|
||||
/** 职务 */
|
||||
private String postName;
|
||||
|
||||
/** 手机号 */
|
||||
private String phone;
|
||||
|
||||
/** 参训名单来源 */
|
||||
private Integer sourceType;
|
||||
|
||||
/** 状态:0待报到 1已报到 2请假 3放弃 4已完成 */
|
||||
private Integer enrollmentStatus;
|
||||
|
||||
/** 住宿需求 (0不住宿 1住宿) */
|
||||
private Integer accommodationRequired;
|
||||
|
||||
/** 当前住宿分配详情 (building, room, bedNo, authStatus) */
|
||||
private AccommodationSummaryVO accommodation;
|
||||
|
||||
/** 报到信息 (reportTime, reportWay, verifyNote) */
|
||||
private ReportSummaryVO report;
|
||||
|
||||
/** 入班时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date joinTime;
|
||||
|
||||
/** 结训时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date completeTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
public Long getEnrollmentId()
|
||||
{
|
||||
return enrollmentId;
|
||||
}
|
||||
|
||||
public void setEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
this.enrollmentId = enrollmentId;
|
||||
}
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public String getPlanName()
|
||||
{
|
||||
return planName;
|
||||
}
|
||||
|
||||
public void setPlanName(String planName)
|
||||
{
|
||||
this.planName = planName;
|
||||
}
|
||||
|
||||
public String getStudentId()
|
||||
{
|
||||
return studentId;
|
||||
}
|
||||
|
||||
public void setStudentId(String studentId)
|
||||
{
|
||||
this.studentId = studentId;
|
||||
}
|
||||
|
||||
public String getStudentName()
|
||||
{
|
||||
return studentName;
|
||||
}
|
||||
|
||||
public void setStudentName(String studentName)
|
||||
{
|
||||
this.studentName = studentName;
|
||||
}
|
||||
|
||||
public String getGender()
|
||||
{
|
||||
return gender;
|
||||
}
|
||||
|
||||
public void setGender(String gender)
|
||||
{
|
||||
this.gender = gender;
|
||||
}
|
||||
|
||||
public String getIdCardMasked()
|
||||
{
|
||||
return idCardMasked;
|
||||
}
|
||||
|
||||
public void setIdCardMasked(String idCardMasked)
|
||||
{
|
||||
this.idCardMasked = idCardMasked;
|
||||
}
|
||||
|
||||
public String getOrgName()
|
||||
{
|
||||
return orgName;
|
||||
}
|
||||
|
||||
public void setOrgName(String orgName)
|
||||
{
|
||||
this.orgName = orgName;
|
||||
}
|
||||
|
||||
public String getPostName()
|
||||
{
|
||||
return postName;
|
||||
}
|
||||
|
||||
public void setPostName(String postName)
|
||||
{
|
||||
this.postName = postName;
|
||||
}
|
||||
|
||||
public String getPhone()
|
||||
{
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone)
|
||||
{
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public Integer getSourceType()
|
||||
{
|
||||
return sourceType;
|
||||
}
|
||||
|
||||
public void setSourceType(Integer sourceType)
|
||||
{
|
||||
this.sourceType = sourceType;
|
||||
}
|
||||
|
||||
public Integer getEnrollmentStatus()
|
||||
{
|
||||
return enrollmentStatus;
|
||||
}
|
||||
|
||||
public void setEnrollmentStatus(Integer enrollmentStatus)
|
||||
{
|
||||
this.enrollmentStatus = enrollmentStatus;
|
||||
}
|
||||
|
||||
public Integer getAccommodationRequired()
|
||||
{
|
||||
return accommodationRequired;
|
||||
}
|
||||
|
||||
public void setAccommodationRequired(Integer accommodationRequired)
|
||||
{
|
||||
this.accommodationRequired = accommodationRequired;
|
||||
}
|
||||
|
||||
public AccommodationSummaryVO getAccommodation()
|
||||
{
|
||||
return accommodation;
|
||||
}
|
||||
|
||||
public void setAccommodation(AccommodationSummaryVO accommodation)
|
||||
{
|
||||
this.accommodation = accommodation;
|
||||
}
|
||||
|
||||
public ReportSummaryVO getReport()
|
||||
{
|
||||
return report;
|
||||
}
|
||||
|
||||
public void setReport(ReportSummaryVO report)
|
||||
{
|
||||
this.report = report;
|
||||
}
|
||||
|
||||
public Date getJoinTime()
|
||||
{
|
||||
return joinTime;
|
||||
}
|
||||
|
||||
public void setJoinTime(Date joinTime)
|
||||
{
|
||||
this.joinTime = joinTime;
|
||||
}
|
||||
|
||||
public Date getCompleteTime()
|
||||
{
|
||||
return completeTime;
|
||||
}
|
||||
|
||||
public void setCompleteTime(Date completeTime)
|
||||
{
|
||||
this.completeTime = completeTime;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark)
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
/** 住宿摘要嵌套对象 */
|
||||
public static class AccommodationSummaryVO
|
||||
{
|
||||
private String building;
|
||||
private String room;
|
||||
private String bedNo;
|
||||
private Integer authStatus;
|
||||
|
||||
public AccommodationSummaryVO()
|
||||
{
|
||||
}
|
||||
|
||||
public AccommodationSummaryVO(String building, String room, String bedNo, Integer authStatus)
|
||||
{
|
||||
this.building = building;
|
||||
this.room = room;
|
||||
this.bedNo = bedNo;
|
||||
this.authStatus = authStatus;
|
||||
}
|
||||
|
||||
public String getBuilding()
|
||||
{
|
||||
return building;
|
||||
}
|
||||
|
||||
public void setBuilding(String building)
|
||||
{
|
||||
this.building = building;
|
||||
}
|
||||
|
||||
public String getRoom()
|
||||
{
|
||||
return room;
|
||||
}
|
||||
|
||||
public void setRoom(String room)
|
||||
{
|
||||
this.room = room;
|
||||
}
|
||||
|
||||
public String getBedNo()
|
||||
{
|
||||
return bedNo;
|
||||
}
|
||||
|
||||
public void setBedNo(String bedNo)
|
||||
{
|
||||
this.bedNo = bedNo;
|
||||
}
|
||||
|
||||
public Integer getAuthStatus()
|
||||
{
|
||||
return authStatus;
|
||||
}
|
||||
|
||||
public void setAuthStatus(Integer authStatus)
|
||||
{
|
||||
this.authStatus = authStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/** 报到摘要嵌套对象 */
|
||||
public static class ReportSummaryVO
|
||||
{
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date reportTime;
|
||||
private Integer reportWay;
|
||||
private String verifyNote;
|
||||
|
||||
public ReportSummaryVO()
|
||||
{
|
||||
}
|
||||
|
||||
public ReportSummaryVO(Date reportTime, Integer reportWay, String verifyNote)
|
||||
{
|
||||
this.reportTime = reportTime;
|
||||
this.reportWay = reportWay;
|
||||
this.verifyNote = verifyNote;
|
||||
}
|
||||
|
||||
public Date getReportTime()
|
||||
{
|
||||
return reportTime;
|
||||
}
|
||||
|
||||
public void setReportTime(Date reportTime)
|
||||
{
|
||||
this.reportTime = reportTime;
|
||||
}
|
||||
|
||||
public Integer getReportWay()
|
||||
{
|
||||
return reportWay;
|
||||
}
|
||||
|
||||
public void setReportWay(Integer reportWay)
|
||||
{
|
||||
this.reportWay = reportWay;
|
||||
}
|
||||
|
||||
public String getVerifyNote()
|
||||
{
|
||||
return verifyNote;
|
||||
}
|
||||
|
||||
public void setVerifyNote(String verifyNote)
|
||||
{
|
||||
this.verifyNote = verifyNote;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 参训学员列表项 VO (UC-06 / API Contract 3.1)。
|
||||
*/
|
||||
public class EnrollmentItemVO
|
||||
{
|
||||
/** 参训档案ID (核心ID之一) */
|
||||
private Long enrollmentId;
|
||||
|
||||
/** 培训班ID */
|
||||
private String planId;
|
||||
|
||||
/** 学员长期主档ID */
|
||||
private String studentId;
|
||||
|
||||
/** 学员姓名 */
|
||||
private String studentName;
|
||||
|
||||
/** 性别 */
|
||||
private String gender;
|
||||
|
||||
/** 身份证脱敏展示 (如 210102********1234) */
|
||||
private String idCardMasked;
|
||||
|
||||
/** 选派单位 */
|
||||
private String orgName;
|
||||
|
||||
/** 职务 */
|
||||
private String postName;
|
||||
|
||||
/** 手机号 */
|
||||
private String phone;
|
||||
|
||||
/** 参训来源 (具体字典值由 train_enrollment_source 定义) */
|
||||
private Integer sourceType;
|
||||
|
||||
/** 参训状态 (0:待报到, 1:已报到, 2:请假, 3:放弃, 4:已完成) */
|
||||
private Integer enrollmentStatus;
|
||||
|
||||
/** 是否需要住宿 (0:否, 1:是) */
|
||||
private Integer accommodationRequired;
|
||||
|
||||
/** 入班报到时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date joinTime;
|
||||
|
||||
/** 培训完成/结课时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date completeTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
public Long getEnrollmentId()
|
||||
{
|
||||
return enrollmentId;
|
||||
}
|
||||
|
||||
public void setEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
this.enrollmentId = enrollmentId;
|
||||
}
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public String getStudentId()
|
||||
{
|
||||
return studentId;
|
||||
}
|
||||
|
||||
public void setStudentId(String studentId)
|
||||
{
|
||||
this.studentId = studentId;
|
||||
}
|
||||
|
||||
public String getStudentName()
|
||||
{
|
||||
return studentName;
|
||||
}
|
||||
|
||||
public void setStudentName(String studentName)
|
||||
{
|
||||
this.studentName = studentName;
|
||||
}
|
||||
|
||||
public String getGender()
|
||||
{
|
||||
return gender;
|
||||
}
|
||||
|
||||
public void setGender(String gender)
|
||||
{
|
||||
this.gender = gender;
|
||||
}
|
||||
|
||||
public String getIdCardMasked()
|
||||
{
|
||||
return idCardMasked;
|
||||
}
|
||||
|
||||
public void setIdCardMasked(String idCardMasked)
|
||||
{
|
||||
this.idCardMasked = idCardMasked;
|
||||
}
|
||||
|
||||
public String getOrgName()
|
||||
{
|
||||
return orgName;
|
||||
}
|
||||
|
||||
public void setOrgName(String orgName)
|
||||
{
|
||||
this.orgName = orgName;
|
||||
}
|
||||
|
||||
public String getPostName()
|
||||
{
|
||||
return postName;
|
||||
}
|
||||
|
||||
public void setPostName(String postName)
|
||||
{
|
||||
this.postName = postName;
|
||||
}
|
||||
|
||||
public String getPhone()
|
||||
{
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone)
|
||||
{
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public Integer getSourceType()
|
||||
{
|
||||
return sourceType;
|
||||
}
|
||||
|
||||
public void setSourceType(Integer sourceType)
|
||||
{
|
||||
this.sourceType = sourceType;
|
||||
}
|
||||
|
||||
public Integer getEnrollmentStatus()
|
||||
{
|
||||
return enrollmentStatus;
|
||||
}
|
||||
|
||||
public void setEnrollmentStatus(Integer enrollmentStatus)
|
||||
{
|
||||
this.enrollmentStatus = enrollmentStatus;
|
||||
}
|
||||
|
||||
public Integer getAccommodationRequired()
|
||||
{
|
||||
return accommodationRequired;
|
||||
}
|
||||
|
||||
public void setAccommodationRequired(Integer accommodationRequired)
|
||||
{
|
||||
this.accommodationRequired = accommodationRequired;
|
||||
}
|
||||
|
||||
public Date getJoinTime()
|
||||
{
|
||||
return joinTime;
|
||||
}
|
||||
|
||||
public void setJoinTime(Date joinTime)
|
||||
{
|
||||
this.joinTime = joinTime;
|
||||
}
|
||||
|
||||
public Date getCompleteTime()
|
||||
{
|
||||
return completeTime;
|
||||
}
|
||||
|
||||
public void setCompleteTime(Date completeTime)
|
||||
{
|
||||
this.completeTime = completeTime;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark)
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package com.ruoyi.train.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.domain.query.EnrollmentQuery;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentItemVO;
|
||||
|
||||
/**
|
||||
* 参训学员登记Mapper接口 train_enrollment。
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
public interface TrainEnrollmentMapper
|
||||
{
|
||||
/**
|
||||
* 锁定开班计划主表行以实现招生容量并发控制 (SELECT ... FOR UPDATE)。
|
||||
*
|
||||
* @param planId 计划ID
|
||||
* @return 开班计划实体
|
||||
*/
|
||||
TrainClassPlan selectPlanForUpdate(@Param("planId") String planId);
|
||||
|
||||
/**
|
||||
* 根据参训ID查询参训记录。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 参训记录
|
||||
*/
|
||||
TrainEnrollment selectTrainEnrollmentById(@Param("enrollmentId") Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 根据计划ID与学员ID查询参训记录(用于唯一性校验)。
|
||||
*
|
||||
* @param planId 培训计划ID
|
||||
* @param studentId 学员ID
|
||||
* @return 参训记录
|
||||
*/
|
||||
TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(@Param("planId") String planId, @Param("studentId") String studentId);
|
||||
|
||||
/**
|
||||
* 分页查询参训人员花名册列表(联查学员主数据)。
|
||||
*
|
||||
* @param query 查询参数
|
||||
* @return 参训人员列表
|
||||
*/
|
||||
List<EnrollmentItemVO> selectEnrollmentItemList(EnrollmentQuery query);
|
||||
|
||||
/**
|
||||
* 统计指定培训计划下的参训人数(排除已放弃3的学员统计,或统计全部)。
|
||||
*
|
||||
* @param planId 培训计划ID
|
||||
* @return 当前计划参训总人数
|
||||
*/
|
||||
int countEnrollmentsByPlanId(@Param("planId") String planId);
|
||||
|
||||
/**
|
||||
* 单条新增参训学员记录。
|
||||
*
|
||||
* @param trainEnrollment 参训记录
|
||||
* @return 插入行数
|
||||
*/
|
||||
int insertTrainEnrollment(TrainEnrollment trainEnrollment);
|
||||
|
||||
/**
|
||||
* 批量新增参训学员记录。
|
||||
*
|
||||
* @param list 待插入列表
|
||||
* @return 插入行数
|
||||
*/
|
||||
int batchInsertTrainEnrollment(@Param("list") List<TrainEnrollment> list);
|
||||
|
||||
/**
|
||||
* 更新参训学员记录。
|
||||
*
|
||||
* @param trainEnrollment 参训记录
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateTrainEnrollment(TrainEnrollment trainEnrollment);
|
||||
|
||||
/**
|
||||
* 变更参训学员状态 (如请假2、放弃3)。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @param enrollmentStatus 新状态
|
||||
* @param remark 备注/原因
|
||||
* @param operator 操作人
|
||||
* @param updateTime 更新时间
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateEnrollmentStatus(@Param("enrollmentId") Long enrollmentId,
|
||||
@Param("enrollmentStatus") Integer enrollmentStatus,
|
||||
@Param("remark") String remark,
|
||||
@Param("operator") String operator,
|
||||
@Param("updateTime") Date updateTime);
|
||||
|
||||
/**
|
||||
* 计划结训时批量将有效在训学员状态更新为已完成(4),并记录结业时间。
|
||||
* 有效状态指未放弃(status != 3)且未完成(status != 4)。
|
||||
*
|
||||
* @param planId 培训班ID
|
||||
* @param operator 操作人
|
||||
* @param completeTime 结训时间
|
||||
* @return 更新行数
|
||||
*/
|
||||
int completeActiveEnrollmentsByPlanId(@Param("planId") String planId,
|
||||
@Param("operator") String operator,
|
||||
@Param("completeTime") Date completeTime);
|
||||
|
||||
/**
|
||||
* 物理删除参训记录。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 删除行数
|
||||
*/
|
||||
int deleteTrainEnrollmentById(@Param("enrollmentId") Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 统计学员考勤签到记录数 (train_student_checkin 下游引用)。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 引用记录数
|
||||
*/
|
||||
int countAttendanceByEnrollmentId(@Param("enrollmentId") Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 统计学员成绩记录数 (train_student_score 下游引用)。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 引用记录数
|
||||
*/
|
||||
int countScoreByEnrollmentId(@Param("enrollmentId") Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 统计学员现场报到记录数 (train_class_report 下游引用)。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 引用记录数
|
||||
*/
|
||||
int countReportByEnrollmentId(@Param("enrollmentId") Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 统计学员住宿分配记录数 (train_accommodation 下游引用)。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 引用记录数
|
||||
*/
|
||||
int countAccommodationByEnrollmentId(@Param("enrollmentId") Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 统计学员自主学习记录数 (train_study_duration 下游引用)。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 引用记录数
|
||||
*/
|
||||
int countStudyDurationByEnrollmentId(@Param("enrollmentId") Long enrollmentId);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.ruoyi.train.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.train.domain.dto.EnrollmentAddDTO;
|
||||
import com.ruoyi.train.domain.dto.EnrollmentStatusUpdateDTO;
|
||||
import com.ruoyi.train.domain.query.EnrollmentQuery;
|
||||
import com.ruoyi.train.domain.vo.BatchImportResultVO;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentDetailVO;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentItemVO;
|
||||
|
||||
/**
|
||||
* 参训人员全流程业务编排应用服务接口 (Application Service 2 / P0)。
|
||||
* 负责跨表业务一致性、招生人数上限并发控制、学员档案全景聚合与生命周期流转。
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
public interface EnrollmentAppService
|
||||
{
|
||||
/**
|
||||
* 查询本期培训参训名单列表 (UC-06)。
|
||||
*
|
||||
* @param planId 培训班ID
|
||||
* @param query 查询参数
|
||||
* @return 参训人员花名册列表
|
||||
*/
|
||||
List<EnrollmentItemVO> listEnrollments(String planId, EnrollmentQuery query);
|
||||
|
||||
/**
|
||||
* 获取指定学员参训档案全景 (P0)。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 档案全景 VO
|
||||
*/
|
||||
EnrollmentDetailVO getEnrollmentDetail(Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 添加单名参训人员到指定培训计划 (UC-04)。
|
||||
*
|
||||
* @param planId 培训班ID
|
||||
* @param dto 添加请求参数
|
||||
* @param operator 操作人
|
||||
* @return 生成的核心参训档案 enrollmentId
|
||||
*/
|
||||
Long addEnrollment(String planId, EnrollmentAddDTO dto, String operator);
|
||||
|
||||
/**
|
||||
* Excel 批量导入参训人员 (UC-05)。
|
||||
*
|
||||
* @param planId 培训班ID
|
||||
* @param file 上传的 Excel 文件
|
||||
* @param autoCreateStudent 是否对不存在的学员自动建档
|
||||
* @param operator 操作人
|
||||
* @return 批量导入汇总结果
|
||||
*/
|
||||
BatchImportResultVO batchImportEnrollments(String planId, MultipartFile file, Boolean autoCreateStudent, String operator);
|
||||
|
||||
/**
|
||||
* 变更参训状态 (UC-06 / P1 冻结范围)。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @param dto 状态变更参数
|
||||
* @param operator 操作人
|
||||
*/
|
||||
void updateEnrollmentStatus(Long enrollmentId, EnrollmentStatusUpdateDTO dto, String operator);
|
||||
|
||||
/**
|
||||
* 移除参训人员 (P1 冻结范围)。
|
||||
* 仅在未产生任何业务数据(考勤/成绩/报到/住宿)且允许删除的阶段执行。
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @param operator 操作人
|
||||
*/
|
||||
void removeEnrollment(Long enrollmentId, String operator);
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.ruoyi.train.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.domain.query.EnrollmentQuery;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentItemVO;
|
||||
|
||||
/**
|
||||
* 参训学员单表持久化服务接口。
|
||||
* 遵循规范:单表 Service 仅负责单表 CRUD 与 Mapper 交互,跨表业务规则与不变量由 EnrollmentAppService 编排。
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
public interface ITrainEnrollmentService
|
||||
{
|
||||
TrainEnrollment selectTrainEnrollmentById(Long enrollmentId);
|
||||
|
||||
TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId);
|
||||
|
||||
List<EnrollmentItemVO> selectEnrollmentItemList(EnrollmentQuery query);
|
||||
|
||||
int countEnrollmentsByPlanId(String planId);
|
||||
|
||||
int insertTrainEnrollment(TrainEnrollment trainEnrollment);
|
||||
|
||||
int batchInsertTrainEnrollment(List<TrainEnrollment> list);
|
||||
|
||||
int updateTrainEnrollment(TrainEnrollment trainEnrollment);
|
||||
|
||||
int updateEnrollmentStatus(Long enrollmentId, Integer enrollmentStatus, String remark, String operator, Date updateTime);
|
||||
|
||||
int completeActiveEnrollmentsByPlanId(String planId, String operator, Date completeTime);
|
||||
|
||||
int deleteTrainEnrollmentById(Long enrollmentId);
|
||||
|
||||
int countAttendanceByEnrollmentId(Long enrollmentId);
|
||||
|
||||
int countScoreByEnrollmentId(Long enrollmentId);
|
||||
|
||||
int countReportByEnrollmentId(Long enrollmentId);
|
||||
|
||||
int countAccommodationByEnrollmentId(Long enrollmentId);
|
||||
|
||||
int countStudyDurationByEnrollmentId(Long enrollmentId);
|
||||
}
|
||||
+579
@@ -0,0 +1,579 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.train.constant.EnrollmentErrorCode;
|
||||
import com.ruoyi.train.constant.PlanErrorCode;
|
||||
import com.ruoyi.train.constant.StudentErrorCode;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.domain.TrainStudent;
|
||||
import com.ruoyi.train.domain.dto.EnrollmentAddDTO;
|
||||
import com.ruoyi.train.domain.dto.EnrollmentStatusUpdateDTO;
|
||||
import com.ruoyi.train.domain.query.EnrollmentQuery;
|
||||
import com.ruoyi.train.domain.vo.BatchImportResultVO;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentDetailVO;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentItemVO;
|
||||
import com.ruoyi.train.mapper.TrainClassPlanMapper;
|
||||
import com.ruoyi.train.mapper.TrainEnrollmentMapper;
|
||||
import com.ruoyi.train.mapper.TrainStudentMapper;
|
||||
import com.ruoyi.train.security.StudentIdentityProtection;
|
||||
import com.ruoyi.train.service.EnrollmentAppService;
|
||||
import com.ruoyi.train.service.ITrainEnrollmentService;
|
||||
|
||||
/**
|
||||
* 参训人员全流程业务编排应用服务实现类 (EnrollmentAppService)。
|
||||
*
|
||||
* 核心架构职责:
|
||||
* 1. 跨表业务编排与业务不变量防护;
|
||||
* 2. 招生人数上限 (recruit_limit) 并发一致性校验 (SELECT ... FOR UPDATE 锁 Plan 主表行);
|
||||
* 3. 拦截数据库 UNIQUE(plan_id, student_id) 并发冲突并统一映射为冻结错误码 40030;
|
||||
* 4. 参训人员生命周期流转与非法状态防护;
|
||||
* 5. 开班前删除校验与防物理级联删除保护 (已有考勤/成绩/报到流水时强阻断 40032);
|
||||
* 6. 学员身份证敏感数据展示脱敏掩码治理。
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@Service
|
||||
public class EnrollmentAppServiceImpl implements EnrollmentAppService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(EnrollmentAppServiceImpl.class);
|
||||
|
||||
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
private static final int MAX_BATCH_ROWS = 500;
|
||||
|
||||
@Autowired
|
||||
private ITrainEnrollmentService trainEnrollmentService;
|
||||
|
||||
@Autowired
|
||||
private TrainEnrollmentMapper trainEnrollmentMapper;
|
||||
|
||||
@Autowired
|
||||
private TrainClassPlanMapper planMapper;
|
||||
|
||||
@Autowired
|
||||
private TrainStudentMapper studentMapper;
|
||||
|
||||
@Autowired(required = false)
|
||||
private StudentIdentityProtection identityProtection;
|
||||
|
||||
@Override
|
||||
public List<EnrollmentItemVO> listEnrollments(String planId, EnrollmentQuery query)
|
||||
{
|
||||
requirePlan(planId);
|
||||
if (query == null)
|
||||
{
|
||||
query = new EnrollmentQuery();
|
||||
}
|
||||
query.setPlanId(planId);
|
||||
List<EnrollmentItemVO> list = trainEnrollmentService.selectEnrollmentItemList(query);
|
||||
for (EnrollmentItemVO item : list)
|
||||
{
|
||||
item.setIdCardMasked(maskIdentity(item.getIdCardMasked()));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnrollmentDetailVO getEnrollmentDetail(Long enrollmentId)
|
||||
{
|
||||
TrainEnrollment enrollment = requireEnrollment(enrollmentId);
|
||||
TrainClassPlan plan = requirePlan(enrollment.getPlanId());
|
||||
TrainStudent student = studentMapper.selectTrainStudentBySId(enrollment.getStudentId());
|
||||
|
||||
EnrollmentDetailVO detail = new EnrollmentDetailVO();
|
||||
detail.setEnrollmentId(enrollment.getEnrollmentId());
|
||||
detail.setPlanId(enrollment.getPlanId());
|
||||
detail.setPlanName(plan.getPlanName());
|
||||
detail.setStudentId(enrollment.getStudentId());
|
||||
detail.setSourceType(enrollment.getSourceType());
|
||||
detail.setEnrollmentStatus(enrollment.getEnrollmentStatus());
|
||||
detail.setAccommodationRequired(enrollment.getAccommodationRequired());
|
||||
detail.setJoinTime(enrollment.getJoinTime());
|
||||
detail.setCompleteTime(enrollment.getCompleteTime());
|
||||
detail.setRemark(enrollment.getRemark());
|
||||
|
||||
if (student != null)
|
||||
{
|
||||
detail.setStudentName(student.getsName());
|
||||
detail.setGender(student.getsGender());
|
||||
detail.setOrgName(student.getOrgName());
|
||||
detail.setPhone(student.getsPhone());
|
||||
detail.setIdCardMasked(maskStudentIdentity(student));
|
||||
}
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long addEnrollment(String planId, EnrollmentAddDTO dto, String operator)
|
||||
{
|
||||
validateEnrollmentAddDTO(dto);
|
||||
|
||||
// 1. 事务中先锁稳定 Plan 主表行: SELECT ... FROM train_class_plan WHERE plan_id = ? FOR UPDATE
|
||||
TrainClassPlan plan = trainEnrollmentMapper.selectPlanForUpdate(planId);
|
||||
if (plan == null || "2".equals(plan.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_NOT_FOUND_MESSAGE, PlanErrorCode.PLAN_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 2. 校验 Plan 状态
|
||||
checkPlanStatusNotLocked(plan);
|
||||
|
||||
// 3. count 当前 Enrollment 并校验 recruitNum
|
||||
if (plan.getRecruitNum() != null && plan.getRecruitNum() > 0)
|
||||
{
|
||||
int currentCount = trainEnrollmentService.countEnrollmentsByPlanId(planId);
|
||||
if (currentCount >= plan.getRecruitNum())
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.RECRUIT_LIMIT_EXCEEDED_MESSAGE, EnrollmentErrorCode.RECRUIT_LIMIT_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 校验学员档案有效性
|
||||
TrainStudent student = studentMapper.selectTrainStudentBySId(dto.getStudentId());
|
||||
if (student == null || "2".equals(student.getDelFlag()) || (student.getStatus() != null && student.getStatus() != 0))
|
||||
{
|
||||
throw new ServiceException(StudentErrorCode.STUDENT_NOT_FOUND_MESSAGE, StudentErrorCode.STUDENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 5. duplicate 预检查
|
||||
TrainEnrollment existing = trainEnrollmentService.selectTrainEnrollmentByPlanIdAndStudentId(planId, dto.getStudentId());
|
||||
if (existing != null)
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_DUPLICATED_MESSAGE, EnrollmentErrorCode.ENROLLMENT_DUPLICATED);
|
||||
}
|
||||
|
||||
// 6. 组装实体并持久化 (DB UNIQUE 约束并发兜底)
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setPlanId(planId);
|
||||
enrollment.setStudentId(dto.getStudentId());
|
||||
enrollment.setSourceType(dto.getSourceType());
|
||||
enrollment.setAccommodationRequired(dto.getAccommodationRequired());
|
||||
enrollment.setEnrollmentStatus(EnrollmentErrorCode.STATUS_WAITING_REPORT); // 初始为 0:待报到
|
||||
enrollment.setRemark(dto.getRemark());
|
||||
enrollment.setCreateBy(operator);
|
||||
enrollment.setCreateTime(DateUtils.getNowDate());
|
||||
|
||||
try
|
||||
{
|
||||
trainEnrollmentService.insertTrainEnrollment(enrollment);
|
||||
return enrollment.getEnrollmentId();
|
||||
}
|
||||
catch (DuplicateKeyException e)
|
||||
{
|
||||
// 捕获并发 UNIQUE(plan_id, student_id) 冲突并统一映射为 40030
|
||||
log.warn("并发添加参训人员发生唯一约束冲突: planId={}, studentId={}", planId, dto.getStudentId());
|
||||
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_DUPLICATED_MESSAGE, EnrollmentErrorCode.ENROLLMENT_DUPLICATED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BatchImportResultVO batchImportEnrollments(String planId, MultipartFile file, Boolean autoCreateStudent, String operator)
|
||||
{
|
||||
if (file == null || file.isEmpty())
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.EXCEL_PARSE_ERROR_MESSAGE, EnrollmentErrorCode.EXCEL_PARSE_ERROR);
|
||||
}
|
||||
if (file.getSize() > MAX_FILE_SIZE)
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.FILE_SIZE_EXCEEDED_MESSAGE, EnrollmentErrorCode.FILE_SIZE_EXCEEDED);
|
||||
}
|
||||
|
||||
// 批量导入涉及同一 Plan 时遵守相同 Plan 锁语义 (SELECT ... FOR UPDATE)
|
||||
TrainClassPlan plan = trainEnrollmentMapper.selectPlanForUpdate(planId);
|
||||
if (plan == null || "2".equals(plan.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_NOT_FOUND_MESSAGE, PlanErrorCode.PLAN_NOT_FOUND);
|
||||
}
|
||||
checkPlanStatusNotLocked(plan);
|
||||
|
||||
List<EnrollmentAddDTO> dtoList;
|
||||
try (InputStream is = file.getInputStream())
|
||||
{
|
||||
dtoList = parseExcelToDTOList(is);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("Excel解析失败: {}", e.getMessage(), e);
|
||||
throw new ServiceException(EnrollmentErrorCode.EXCEL_PARSE_ERROR_MESSAGE, EnrollmentErrorCode.EXCEL_PARSE_ERROR);
|
||||
}
|
||||
|
||||
return processBatchImport(plan, dtoList, Boolean.TRUE.equals(autoCreateStudent), operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入处理引擎 (供 Excel 导入与批量接口复用)。
|
||||
*/
|
||||
public BatchImportResultVO processBatchImport(TrainClassPlan plan, List<EnrollmentAddDTO> dtoList, boolean autoCreateStudent, String operator)
|
||||
{
|
||||
String planId = plan.getPlanId();
|
||||
int totalCount = dtoList != null ? dtoList.size() : 0;
|
||||
int successCount = 0;
|
||||
int failureCount = 0;
|
||||
List<String> errorList = new ArrayList<>();
|
||||
|
||||
if (totalCount > MAX_BATCH_ROWS)
|
||||
{
|
||||
throw new ServiceException("单批次导入人数不得超过 " + MAX_BATCH_ROWS + " 行");
|
||||
}
|
||||
|
||||
int currentCount = trainEnrollmentService.countEnrollmentsByPlanId(planId);
|
||||
|
||||
for (int i = 0; i < totalCount; i++)
|
||||
{
|
||||
int rowNum = i + 1;
|
||||
EnrollmentAddDTO dto = dtoList.get(i);
|
||||
if (dto == null || StringUtils.isEmpty(dto.getStudentId()))
|
||||
{
|
||||
failureCount++;
|
||||
errorList.add("第 " + rowNum + " 行: 学员ID不能为空");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dto.getSourceType() == null)
|
||||
{
|
||||
dto.setSourceType(2); // 默认 2:Excel批量导入
|
||||
}
|
||||
if (dto.getAccommodationRequired() == null)
|
||||
{
|
||||
dto.setAccommodationRequired(EnrollmentErrorCode.ACCOMMODATION_NOT_REQUIRED);
|
||||
}
|
||||
|
||||
// 招生上限校验
|
||||
if (plan.getRecruitNum() != null && plan.getRecruitNum() > 0 && currentCount >= plan.getRecruitNum())
|
||||
{
|
||||
failureCount++;
|
||||
errorList.add("第 " + rowNum + " 行: 计划已达到招生上限(" + plan.getRecruitNum() + "人)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 学员存在性校验
|
||||
TrainStudent student = studentMapper.selectTrainStudentBySId(dto.getStudentId());
|
||||
if (student == null)
|
||||
{
|
||||
if (autoCreateStudent)
|
||||
{
|
||||
// 自动建立基础学员档案
|
||||
student = new TrainStudent();
|
||||
student.setsId(dto.getStudentId());
|
||||
student.setsName(StringUtils.isNotEmpty(dto.getRemark()) ? dto.getRemark() : "导入学员_" + dto.getStudentId());
|
||||
student.setOrgName(dto.getOrgName());
|
||||
student.setSourceType(2); // 2:Excel批量导入
|
||||
student.setStatus(0);
|
||||
student.setDelFlag("0");
|
||||
student.setCreateBy(operator);
|
||||
student.setCreateTime(DateUtils.getNowDate());
|
||||
try
|
||||
{
|
||||
studentMapper.insertTrainStudent(student);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.warn("自动建档失败: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
failureCount++;
|
||||
errorList.add("第 " + rowNum + " 行: 学员档案不存在(" + dto.getStudentId() + ")");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 重复入班校验
|
||||
TrainEnrollment existing = trainEnrollmentService.selectTrainEnrollmentByPlanIdAndStudentId(planId, dto.getStudentId());
|
||||
if (existing != null)
|
||||
{
|
||||
failureCount++;
|
||||
errorList.add("第 " + rowNum + " 行: 学员已在参训名单中(" + dto.getStudentId() + ")");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 入库
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setPlanId(planId);
|
||||
enrollment.setStudentId(dto.getStudentId());
|
||||
enrollment.setSourceType(dto.getSourceType());
|
||||
enrollment.setAccommodationRequired(dto.getAccommodationRequired());
|
||||
enrollment.setEnrollmentStatus(EnrollmentErrorCode.STATUS_WAITING_REPORT);
|
||||
enrollment.setRemark(dto.getRemark());
|
||||
enrollment.setCreateBy(operator);
|
||||
enrollment.setCreateTime(DateUtils.getNowDate());
|
||||
|
||||
try
|
||||
{
|
||||
trainEnrollmentService.insertTrainEnrollment(enrollment);
|
||||
successCount++;
|
||||
currentCount++;
|
||||
}
|
||||
catch (DuplicateKeyException e)
|
||||
{
|
||||
failureCount++;
|
||||
errorList.add("第 " + rowNum + " 行: 并发重复入班(" + dto.getStudentId() + ")");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
failureCount++;
|
||||
errorList.add("第 " + rowNum + " 行: 插入失败 (" + e.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
return new BatchImportResultVO(totalCount, successCount, failureCount, errorList);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateEnrollmentStatus(Long enrollmentId, EnrollmentStatusUpdateDTO dto, String operator)
|
||||
{
|
||||
if (dto == null || dto.getEnrollmentStatus() == null)
|
||||
{
|
||||
throw new ServiceException("参训状态不能为空");
|
||||
}
|
||||
|
||||
Integer newStatus = dto.getEnrollmentStatus();
|
||||
// 严格限制在冻结状态集合: 0待报到 1已报到 2请假 3放弃 4已完成
|
||||
if (newStatus < EnrollmentErrorCode.STATUS_WAITING_REPORT || newStatus > EnrollmentErrorCode.STATUS_COMPLETED)
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_STATUS_INVALID_MESSAGE);
|
||||
}
|
||||
|
||||
TrainEnrollment enrollment = requireEnrollment(enrollmentId);
|
||||
// 计划已结训或取消时,禁止状态变更
|
||||
TrainClassPlan plan = requirePlan(enrollment.getPlanId());
|
||||
checkPlanStatusNotLocked(plan);
|
||||
|
||||
// 如果已经结训(4),禁止任意更改状态
|
||||
if (Integer.valueOf(EnrollmentErrorCode.STATUS_COMPLETED).equals(enrollment.getEnrollmentStatus()))
|
||||
{
|
||||
throw new ServiceException("该学员已完成结训归档,不可修改参训状态");
|
||||
}
|
||||
|
||||
trainEnrollmentService.updateEnrollmentStatus(
|
||||
enrollmentId,
|
||||
newStatus,
|
||||
dto.getRemark(),
|
||||
operator,
|
||||
DateUtils.getNowDate()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeEnrollment(Long enrollmentId, String operator)
|
||||
{
|
||||
TrainEnrollment enrollment = requireEnrollment(enrollmentId);
|
||||
TrainClassPlan plan = requirePlan(enrollment.getPlanId());
|
||||
|
||||
// 阶段校验:已结训(2)或已取消(3)禁止删除
|
||||
if (Integer.valueOf(2).equals(plan.getPlanStatus()) || Integer.valueOf(3).equals(plan.getPlanStatus()))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_STATUS_LOCKED_MESSAGE, PlanErrorCode.PLAN_STATUS_LOCKED);
|
||||
}
|
||||
|
||||
// 业务依赖校验:已产生考勤或成绩等业务数据禁止物理删除,必须提示转为放弃状态
|
||||
int attendanceCount = trainEnrollmentService.countAttendanceByEnrollmentId(enrollmentId);
|
||||
int scoreCount = trainEnrollmentService.countScoreByEnrollmentId(enrollmentId);
|
||||
int reportCount = trainEnrollmentService.countReportByEnrollmentId(enrollmentId);
|
||||
int accommCount = trainEnrollmentService.countAccommodationByEnrollmentId(enrollmentId);
|
||||
int studyCount = trainEnrollmentService.countStudyDurationByEnrollmentId(enrollmentId);
|
||||
|
||||
if (attendanceCount > 0 || scoreCount > 0 || reportCount > 0 || accommCount > 0 || studyCount > 0)
|
||||
{
|
||||
throw new ServiceException(
|
||||
EnrollmentErrorCode.ENROLLMENT_HAS_BUSINESS_DATA_MESSAGE,
|
||||
EnrollmentErrorCode.ENROLLMENT_HAS_BUSINESS_DATA
|
||||
);
|
||||
}
|
||||
|
||||
// 严禁 cascade delete,物理删除参训单表行
|
||||
trainEnrollmentService.deleteTrainEnrollmentById(enrollmentId);
|
||||
}
|
||||
|
||||
private TrainClassPlan requirePlan(String planId)
|
||||
{
|
||||
if (StringUtils.isEmpty(planId))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_NOT_FOUND_MESSAGE, PlanErrorCode.PLAN_NOT_FOUND);
|
||||
}
|
||||
TrainClassPlan plan = planMapper.selectTrainClassPlanByPlanId(planId);
|
||||
if (plan == null || "2".equals(plan.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_NOT_FOUND_MESSAGE, PlanErrorCode.PLAN_NOT_FOUND);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
private TrainEnrollment requireEnrollment(Long enrollmentId)
|
||||
{
|
||||
if (enrollmentId == null)
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_NOT_FOUND_MESSAGE, EnrollmentErrorCode.ENROLLMENT_NOT_FOUND);
|
||||
}
|
||||
TrainEnrollment enrollment = trainEnrollmentService.selectTrainEnrollmentById(enrollmentId);
|
||||
if (enrollment == null)
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_NOT_FOUND_MESSAGE, EnrollmentErrorCode.ENROLLMENT_NOT_FOUND);
|
||||
}
|
||||
return enrollment;
|
||||
}
|
||||
|
||||
private void checkPlanStatusNotLocked(TrainClassPlan plan)
|
||||
{
|
||||
if (Integer.valueOf(2).equals(plan.getPlanStatus()) || Integer.valueOf(3).equals(plan.getPlanStatus()))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_STATUS_LOCKED_MESSAGE, PlanErrorCode.PLAN_STATUS_LOCKED);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateEnrollmentAddDTO(EnrollmentAddDTO dto)
|
||||
{
|
||||
if (dto == null)
|
||||
{
|
||||
throw new ServiceException("请求参数不能为空");
|
||||
}
|
||||
if (StringUtils.isEmpty(dto.getStudentId()))
|
||||
{
|
||||
throw new ServiceException("学员ID不能为空");
|
||||
}
|
||||
if (dto.getSourceType() == null)
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_SOURCE_REQUIRED_MESSAGE);
|
||||
}
|
||||
if (dto.getAccommodationRequired() == null)
|
||||
{
|
||||
throw new ServiceException(EnrollmentErrorCode.ACCOMMODATION_REQUIRED_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private String maskIdentity(String idCardLast4OrPlain)
|
||||
{
|
||||
if (StringUtils.isEmpty(idCardLast4OrPlain))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
if (idCardLast4OrPlain.length() <= 4)
|
||||
{
|
||||
return "**************" + idCardLast4OrPlain;
|
||||
}
|
||||
if (idCardLast4OrPlain.length() == 18)
|
||||
{
|
||||
return idCardLast4OrPlain.substring(0, 6) + "********" + idCardLast4OrPlain.substring(14);
|
||||
}
|
||||
return idCardLast4OrPlain;
|
||||
}
|
||||
|
||||
private String maskStudentIdentity(TrainStudent student)
|
||||
{
|
||||
if (student == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
if (identityProtection != null && StringUtils.isNotEmpty(student.getIdCardCiphertext()))
|
||||
{
|
||||
try
|
||||
{
|
||||
String plain = identityProtection.decrypt(student.getIdCardCiphertext());
|
||||
return identityProtection.mask(plain);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.debug("解密掩码脱敏异常: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotEmpty(student.getIdCardLast4()))
|
||||
{
|
||||
return "**************" + student.getIdCardLast4();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private List<EnrollmentAddDTO> parseExcelToDTOList(InputStream is) throws Exception
|
||||
{
|
||||
List<EnrollmentAddDTO> list = new ArrayList<>();
|
||||
Workbook workbook = WorkbookFactory.create(is);
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
int rowCount = sheet.getPhysicalNumberOfRows();
|
||||
// 假定第 1 行是表头,从第 2 行 (index=1) 开始读
|
||||
for (int r = 1; r < rowCount; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String studentId = getCellValue(row.getCell(0));
|
||||
if (StringUtils.isEmpty(studentId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String sourceStr = getCellValue(row.getCell(1));
|
||||
String accommStr = getCellValue(row.getCell(2));
|
||||
String remark = getCellValue(row.getCell(3));
|
||||
|
||||
EnrollmentAddDTO dto = new EnrollmentAddDTO();
|
||||
dto.setStudentId(studentId);
|
||||
try
|
||||
{
|
||||
dto.setSourceType(StringUtils.isNotEmpty(sourceStr) ? Integer.parseInt(sourceStr) : 2);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
dto.setSourceType(2);
|
||||
}
|
||||
try
|
||||
{
|
||||
dto.setAccommodationRequired(StringUtils.isNotEmpty(accommStr) ? Integer.parseInt(accommStr) : 0);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
dto.setAccommodationRequired(0);
|
||||
}
|
||||
dto.setRemark(remark);
|
||||
list.add(dto);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String getCellValue(Cell cell)
|
||||
{
|
||||
if (cell == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
if (cell.getCellType() == CellType.STRING)
|
||||
{
|
||||
return cell.getStringCellValue().trim();
|
||||
}
|
||||
else if (cell.getCellType() == CellType.NUMERIC)
|
||||
{
|
||||
return String.valueOf((long) cell.getNumericCellValue()).trim();
|
||||
}
|
||||
else if (cell.getCellType() == CellType.BOOLEAN)
|
||||
{
|
||||
return String.valueOf(cell.getBooleanCellValue());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.train.service.ITrainEnrollmentService;
|
||||
import com.ruoyi.train.service.port.PlanEnrollmentCompletionPort;
|
||||
|
||||
/**
|
||||
* 计划结训联动参训学员归档适配器 (实现 PlanEnrollmentCompletionPort)。
|
||||
* 严禁 NoOp 桩实现。本适配器与 PlanAppService.completePlan 处于同一本地事务中。
|
||||
* 业务规则:
|
||||
* 1. 将该计划仍处于有效参训状态 (0:待报到, 1:已报到) 的学员更新为 4:已完成;
|
||||
* 2. 写入 complete_time;
|
||||
* 3. 完整保留考勤、成绩、自主学习时长等历史业务数据,严禁物理删除或级联删除。
|
||||
*/
|
||||
@Component
|
||||
public class EnrollmentPlanCompletionAdapter implements PlanEnrollmentCompletionPort
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(EnrollmentPlanCompletionAdapter.class);
|
||||
|
||||
@Autowired
|
||||
private ITrainEnrollmentService trainEnrollmentService;
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)
|
||||
public void completeEnrollments(String planId, String operator, Date completeTime)
|
||||
{
|
||||
log.info("执行计划结训学员状态联动归档: planId={}, operator={}, completeTime={}", planId, operator, completeTime);
|
||||
int updatedRows = trainEnrollmentService.completeActiveEnrollmentsByPlanId(planId, operator, completeTime);
|
||||
log.info("计划结训学员归档完成: planId={}, 更新记录数={}", planId, updatedRows);
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.domain.query.EnrollmentQuery;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentItemVO;
|
||||
import com.ruoyi.train.mapper.TrainEnrollmentMapper;
|
||||
import com.ruoyi.train.service.ITrainEnrollmentService;
|
||||
|
||||
/**
|
||||
* 参训学员单表持久化服务实现。
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@Service
|
||||
public class TrainEnrollmentServiceImpl implements ITrainEnrollmentService
|
||||
{
|
||||
@Autowired
|
||||
private TrainEnrollmentMapper trainEnrollmentMapper;
|
||||
|
||||
@Override
|
||||
public TrainEnrollment selectTrainEnrollmentById(Long enrollmentId)
|
||||
{
|
||||
return trainEnrollmentMapper.selectTrainEnrollmentById(enrollmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId)
|
||||
{
|
||||
return trainEnrollmentMapper.selectTrainEnrollmentByPlanIdAndStudentId(planId, studentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EnrollmentItemVO> selectEnrollmentItemList(EnrollmentQuery query)
|
||||
{
|
||||
return trainEnrollmentMapper.selectEnrollmentItemList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countEnrollmentsByPlanId(String planId)
|
||||
{
|
||||
return trainEnrollmentMapper.countEnrollmentsByPlanId(planId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertTrainEnrollment(TrainEnrollment trainEnrollment)
|
||||
{
|
||||
return trainEnrollmentMapper.insertTrainEnrollment(trainEnrollment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchInsertTrainEnrollment(List<TrainEnrollment> list)
|
||||
{
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return trainEnrollmentMapper.batchInsertTrainEnrollment(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainEnrollment(TrainEnrollment trainEnrollment)
|
||||
{
|
||||
return trainEnrollmentMapper.updateTrainEnrollment(trainEnrollment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateEnrollmentStatus(Long enrollmentId, Integer enrollmentStatus, String remark, String operator, Date updateTime)
|
||||
{
|
||||
return trainEnrollmentMapper.updateEnrollmentStatus(enrollmentId, enrollmentStatus, remark, operator, updateTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int completeActiveEnrollmentsByPlanId(String planId, String operator, Date completeTime)
|
||||
{
|
||||
return trainEnrollmentMapper.completeActiveEnrollmentsByPlanId(planId, operator, completeTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainEnrollmentById(Long enrollmentId)
|
||||
{
|
||||
return trainEnrollmentMapper.deleteTrainEnrollmentById(enrollmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countAttendanceByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return trainEnrollmentMapper.countAttendanceByEnrollmentId(enrollmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countScoreByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return trainEnrollmentMapper.countScoreByEnrollmentId(enrollmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countReportByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return trainEnrollmentMapper.countReportByEnrollmentId(enrollmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countAccommodationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return trainEnrollmentMapper.countAccommodationByEnrollmentId(enrollmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countStudyDurationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return trainEnrollmentMapper.countStudyDurationByEnrollmentId(enrollmentId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.train.mapper.TrainEnrollmentMapper">
|
||||
|
||||
<resultMap type="com.ruoyi.train.domain.TrainEnrollment" id="TrainEnrollmentResult">
|
||||
<id property="enrollmentId" column="enrollment_id" />
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="studentId" column="student_id" />
|
||||
<result property="sourceType" column="source_type" />
|
||||
<result property="enrollmentStatus" column="enrollment_status" />
|
||||
<result property="accommodationRequired" column="accommodation_required" />
|
||||
<result property="joinTime" column="join_time" />
|
||||
<result property="completeTime" column="complete_time" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectTrainEnrollmentDomain">
|
||||
select enrollment_id, plan_id, student_id, source_type, enrollment_status,
|
||||
accommodation_required, join_time, complete_time, create_by, create_time,
|
||||
update_by, update_time, remark
|
||||
from train_enrollment
|
||||
</sql>
|
||||
|
||||
<select id="selectPlanForUpdate" resultType="com.ruoyi.train.domain.TrainClassPlan">
|
||||
select plan_id as planId,
|
||||
plan_name as planName,
|
||||
plan_status as planStatus,
|
||||
recruit_num as recruitNum,
|
||||
start_time as startTime,
|
||||
end_time as endTime,
|
||||
del_flag as delFlag
|
||||
from train_class_plan
|
||||
where plan_id = #{planId}
|
||||
for update
|
||||
</select>
|
||||
|
||||
<select id="selectTrainEnrollmentById" parameterType="Long" resultMap="TrainEnrollmentResult">
|
||||
<include refid="selectTrainEnrollmentDomain" />
|
||||
where enrollment_id = #{enrollmentId}
|
||||
</select>
|
||||
|
||||
<select id="selectTrainEnrollmentByPlanIdAndStudentId" resultMap="TrainEnrollmentResult">
|
||||
<include refid="selectTrainEnrollmentDomain" />
|
||||
where plan_id = #{planId} and student_id = #{studentId}
|
||||
</select>
|
||||
|
||||
<select id="selectEnrollmentItemList" parameterType="com.ruoyi.train.domain.query.EnrollmentQuery"
|
||||
resultType="com.ruoyi.train.domain.vo.EnrollmentItemVO">
|
||||
select e.enrollment_id as enrollmentId,
|
||||
e.plan_id as planId,
|
||||
e.student_id as studentId,
|
||||
s.s_name as studentName,
|
||||
s.s_gender as gender,
|
||||
s.id_card_last4 as idCardMasked,
|
||||
s.org_name as orgName,
|
||||
s.s_phone as phone,
|
||||
e.source_type as sourceType,
|
||||
e.enrollment_status as enrollmentStatus,
|
||||
e.accommodation_required as accommodationRequired,
|
||||
e.join_time as joinTime,
|
||||
e.complete_time as completeTime,
|
||||
e.remark as remark
|
||||
from train_enrollment e
|
||||
left join train_student s on e.student_id = s.s_id
|
||||
<where>
|
||||
e.plan_id = #{planId}
|
||||
<if test="studentName != null and studentName != ''">
|
||||
and s.s_name like concat('%', #{studentName}, '%')
|
||||
</if>
|
||||
<if test="enrollmentStatus != null">
|
||||
and e.enrollment_status = #{enrollmentStatus}
|
||||
</if>
|
||||
<if test="accommodationRequired != null">
|
||||
and e.accommodation_required = #{accommodationRequired}
|
||||
</if>
|
||||
</where>
|
||||
order by e.enrollment_id asc
|
||||
</select>
|
||||
|
||||
<select id="countEnrollmentsByPlanId" parameterType="String" resultType="int">
|
||||
select count(1) from train_enrollment where plan_id = #{planId}
|
||||
</select>
|
||||
|
||||
<insert id="insertTrainEnrollment" parameterType="com.ruoyi.train.domain.TrainEnrollment"
|
||||
useGeneratedKeys="true" keyProperty="enrollmentId">
|
||||
insert into train_enrollment (
|
||||
plan_id, student_id, source_type, enrollment_status, accommodation_required,
|
||||
join_time, complete_time, create_by, create_time, update_by, update_time, remark
|
||||
) values (
|
||||
#{planId}, #{studentId}, #{sourceType}, #{enrollmentStatus}, #{accommodationRequired},
|
||||
#{joinTime}, #{completeTime}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<insert id="batchInsertTrainEnrollment">
|
||||
insert into train_enrollment (
|
||||
plan_id, student_id, source_type, enrollment_status, accommodation_required,
|
||||
join_time, complete_time, create_by, create_time, remark
|
||||
) values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(
|
||||
#{item.planId}, #{item.studentId}, #{item.sourceType}, #{item.enrollmentStatus},
|
||||
#{item.accommodationRequired}, #{item.joinTime}, #{item.completeTime},
|
||||
#{item.createBy}, #{item.createTime}, #{item.remark}
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<update id="updateTrainEnrollment" parameterType="com.ruoyi.train.domain.TrainEnrollment">
|
||||
update train_enrollment
|
||||
<set>
|
||||
<if test="sourceType != null">source_type = #{sourceType},</if>
|
||||
<if test="enrollmentStatus != null">enrollment_status = #{enrollmentStatus},</if>
|
||||
<if test="accommodationRequired != null">accommodation_required = #{accommodationRequired},</if>
|
||||
<if test="joinTime != null">join_time = #{joinTime},</if>
|
||||
<if test="completeTime != null">complete_time = #{completeTime},</if>
|
||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</set>
|
||||
where enrollment_id = #{enrollmentId}
|
||||
</update>
|
||||
|
||||
<update id="updateEnrollmentStatus">
|
||||
update train_enrollment
|
||||
set enrollment_status = #{enrollmentStatus},
|
||||
remark = coalesce(#{remark}, remark),
|
||||
update_by = #{operator},
|
||||
update_time = #{updateTime}
|
||||
where enrollment_id = #{enrollmentId}
|
||||
</update>
|
||||
|
||||
<update id="completeActiveEnrollmentsByPlanId">
|
||||
update train_enrollment
|
||||
set enrollment_status = 4,
|
||||
complete_time = #{completeTime},
|
||||
update_by = #{operator},
|
||||
update_time = #{completeTime}
|
||||
where plan_id = #{planId}
|
||||
and enrollment_status in (0, 1)
|
||||
</update>
|
||||
|
||||
<delete id="deleteTrainEnrollmentById" parameterType="Long">
|
||||
delete from train_enrollment where enrollment_id = #{enrollmentId}
|
||||
</delete>
|
||||
|
||||
<select id="countAttendanceByEnrollmentId" parameterType="Long" resultType="int">
|
||||
select count(1) from train_student_checkin where enrollment_id = #{enrollmentId}
|
||||
</select>
|
||||
|
||||
<select id="countScoreByEnrollmentId" parameterType="Long" resultType="int">
|
||||
select count(1) from train_student_score where enrollment_id = #{enrollmentId}
|
||||
</select>
|
||||
|
||||
<select id="countReportByEnrollmentId" parameterType="Long" resultType="int">
|
||||
select count(1) from train_class_report where enrollment_id = #{enrollmentId}
|
||||
</select>
|
||||
|
||||
<select id="countAccommodationByEnrollmentId" parameterType="Long" resultType="int">
|
||||
select count(1) from train_accommodation where enrollment_id = #{enrollmentId}
|
||||
</select>
|
||||
|
||||
<select id="countStudyDurationByEnrollmentId" parameterType="Long" resultType="int">
|
||||
select count(1) from train_study_duration where enrollment_id = #{enrollmentId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
+1021
File diff suppressed because it is too large.
Load diff
+381
@@ -0,0 +1,381 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.domain.query.EnrollmentQuery;
|
||||
import com.ruoyi.train.domain.query.PlanQuery;
|
||||
import com.ruoyi.train.domain.vo.EnrollmentItemVO;
|
||||
import com.ruoyi.train.domain.vo.PlanItemVO;
|
||||
import com.ruoyi.train.service.ITrainClassPlanService;
|
||||
import com.ruoyi.train.service.ITrainEnrollmentService;
|
||||
import com.ruoyi.train.service.port.PlanEnrollmentCompletionPort;
|
||||
|
||||
/**
|
||||
* AC-03b 专项集成测试:验证 Plan complete -> 真实 Enrollment 联动归档。
|
||||
* 对应 docs/development/issue-02-01-plan.md 中冻结的 AC-03b 验收门禁。
|
||||
* 验证:
|
||||
* 1. 计划 completePlan 真实驱动 EnrollmentPlanCompletionAdapter 执行;
|
||||
* 2. 计划内 30 名有效状态(0:待报到、1:已报到)学员状态统一更新为 4:已完成;
|
||||
* 3. 真实写入 complete_time;
|
||||
* 4. 已放弃(3)的学员状态不受影响;
|
||||
* 5. 学员历史考勤/成绩/打卡/学时记录完好保留,严禁物理或级联删除;
|
||||
* 6. 验证 Adapter 事务注解为 MANDATORY,确保纳入同一本地强事务边界。
|
||||
*/
|
||||
public class PlanEnrollmentCompletionIntegrationTest
|
||||
{
|
||||
private MemoryPlanService planService;
|
||||
private MemoryEnrollmentService enrollmentService;
|
||||
private EnrollmentPlanCompletionAdapter realAdapter;
|
||||
private PlanAppServiceImpl planAppService;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
planService = new MemoryPlanService();
|
||||
enrollmentService = new MemoryEnrollmentService();
|
||||
|
||||
// 实例化真实 Adapter 并注入真实单表 service
|
||||
realAdapter = new EnrollmentPlanCompletionAdapter();
|
||||
setField(realAdapter, "trainEnrollmentService", enrollmentService);
|
||||
|
||||
// 将真实 Adapter 注入 PlanAppService
|
||||
List<PlanEnrollmentCompletionPort> ports = Collections.singletonList(realAdapter);
|
||||
planAppService = new PlanAppServiceImpl(planService, ports);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAc03bRealEnrollmentCompletionBatchOf30Students()
|
||||
{
|
||||
String planId = "PLAN-2026-SIGNAL";
|
||||
TrainClassPlan plan = new TrainClassPlan();
|
||||
plan.setPlanId(planId);
|
||||
plan.setPlanName("2026年高速铁路信号综合班");
|
||||
plan.setPlanStatus(1); // 进行中
|
||||
planService.planStore.put(planId, plan);
|
||||
|
||||
// 创建 30 名有效学员 (20 名已报到 1,10 名待报到 0)
|
||||
List<Long> activeEnrollmentIds = new ArrayList<>();
|
||||
for (int i = 1; i <= 30; i++)
|
||||
{
|
||||
TrainEnrollment e = new TrainEnrollment();
|
||||
e.setEnrollmentId((long) i);
|
||||
e.setPlanId(planId);
|
||||
e.setStudentId("STU-" + i);
|
||||
e.setSourceType(1);
|
||||
e.setAccommodationRequired(1);
|
||||
e.setEnrollmentStatus(i <= 20 ? 1 : 0); // 1~20:已报到, 21~30:待报到
|
||||
enrollmentService.enrollmentStore.put(e.getEnrollmentId(), e);
|
||||
activeEnrollmentIds.add(e.getEnrollmentId());
|
||||
|
||||
// 模拟每位学员已有 10 条打卡考勤与 2 门课程成绩
|
||||
enrollmentService.mockAttendanceCount.put(e.getEnrollmentId(), 10);
|
||||
enrollmentService.mockScoreCount.put(e.getEnrollmentId(), 2);
|
||||
}
|
||||
|
||||
// 另外创建 2 名非有效学员 (第 31 名请假 2,第 32 名中途放弃 3)
|
||||
TrainEnrollment leaveStudent = new TrainEnrollment();
|
||||
leaveStudent.setEnrollmentId(31L);
|
||||
leaveStudent.setPlanId(planId);
|
||||
leaveStudent.setStudentId("STU-31");
|
||||
leaveStudent.setEnrollmentStatus(2); // 请假
|
||||
enrollmentService.enrollmentStore.put(31L, leaveStudent);
|
||||
|
||||
TrainEnrollment abandonStudent = new TrainEnrollment();
|
||||
abandonStudent.setEnrollmentId(32L);
|
||||
abandonStudent.setPlanId(planId);
|
||||
abandonStudent.setStudentId("STU-32");
|
||||
abandonStudent.setEnrollmentStatus(3); // 放弃
|
||||
enrollmentService.enrollmentStore.put(32L, abandonStudent);
|
||||
|
||||
// WHEN: 执行计划结训 completePlan
|
||||
planAppService.completePlan(planId, "lead_admin");
|
||||
|
||||
// THEN:
|
||||
// 1. 计划自身状态更新为 2:已结束
|
||||
assertEquals(Integer.valueOf(2), planService.planStore.get(planId).getPlanStatus());
|
||||
|
||||
// 2. 30 名有效参训学员全部被更新为 4:已完成,并且写入了 complete_time
|
||||
for (Long eid : activeEnrollmentIds)
|
||||
{
|
||||
TrainEnrollment record = enrollmentService.enrollmentStore.get(eid);
|
||||
assertEquals("学员 ID=" + eid + " 参训状态必须置为已完成(4)",
|
||||
Integer.valueOf(4), record.getEnrollmentStatus());
|
||||
assertNotNull("学员 ID=" + eid + " 结业时间 complete_time 必须真实写入",
|
||||
record.getCompleteTime());
|
||||
assertEquals("lead_admin", record.getUpdateBy());
|
||||
|
||||
// 3. 历史数据完整保留,绝无物理或级联删除
|
||||
assertEquals(10, enrollmentService.countAttendanceByEnrollmentId(eid));
|
||||
assertEquals(2, enrollmentService.countScoreByEnrollmentId(eid));
|
||||
}
|
||||
|
||||
// 4. 已放弃学员不被误刷
|
||||
assertEquals(Integer.valueOf(3), enrollmentService.enrollmentStore.get(32L).getEnrollmentStatus());
|
||||
assertNull("已放弃学员不写入 complete_time", enrollmentService.enrollmentStore.get(32L).getCompleteTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransactionSemanticsPropagation() throws Exception
|
||||
{
|
||||
// 验证 Adapter 上具备 @Transactional(propagation = Propagation.MANDATORY)
|
||||
Transactional tx = realAdapter.getClass()
|
||||
.getMethod("completeEnrollments", String.class, String.class, Date.class)
|
||||
.getAnnotation(Transactional.class);
|
||||
assertNotNull("Adapter 必须具有事务注解", tx);
|
||||
assertEquals("Adapter 事务传播级别必须是 MANDATORY,强制要求在调用方本地事务内运行",
|
||||
Propagation.MANDATORY, tx.propagation());
|
||||
assertEquals("回滚异常类需为 Exception.class", Exception.class, tx.rollbackFor()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdapterExceptionRollbackPlanStatus()
|
||||
{
|
||||
// 模拟 Adapter 在更新时抛出底层数据库异常
|
||||
enrollmentService.simulateException = new IllegalStateException("数据库更新参训状态超时");
|
||||
|
||||
String planId = "PLAN-FAIL-TX";
|
||||
TrainClassPlan plan = new TrainClassPlan();
|
||||
plan.setPlanId(planId);
|
||||
plan.setPlanName("异常回滚班");
|
||||
plan.setPlanStatus(0);
|
||||
planService.planStore.put(planId, plan);
|
||||
|
||||
try
|
||||
{
|
||||
planAppService.completePlan(planId, "admin");
|
||||
fail("Adapter 异常必须向外层事务传播导致回滚");
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("数据库更新参训状态超时"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MemoryPlanService implements ITrainClassPlanService
|
||||
{
|
||||
private final Map<String, TrainClassPlan> planStore = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public TrainClassPlan selectTrainClassPlanByPlanId(String planId)
|
||||
{
|
||||
return planStore.get(planId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainClassPlan selectTrainClassPlanByPlanIdForUpdate(String planId)
|
||||
{
|
||||
return planStore.get(planId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PlanItemVO> selectTrainClassPlanList(PlanQuery query)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByPlanName(String planName, String excludePlanId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertTrainClassPlan(TrainClassPlan trainClassPlan)
|
||||
{
|
||||
planStore.put(trainClassPlan.getPlanId(), trainClassPlan);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainClassPlan(TrainClassPlan trainClassPlan)
|
||||
{
|
||||
planStore.put(trainClassPlan.getPlanId(), trainClassPlan);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainClassPlanStatus(TrainClassPlan trainClassPlan)
|
||||
{
|
||||
TrainClassPlan p = planStore.get(trainClassPlan.getPlanId());
|
||||
if (p != null)
|
||||
{
|
||||
p.setPlanStatus(trainClassPlan.getPlanStatus());
|
||||
p.setUpdateBy(trainClassPlan.getUpdateBy());
|
||||
p.setUpdateTime(trainClassPlan.getUpdateTime());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MemoryEnrollmentService implements ITrainEnrollmentService
|
||||
{
|
||||
private final Map<Long, TrainEnrollment> enrollmentStore = new HashMap<>();
|
||||
private final Map<Long, Integer> mockAttendanceCount = new HashMap<>();
|
||||
private final Map<Long, Integer> mockScoreCount = new HashMap<>();
|
||||
private RuntimeException simulateException = null;
|
||||
|
||||
@Override
|
||||
public TrainEnrollment selectTrainEnrollmentById(Long enrollmentId)
|
||||
{
|
||||
return enrollmentStore.get(enrollmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId)
|
||||
{
|
||||
for (TrainEnrollment e : enrollmentStore.values())
|
||||
{
|
||||
if (e.getPlanId().equals(planId) && e.getStudentId().equals(studentId))
|
||||
{
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EnrollmentItemVO> selectEnrollmentItemList(EnrollmentQuery query)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countEnrollmentsByPlanId(String planId)
|
||||
{
|
||||
int c = 0;
|
||||
for (TrainEnrollment e : enrollmentStore.values())
|
||||
{
|
||||
if (e.getPlanId().equals(planId))
|
||||
{
|
||||
c++;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertTrainEnrollment(TrainEnrollment trainEnrollment)
|
||||
{
|
||||
enrollmentStore.put(trainEnrollment.getEnrollmentId(), trainEnrollment);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchInsertTrainEnrollment(List<TrainEnrollment> list)
|
||||
{
|
||||
for (TrainEnrollment e : list)
|
||||
{
|
||||
enrollmentStore.put(e.getEnrollmentId(), e);
|
||||
}
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainEnrollment(TrainEnrollment trainEnrollment)
|
||||
{
|
||||
enrollmentStore.put(trainEnrollment.getEnrollmentId(), trainEnrollment);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateEnrollmentStatus(Long enrollmentId, Integer enrollmentStatus, String remark, String operator, Date updateTime)
|
||||
{
|
||||
TrainEnrollment e = enrollmentStore.get(enrollmentId);
|
||||
if (e != null)
|
||||
{
|
||||
e.setEnrollmentStatus(enrollmentStatus);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int completeActiveEnrollmentsByPlanId(String planId, String operator, Date completeTime)
|
||||
{
|
||||
if (simulateException != null)
|
||||
{
|
||||
throw simulateException;
|
||||
}
|
||||
int updated = 0;
|
||||
for (TrainEnrollment e : enrollmentStore.values())
|
||||
{
|
||||
if (e.getPlanId().equals(planId) && (e.getEnrollmentStatus() == 0 || e.getEnrollmentStatus() == 1))
|
||||
{
|
||||
e.setEnrollmentStatus(4);
|
||||
e.setCompleteTime(completeTime);
|
||||
e.setUpdateBy(operator);
|
||||
e.setUpdateTime(completeTime);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainEnrollmentById(Long enrollmentId)
|
||||
{
|
||||
return enrollmentStore.remove(enrollmentId) != null ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countAttendanceByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return mockAttendanceCount.getOrDefault(enrollmentId, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countScoreByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return mockScoreCount.getOrDefault(enrollmentId, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countReportByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countAccommodationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countStudyDurationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user