新增学员:支持零业务引用档案安全删除与引用保护
This commit is contained in:
1 parent
2dc22b06f8
commit
225f58481a
13 files changed
+469
-5
No files matched your search
+9
@@ -6,6 +6,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
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.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -74,4 +75,12 @@ public class TrainStudentController extends BaseController
|
||||
{
|
||||
return success(trainStudentService.createStudent(dto));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('train:student:remove')")
|
||||
@Log(title = "学员主档", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{sId}")
|
||||
public AjaxResult remove(@PathVariable("sId") String sId)
|
||||
{
|
||||
return toAjax(trainStudentService.deleteTrainStudentBySId(sId));
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -23,16 +23,21 @@ public class StudentApiTest
|
||||
Method list = TrainStudentController.class.getMethod("list", StudentQuery.class);
|
||||
Method get = TrainStudentController.class.getMethod("getInfo", String.class);
|
||||
Method add = TrainStudentController.class.getMethod("add", StudentCreateDTO.class);
|
||||
Method remove = TrainStudentController.class.getMethod("remove", String.class);
|
||||
|
||||
assertEquals("/list", list.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("/{sId}", get.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertNotNull(add.getAnnotation(PostMapping.class));
|
||||
assertEquals("/{sId}", remove.getAnnotation(org.springframework.web.bind.annotation.DeleteMapping.class).value()[0]);
|
||||
|
||||
assertEquals("@ss.hasPermi('train:student:list')",
|
||||
list.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("@ss.hasPermi('train:student:query')",
|
||||
get.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("@ss.hasPermi('train:student:add')",
|
||||
add.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("@ss.hasPermi('train:student:remove')",
|
||||
remove.getAnnotation(PreAuthorize.class).value());
|
||||
assertFalse(add.getAnnotation(Log.class).isSaveRequestData());
|
||||
}
|
||||
|
||||
@@ -66,12 +71,16 @@ public class StudentApiTest
|
||||
endpointCount++;
|
||||
assertFalse("不得公开 PUT 接口",
|
||||
method.isAnnotationPresent(org.springframework.web.bind.annotation.PutMapping.class));
|
||||
assertFalse("不得公开 DELETE 接口",
|
||||
method.isAnnotationPresent(org.springframework.web.bind.annotation.DeleteMapping.class));
|
||||
assertFalse("不得公开 export 接口",
|
||||
method.getName().toLowerCase().contains("export"));
|
||||
if (method.isAnnotationPresent(org.springframework.web.bind.annotation.DeleteMapping.class))
|
||||
{
|
||||
assertEquals("remove", method.getName());
|
||||
assertEquals("@ss.hasPermi('train:student:remove')",
|
||||
method.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals("Controller必须严格只有3个P0端点", 3, endpointCount);
|
||||
assertEquals("Controller包含经过授权的4个端点(list, getInfo, add, remove)", 4, endpointCount);
|
||||
}
|
||||
}
|
||||
@@ -179,4 +179,20 @@ public interface TrainEnrollmentMapper
|
||||
* @return 参训记录列表
|
||||
*/
|
||||
List<EnrollmentItemVO> selectEnrollmentsByStudentId(@Param("studentId") String studentId);
|
||||
|
||||
/**
|
||||
* 统计指定学员的参训记录数 (train_enrollment)。
|
||||
*
|
||||
* @param studentId 学员ID
|
||||
* @return 参训记录数
|
||||
*/
|
||||
int countEnrollmentsByStudentId(@Param("studentId") String studentId);
|
||||
|
||||
/**
|
||||
* 统计指定学员下游业务事实总记录数(考勤签到、成绩、报到、住宿、自主学习等)。
|
||||
*
|
||||
* @param studentId 学员ID
|
||||
* @return 业务记录数
|
||||
*/
|
||||
int countActiveBusinessByStudentId(@Param("studentId") String studentId);
|
||||
}
|
||||
@@ -50,4 +50,12 @@ public interface TrainStudentMapper
|
||||
* @return 影响行数
|
||||
*/
|
||||
int unbindUserId(@org.apache.ibatis.annotations.Param("sId") String sId);
|
||||
|
||||
/**
|
||||
* 物理删除无业务关联且未绑定账号的学员档案
|
||||
*
|
||||
* @param sId 学员ID
|
||||
* @return 影响行数
|
||||
*/
|
||||
int deleteTrainStudentBySId(@org.apache.ibatis.annotations.Param("sId") String sId);
|
||||
}
|
||||
@@ -14,4 +14,12 @@ public interface ITrainStudentService
|
||||
StudentDetailVO selectStudentDetail(String sId);
|
||||
|
||||
String createStudent(StudentCreateDTO dto);
|
||||
|
||||
/**
|
||||
* 安全删除学员档案(ZERO-REFERENCE SAFE DELETE)
|
||||
*
|
||||
* @param sId 学员主档ID
|
||||
* @return 影响行数
|
||||
*/
|
||||
int deleteTrainStudentBySId(String sId);
|
||||
}
|
||||
+65
@@ -16,9 +16,11 @@ import com.ruoyi.train.domain.dto.StudentCreateDTO;
|
||||
import com.ruoyi.train.domain.query.StudentQuery;
|
||||
import com.ruoyi.train.domain.vo.StudentDetailVO;
|
||||
import com.ruoyi.train.domain.vo.StudentItemVO;
|
||||
import com.ruoyi.train.mapper.TrainEnrollmentMapper;
|
||||
import com.ruoyi.train.mapper.TrainStudentMapper;
|
||||
import com.ruoyi.train.security.StudentIdentityProtection;
|
||||
import com.ruoyi.train.service.ITrainStudentService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -33,6 +35,7 @@ public class TrainStudentServiceImpl implements ITrainStudentService
|
||||
|
||||
private final TrainStudentMapper trainStudentMapper;
|
||||
private final StudentIdentityProtection identityProtection;
|
||||
private TrainEnrollmentMapper trainEnrollmentMapper;
|
||||
|
||||
public TrainStudentServiceImpl(TrainStudentMapper trainStudentMapper,
|
||||
StudentIdentityProtection identityProtection)
|
||||
@@ -41,6 +44,16 @@ public class TrainStudentServiceImpl implements ITrainStudentService
|
||||
this.identityProtection = identityProtection;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public TrainStudentServiceImpl(TrainStudentMapper trainStudentMapper,
|
||||
StudentIdentityProtection identityProtection,
|
||||
@Autowired(required = false) TrainEnrollmentMapper trainEnrollmentMapper)
|
||||
{
|
||||
this.trainStudentMapper = trainStudentMapper;
|
||||
this.identityProtection = identityProtection;
|
||||
this.trainEnrollmentMapper = trainEnrollmentMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StudentItemVO> selectStudentList(StudentQuery query)
|
||||
{
|
||||
@@ -217,4 +230,56 @@ public class TrainStudentServiceImpl implements ITrainStudentService
|
||||
return new ServiceException(StudentErrorCode.STUDENT_IDCARD_DUPLICATED_MESSAGE,
|
||||
StudentErrorCode.STUDENT_IDCARD_DUPLICATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全删除学员主档(ZERO-REFERENCE SAFE DELETE)
|
||||
* 只有同时满足:
|
||||
* A. 无任何历史业务引用(参训记录、考勤、成绩、报到、住宿、自主学习等)
|
||||
* B. 无有效系统账号绑定
|
||||
* 才允许物理删除;否则强阻断并提示明确原因。
|
||||
*
|
||||
* @param sId 学员主档ID
|
||||
* @return 影响行数
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteTrainStudentBySId(String sId)
|
||||
{
|
||||
if (StringUtils.isEmpty(sId))
|
||||
{
|
||||
throw new ServiceException("学员ID不能为空");
|
||||
}
|
||||
// 1. 查验学员主档是否存在
|
||||
TrainStudent student = trainStudentMapper.selectTrainStudentBySId(sId);
|
||||
if (student == null)
|
||||
{
|
||||
throw new ServiceException("学员不存在", 40401);
|
||||
}
|
||||
|
||||
// 2. 检查是否已绑定系统账号
|
||||
if (student.getUserId() != null)
|
||||
{
|
||||
throw new ServiceException("该学员已绑定系统账号,请先解除账号绑定后再删除。");
|
||||
}
|
||||
|
||||
// 3. 检查下游具体业务记录(住宿、考勤、成绩、报到、自主学习等)
|
||||
if (trainEnrollmentMapper != null)
|
||||
{
|
||||
int businessCount = trainEnrollmentMapper.countActiveBusinessByStudentId(sId);
|
||||
if (businessCount > 0)
|
||||
{
|
||||
throw new ServiceException("该学员已产生住宿/考勤/成绩等业务数据,无法删除。");
|
||||
}
|
||||
|
||||
// 4. 检查是否已存在参训记录
|
||||
int enrollmentCount = trainEnrollmentMapper.countEnrollmentsByStudentId(sId);
|
||||
if (enrollmentCount > 0)
|
||||
{
|
||||
throw new ServiceException("该学员已存在参训记录,无法删除。");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 执行零引用物理删除
|
||||
return trainStudentMapper.deleteTrainStudentBySId(sId);
|
||||
}
|
||||
}
|
||||
@@ -201,4 +201,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by e.create_time desc
|
||||
</select>
|
||||
|
||||
<!-- 统计指定学员的参训记录数 -->
|
||||
<select id="countEnrollmentsByStudentId" parameterType="String" resultType="int">
|
||||
select count(1) from train_enrollment where student_id = #{studentId}
|
||||
</select>
|
||||
|
||||
<!-- 统计指定学员下游业务事实总记录数(考勤打卡、成绩、报到、住宿、门禁授权、自主学习) -->
|
||||
<select id="countActiveBusinessByStudentId" parameterType="String" resultType="int">
|
||||
select (
|
||||
(select count(1) from train_student_checkin c join train_enrollment e on c.enrollment_id = e.enrollment_id where e.student_id = #{studentId}) +
|
||||
(select count(1) from train_student_score s join train_enrollment e on s.enrollment_id = e.enrollment_id where e.student_id = #{studentId}) +
|
||||
(select count(1) from train_class_report r join train_enrollment e on r.enrollment_id = e.enrollment_id where e.student_id = #{studentId}) +
|
||||
(select count(1) from train_accommodation a join train_enrollment e on a.enrollment_id = e.enrollment_id where e.student_id = #{studentId}) +
|
||||
(select count(1) from train_dorm_auth auth join train_accommodation a on auth.accommodation_id = a.accommodation_id join train_enrollment e on a.enrollment_id = e.enrollment_id where e.student_id = #{studentId}) +
|
||||
(select count(1) from train_study_duration d join train_enrollment e on d.enrollment_id = e.enrollment_id where e.student_id = #{studentId})
|
||||
)
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -106,4 +106,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<update id="unbindUserId">
|
||||
update train_student set user_id = null, update_time = CURRENT_TIMESTAMP where s_id = #{sId}
|
||||
</update>
|
||||
|
||||
<!-- 物理删除无业务关联且未绑定账号的学员档案 -->
|
||||
<delete id="deleteTrainStudentBySId" parameterType="String">
|
||||
delete from train_student where s_id = #{sId}
|
||||
</delete>
|
||||
</mapper>
|
||||
+18
@@ -1001,6 +1001,12 @@ public class EnrollmentAppServiceImplTest
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainStudentBySId(String sId)
|
||||
{
|
||||
return studentStore.remove(sId) != null ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MockTrainEnrollmentMapper implements TrainEnrollmentMapper
|
||||
@@ -1138,5 +1144,17 @@ public class EnrollmentAppServiceImplTest
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countEnrollmentsByStudentId(String studentId)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countActiveBusinessByStudentId(String studentId)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
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 static org.junit.Assert.fail;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.train.domain.TrainStudent;
|
||||
import com.ruoyi.train.mapper.TrainEnrollmentMapper;
|
||||
import com.ruoyi.train.mapper.TrainStudentMapper;
|
||||
import com.ruoyi.train.security.JceStudentIdentityProtection;
|
||||
import com.ruoyi.train.security.StudentIdentityProtection;
|
||||
|
||||
/**
|
||||
* 学员零引用安全删除测试与契约防护 (StudentZeroReferenceSafeDeleteTest)
|
||||
*
|
||||
* 验证规则:
|
||||
* 1. 零引用 + 无账号绑定 -> 删除成功
|
||||
* 2. 有账号绑定 -> 强阻断,提示解绑,严禁级联删除 sys_user
|
||||
* 3. 有参训记录 (Enrollment) -> 强阻断
|
||||
* 4. 有考勤/报到等业务记录 -> 强阻断
|
||||
* 5. 有住宿/门禁授权记录 -> 强阻断
|
||||
* 6. 有成绩/自主学习记录 -> 强阻断
|
||||
* 7. 契约保证具备 @Transactional 注解,未注入 sys_user 删除依赖,零孤儿风险
|
||||
*/
|
||||
public class StudentZeroReferenceSafeDeleteTest
|
||||
{
|
||||
private StudentIdentityProtection protection;
|
||||
private Map<String, TrainStudent> studentStore;
|
||||
private int mockEnrollmentCount;
|
||||
private int mockActiveBusinessCount;
|
||||
private int deletedStudentCount;
|
||||
private TrainStudentServiceImpl studentService;
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
byte[] encKey = new byte[32];
|
||||
byte[] hashKey = new byte[32];
|
||||
Arrays.fill(encKey, (byte) 0x33);
|
||||
Arrays.fill(hashKey, (byte) 0x77);
|
||||
protection = new JceStudentIdentityProtection(
|
||||
Base64.getEncoder().encodeToString(encKey),
|
||||
Base64.getEncoder().encodeToString(hashKey)
|
||||
);
|
||||
|
||||
studentStore = new HashMap<>();
|
||||
mockEnrollmentCount = 0;
|
||||
mockActiveBusinessCount = 0;
|
||||
deletedStudentCount = 0;
|
||||
|
||||
TrainStudentMapper studentMapperProxy = (TrainStudentMapper) Proxy.newProxyInstance(
|
||||
TrainStudentMapper.class.getClassLoader(),
|
||||
new Class<?>[]{ TrainStudentMapper.class },
|
||||
(proxy, method, args) -> {
|
||||
String methodName = method.getName();
|
||||
if ("selectTrainStudentBySId".equals(methodName))
|
||||
{
|
||||
return studentStore.get((String) args[0]);
|
||||
}
|
||||
else if ("deleteTrainStudentBySId".equals(methodName))
|
||||
{
|
||||
String sId = (String) args[0];
|
||||
if (studentStore.remove(sId) != null)
|
||||
{
|
||||
deletedStudentCount++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
TrainEnrollmentMapper enrollmentMapperProxy = (TrainEnrollmentMapper) Proxy.newProxyInstance(
|
||||
TrainEnrollmentMapper.class.getClassLoader(),
|
||||
new Class<?>[]{ TrainEnrollmentMapper.class },
|
||||
(proxy, method, args) -> {
|
||||
String methodName = method.getName();
|
||||
if ("countEnrollmentsByStudentId".equals(methodName))
|
||||
{
|
||||
return mockEnrollmentCount;
|
||||
}
|
||||
else if ("countActiveBusinessByStudentId".equals(methodName))
|
||||
{
|
||||
return mockActiveBusinessCount;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
studentService = new TrainStudentServiceImpl(studentMapperProxy, protection, enrollmentMapperProxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStudentZeroReferenceSuccess()
|
||||
{
|
||||
TrainStudent student = new TrainStudent();
|
||||
student.setsId("S_MOCK_001");
|
||||
student.setsName("小明");
|
||||
student.setUserId(null);
|
||||
studentStore.put("S_MOCK_001", student);
|
||||
mockEnrollmentCount = 0;
|
||||
mockActiveBusinessCount = 0;
|
||||
|
||||
int rows = studentService.deleteTrainStudentBySId("S_MOCK_001");
|
||||
assertEquals("零引用学员应删除成功", 1, rows);
|
||||
assertEquals(1, deletedStudentCount);
|
||||
assertFalse("学员主档应已从存储中物理移除", studentStore.containsKey("S_MOCK_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStudentWithAccountBindingBlocked()
|
||||
{
|
||||
TrainStudent student = new TrainStudent();
|
||||
student.setsId("S_MOCK_002");
|
||||
student.setsName("小红");
|
||||
student.setUserId(2001L);
|
||||
studentStore.put("S_MOCK_002", student);
|
||||
mockEnrollmentCount = 0;
|
||||
mockActiveBusinessCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
studentService.deleteTrainStudentBySId("S_MOCK_002");
|
||||
fail("已绑定系统账号的学员应禁止删除");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue("提示文案应明确指出解绑要求", e.getMessage().contains("该学员已绑定系统账号,请先解除账号绑定后再删除。"));
|
||||
assertEquals(0, deletedStudentCount);
|
||||
assertTrue("学员主档不得被物理删除", studentStore.containsKey("S_MOCK_002"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStudentWithEnrollmentBlocked()
|
||||
{
|
||||
TrainStudent student = new TrainStudent();
|
||||
student.setsId("S_MOCK_003");
|
||||
student.setsName("小华");
|
||||
student.setUserId(null);
|
||||
studentStore.put("S_MOCK_003", student);
|
||||
mockEnrollmentCount = 1;
|
||||
mockActiveBusinessCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
studentService.deleteTrainStudentBySId("S_MOCK_003");
|
||||
fail("存在参训记录的学员应禁止删除");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue("提示文案应明确指出参训记录阻断", e.getMessage().contains("该学员已存在参训记录,无法删除。"));
|
||||
assertEquals(0, deletedStudentCount);
|
||||
assertTrue("学员主档不得被物理删除", studentStore.containsKey("S_MOCK_003"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStudentWithActiveBusinessBlocked_AttendanceOrReport()
|
||||
{
|
||||
TrainStudent student = new TrainStudent();
|
||||
student.setsId("S_MOCK_004");
|
||||
student.setsName("小强");
|
||||
student.setUserId(null);
|
||||
studentStore.put("S_MOCK_004", student);
|
||||
mockEnrollmentCount = 1;
|
||||
mockActiveBusinessCount = 2;
|
||||
|
||||
try
|
||||
{
|
||||
studentService.deleteTrainStudentBySId("S_MOCK_004");
|
||||
fail("存在考勤打卡或报到的学员应禁止删除");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue("提示文案应指出业务数据阻断", e.getMessage().contains("该学员已产生住宿/考勤/成绩等业务数据,无法删除。"));
|
||||
assertEquals(0, deletedStudentCount);
|
||||
assertTrue("学员主档不得被物理删除", studentStore.containsKey("S_MOCK_004"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStudentWithActiveBusinessBlocked_AccommodationOrDoorAuth()
|
||||
{
|
||||
TrainStudent student = new TrainStudent();
|
||||
student.setsId("S_MOCK_005");
|
||||
student.setsName("小刚");
|
||||
student.setUserId(null);
|
||||
studentStore.put("S_MOCK_005", student);
|
||||
mockEnrollmentCount = 1;
|
||||
mockActiveBusinessCount = 1;
|
||||
|
||||
try
|
||||
{
|
||||
studentService.deleteTrainStudentBySId("S_MOCK_005");
|
||||
fail("存在住宿或门禁授权的学员应禁止删除");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue("提示文案应指出业务数据阻断", e.getMessage().contains("该学员已产生住宿/考勤/成绩等业务数据,无法删除。"));
|
||||
assertEquals(0, deletedStudentCount);
|
||||
assertTrue("学员主档不得被物理删除", studentStore.containsKey("S_MOCK_005"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStudentWithActiveBusinessBlocked_ScoreOrStudyDuration()
|
||||
{
|
||||
TrainStudent student = new TrainStudent();
|
||||
student.setsId("S_MOCK_006");
|
||||
student.setsName("小芳");
|
||||
student.setUserId(null);
|
||||
studentStore.put("S_MOCK_006", student);
|
||||
mockEnrollmentCount = 1;
|
||||
mockActiveBusinessCount = 1;
|
||||
|
||||
try
|
||||
{
|
||||
studentService.deleteTrainStudentBySId("S_MOCK_006");
|
||||
fail("存在成绩或自主学时记录的学员应禁止删除");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue("提示文案应指出业务数据阻断", e.getMessage().contains("该学员已产生住宿/考勤/成绩等业务数据,无法删除。"));
|
||||
assertEquals(0, deletedStudentCount);
|
||||
assertTrue("学员主档不得被物理删除", studentStore.containsKey("S_MOCK_006"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContractNeverDeletesSysUser()
|
||||
{
|
||||
for (Field f : TrainStudentServiceImpl.class.getDeclaredFields())
|
||||
{
|
||||
assertFalse("TrainStudentServiceImpl不得注入系统用户删除组件: " + f.getName(),
|
||||
f.getType().getSimpleName().contains("SysUser"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContractTransactionalAnnotationsPresent() throws Exception
|
||||
{
|
||||
Method studentDelMethod = TrainStudentServiceImpl.class.getMethod("deleteTrainStudentBySId", String.class);
|
||||
Transactional studentTx = studentDelMethod.getAnnotation(Transactional.class);
|
||||
assertNotNull("学员安全删除方法必须具备 @Transactional 注解", studentTx);
|
||||
assertEquals(Exception.class, studentTx.rollbackFor()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContractZeroOrphanRisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
studentService.deleteTrainStudentBySId("S_NON_EXISTING");
|
||||
fail("不存在的学员应返回404");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals(Integer.valueOf(40401), e.getCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -330,5 +330,11 @@ public class TrainStudentServiceImplTest
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainStudentBySId(String sId)
|
||||
{
|
||||
return store.remove(sId) != null ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,3 +51,11 @@ export function unbindStudentUser(sId) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除学员主档
|
||||
export function delStudent(sId) {
|
||||
return request({
|
||||
url: '/train/student/' + sId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -98,7 +98,7 @@
|
||||
<el-tag v-else type="info" size="mini">未关联</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="210">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:student:query']"
|
||||
@@ -124,6 +124,14 @@
|
||||
style="color: #F56C6C;"
|
||||
@click="handleUnbind(scope.row)"
|
||||
>解绑账号</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:student:remove']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
style="color: #F56C6C;"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -267,7 +275,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addStudent, getStudent, listStudent, getStudentBindableUsers, bindStudentUser, unbindStudentUser } from '@/api/train/student'
|
||||
import { addStudent, getStudent, listStudent, delStudent, getStudentBindableUsers, bindStudentUser, unbindStudentUser } from '@/api/train/student'
|
||||
|
||||
export default {
|
||||
name: 'Student',
|
||||
@@ -440,6 +448,17 @@ export default {
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const sId = row.sId;
|
||||
const sName = row.sName;
|
||||
this.$modal.confirm(`确定删除学员【${sName} / ${sId}】吗?`).then(() => {
|
||||
return delStudent(sId);
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
},
|
||||
sourceTypeLabel(sourceType) {
|
||||
const labels = {
|
||||
1: '管理员录入',
|
||||
|
||||
Reference in new issue
Block a user