新增教师:支持零业务引用档案安全删除与引用保护
This commit is contained in:
1 parent
740c59d5dd
commit
2dc22b06f8
10 files changed
+353
-7
No files matched your search
+12
@@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -127,6 +128,17 @@ public class TrainTeacherController extends BaseController
|
||||
return toAjax(trainTeacherService.updateTrainTeacher(trainTeacher));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除教师信息 (DELETE /train/teacher/{tId})
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:teacher:remove')")
|
||||
@Log(title = "教师管理", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{tId}")
|
||||
public AjaxResult remove(@PathVariable("tId") String tId)
|
||||
{
|
||||
return toAjax(trainTeacherService.deleteTrainTeacherByTId(tId));
|
||||
}
|
||||
|
||||
private boolean isValidPhone(String phone)
|
||||
{
|
||||
return PHONE_PATTERN.matcher(phone).matches();
|
||||
|
||||
@@ -150,4 +150,12 @@ public interface TrainLessonMapper
|
||||
* @return 课次引用数
|
||||
*/
|
||||
int countLessonsByClassRoomId(@Param("classRoomId") String classRoomId);
|
||||
|
||||
/**
|
||||
* 统计指定教师在排课课次中的引用数(train_lesson)。
|
||||
*
|
||||
* @param tId 教师工号
|
||||
* @return 课次引用数
|
||||
*/
|
||||
int countLessonsByTeacherId(@Param("tId") String tId);
|
||||
}
|
||||
@@ -52,13 +52,21 @@ public interface TrainTeacherMapper
|
||||
public TrainTeacher checkPhoneUnique(String tPhone);
|
||||
|
||||
/**
|
||||
* 删除教师信息
|
||||
* 删除教师信息 (逻辑删除)
|
||||
*
|
||||
* @param tId 教师信息主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainTeacherByTId(String tId);
|
||||
|
||||
/**
|
||||
* 物理删除无业务关联且未绑定账号的教师档案
|
||||
*
|
||||
* @param tId 教师信息主键
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int deleteTrainTeacherRealByTId(String tId);
|
||||
|
||||
/**
|
||||
* 批量删除教师信息
|
||||
*
|
||||
|
||||
+58
-4
@@ -8,6 +8,8 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.train.mapper.TrainLessonMapper;
|
||||
import com.ruoyi.train.mapper.TrainTeacherMapper;
|
||||
import com.ruoyi.train.domain.TrainTeacher;
|
||||
import com.ruoyi.train.service.ITrainTeacherService;
|
||||
@@ -24,6 +26,19 @@ public class TrainTeacherServiceImpl implements ITrainTeacherService
|
||||
@Autowired
|
||||
private TrainTeacherMapper trainTeacherMapper;
|
||||
|
||||
@Autowired
|
||||
private TrainLessonMapper trainLessonMapper;
|
||||
|
||||
public TrainTeacherServiceImpl()
|
||||
{
|
||||
}
|
||||
|
||||
public TrainTeacherServiceImpl(TrainTeacherMapper trainTeacherMapper, TrainLessonMapper trainLessonMapper)
|
||||
{
|
||||
this.trainTeacherMapper = trainTeacherMapper;
|
||||
this.trainLessonMapper = trainLessonMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询教师信息
|
||||
*
|
||||
@@ -165,26 +180,65 @@ public class TrainTeacherServiceImpl implements ITrainTeacherService
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除教师信息(逻辑删除)
|
||||
* 批量安全删除教师信息
|
||||
*
|
||||
* @param tIds 需要删除的教师信息主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteTrainTeacherByTIds(String[] tIds)
|
||||
{
|
||||
return trainTeacherMapper.deleteTrainTeacherByTIds(tIds);
|
||||
if (tIds == null || tIds.length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
for (String tId : tIds)
|
||||
{
|
||||
deleteTrainTeacherByTId(tId);
|
||||
}
|
||||
return tIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除教师信息(逻辑删除)
|
||||
* 安全删除教师档案(ZERO-REFERENCE SAFE DELETE)
|
||||
* 只有同时满足:
|
||||
* A. 无任何业务引用(排课/授课课次)
|
||||
* B. 无有效系统账号绑定
|
||||
* 才允许物理删除;否则强阻断并提示明确原因。
|
||||
*
|
||||
* @param tId 教师信息主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteTrainTeacherByTId(String tId)
|
||||
{
|
||||
return trainTeacherMapper.deleteTrainTeacherByTId(tId);
|
||||
if (StringUtils.isEmpty(tId))
|
||||
{
|
||||
throw new ServiceException("教师工号不能为空");
|
||||
}
|
||||
// 1. 查验教师档案是否存在
|
||||
TrainTeacher teacher = trainTeacherMapper.selectTrainTeacherByTId(tId);
|
||||
if (teacher == null)
|
||||
{
|
||||
throw new ServiceException("教师不存在", 40401);
|
||||
}
|
||||
|
||||
// 2. 检查是否已绑定系统账号
|
||||
if (teacher.getUserId() != null)
|
||||
{
|
||||
throw new ServiceException("该教师已绑定系统账号,请先解除账号绑定后再删除。");
|
||||
}
|
||||
|
||||
// 3. 检查是否存在排课/授课业务记录
|
||||
int lessonCount = trainLessonMapper.countLessonsByTeacherId(tId);
|
||||
if (lessonCount > 0)
|
||||
{
|
||||
throw new ServiceException("该教师已存在授课或排课记录,无法删除,可将教师状态调整为停用。");
|
||||
}
|
||||
|
||||
// 4. 执行零引用物理删除
|
||||
return trainTeacherMapper.deleteTrainTeacherRealByTId(tId);
|
||||
}
|
||||
}
|
||||
@@ -390,4 +390,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
select count(1) from train_lesson where class_room_id = #{classRoomId}
|
||||
</select>
|
||||
|
||||
<select id="countLessonsByTeacherId" parameterType="String" resultType="int">
|
||||
select count(1) from train_lesson where t_id = #{tId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -117,6 +117,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
update train_teacher set del_flag = '2' where t_id = #{tId}
|
||||
</update>
|
||||
|
||||
<!-- 物理删除:彻底清除无业务引用且未绑定账号的错误录入档案 -->
|
||||
<delete id="deleteTrainTeacherRealByTId" parameterType="String">
|
||||
delete from train_teacher where t_id = #{tId}
|
||||
</delete>
|
||||
|
||||
<!-- 批量逻辑删除:将 del_flag 置为 '2' -->
|
||||
<update id="deleteTrainTeacherByTIds" parameterType="String">
|
||||
update train_teacher set del_flag = '2' where t_id in
|
||||
|
||||
+14
@@ -1073,6 +1073,20 @@ public class SchedulingAppServiceTest
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countLessonsByTeacherId(String tId)
|
||||
{
|
||||
int count = 0;
|
||||
for (TrainLesson l : lessons.values())
|
||||
{
|
||||
if (tId != null && tId.equals(l.getTId()))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
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.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.TrainTeacher;
|
||||
import com.ruoyi.train.mapper.TrainLessonMapper;
|
||||
import com.ruoyi.train.mapper.TrainTeacherMapper;
|
||||
|
||||
/**
|
||||
* 教师零引用安全删除测试与契约防护 (TeacherZeroReferenceSafeDeleteTest)
|
||||
*
|
||||
* 验证规则:
|
||||
* 1. 零引用 + 无账号绑定 -> 删除成功
|
||||
* 2. 有账号绑定 -> 强阻断,提示解绑,严禁级联删除 sys_user
|
||||
* 3. 有排课/授课引用 -> 强阻断,引导调整为停用状态
|
||||
* 4. 历史教师 -> 不可删除,支持 status=0 停用
|
||||
* 5. 契约保证具备 @Transactional 注解,未注入 sys_user 删除依赖
|
||||
*/
|
||||
public class TeacherZeroReferenceSafeDeleteTest
|
||||
{
|
||||
private Map<String, TrainTeacher> teacherStore;
|
||||
private int mockLessonCount;
|
||||
private int deletedTeacherCount;
|
||||
private TrainTeacherServiceImpl teacherService;
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
teacherStore = new HashMap<>();
|
||||
mockLessonCount = 0;
|
||||
deletedTeacherCount = 0;
|
||||
|
||||
TrainTeacherMapper teacherMapperProxy = (TrainTeacherMapper) Proxy.newProxyInstance(
|
||||
TrainTeacherMapper.class.getClassLoader(),
|
||||
new Class<?>[]{ TrainTeacherMapper.class },
|
||||
(proxy, method, args) -> {
|
||||
String methodName = method.getName();
|
||||
if ("selectTrainTeacherByTId".equals(methodName))
|
||||
{
|
||||
return teacherStore.get((String) args[0]);
|
||||
}
|
||||
else if ("deleteTrainTeacherRealByTId".equals(methodName))
|
||||
{
|
||||
String tId = (String) args[0];
|
||||
if (teacherStore.remove(tId) != null)
|
||||
{
|
||||
deletedTeacherCount++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
else if ("updateTrainTeacher".equals(methodName))
|
||||
{
|
||||
TrainTeacher t = (TrainTeacher) args[0];
|
||||
if (teacherStore.containsKey(t.gettId()))
|
||||
{
|
||||
teacherStore.put(t.gettId(), t);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
TrainLessonMapper lessonMapperProxy = (TrainLessonMapper) Proxy.newProxyInstance(
|
||||
TrainLessonMapper.class.getClassLoader(),
|
||||
new Class<?>[]{ TrainLessonMapper.class },
|
||||
(proxy, method, args) -> {
|
||||
if ("countLessonsByTeacherId".equals(method.getName()))
|
||||
{
|
||||
return mockLessonCount;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
teacherService = new TrainTeacherServiceImpl(teacherMapperProxy, lessonMapperProxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTeacherZeroReferenceSuccess()
|
||||
{
|
||||
TrainTeacher teacher = new TrainTeacher();
|
||||
teacher.settId("T_MOCK_001");
|
||||
teacher.settName("张老师");
|
||||
teacher.setUserId(null);
|
||||
teacherStore.put("T_MOCK_001", teacher);
|
||||
mockLessonCount = 0;
|
||||
|
||||
int rows = teacherService.deleteTrainTeacherByTId("T_MOCK_001");
|
||||
assertEquals("零引用教师应删除成功", 1, rows);
|
||||
assertEquals(1, deletedTeacherCount);
|
||||
assertFalse("教师档案应已从存储中物理移除", teacherStore.containsKey("T_MOCK_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTeacherWithAccountBindingBlocked()
|
||||
{
|
||||
TrainTeacher teacher = new TrainTeacher();
|
||||
teacher.settId("T_MOCK_002");
|
||||
teacher.settName("李老师");
|
||||
teacher.setUserId(1001L);
|
||||
teacherStore.put("T_MOCK_002", teacher);
|
||||
mockLessonCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
teacherService.deleteTrainTeacherByTId("T_MOCK_002");
|
||||
fail("已绑定系统账号的教师应禁止删除");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue("提示文案应明确指出解绑要求", e.getMessage().contains("该教师已绑定系统账号,请先解除账号绑定后再删除。"));
|
||||
assertEquals(0, deletedTeacherCount);
|
||||
assertTrue("教师主档不得被物理删除", teacherStore.containsKey("T_MOCK_002"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTeacherWithLessonReferenceBlocked()
|
||||
{
|
||||
TrainTeacher teacher = new TrainTeacher();
|
||||
teacher.settId("T_MOCK_003");
|
||||
teacher.settName("王老师");
|
||||
teacher.setUserId(null);
|
||||
teacherStore.put("T_MOCK_003", teacher);
|
||||
mockLessonCount = 3;
|
||||
|
||||
try
|
||||
{
|
||||
teacherService.deleteTrainTeacherByTId("T_MOCK_003");
|
||||
fail("有排课或授课记录的教师应禁止删除");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue("提示文案应引导调整为停用", e.getMessage().contains("该教师已存在授课或排课记录,无法删除,可将教师状态调整为停用。"));
|
||||
assertEquals(0, deletedTeacherCount);
|
||||
assertTrue("教师主档不得被物理删除", teacherStore.containsKey("T_MOCK_003"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTeacherHistoricalStatusDeactivateSupported()
|
||||
{
|
||||
TrainTeacher teacher = new TrainTeacher();
|
||||
teacher.settId("T_MOCK_004");
|
||||
teacher.settName("赵老师");
|
||||
teacher.setStatus(1);
|
||||
teacherStore.put("T_MOCK_004", teacher);
|
||||
mockLessonCount = 10;
|
||||
|
||||
try
|
||||
{
|
||||
teacherService.deleteTrainTeacherByTId("T_MOCK_004");
|
||||
fail("历史教师应禁止物理删除");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("可将教师状态调整为停用"));
|
||||
}
|
||||
|
||||
teacher.setStatus(0);
|
||||
int updateRows = teacherService.updateTrainTeacher(teacher);
|
||||
assertEquals("历史教师应可调整状态为停用", 1, updateRows);
|
||||
assertEquals(Integer.valueOf(0), teacherStore.get("T_MOCK_004").getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContractNeverDeletesSysUser()
|
||||
{
|
||||
for (Field f : TrainTeacherServiceImpl.class.getDeclaredFields())
|
||||
{
|
||||
assertFalse("TrainTeacherServiceImpl不得注入系统用户删除组件: " + f.getName(),
|
||||
f.getType().getSimpleName().contains("SysUser"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContractTransactionalAnnotationsPresent() throws Exception
|
||||
{
|
||||
Method teacherDelMethod = TrainTeacherServiceImpl.class.getMethod("deleteTrainTeacherByTId", String.class);
|
||||
Transactional teacherTx = teacherDelMethod.getAnnotation(Transactional.class);
|
||||
assertNotNull("教师安全删除方法必须具备 @Transactional 注解", teacherTx);
|
||||
assertEquals(Exception.class, teacherTx.rollbackFor()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContractZeroOrphanRisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
teacherService.deleteTrainTeacherByTId("T_NON_EXISTING");
|
||||
fail("不存在的教师应返回404");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals(Integer.valueOf(40401), e.getCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,3 +60,11 @@ export function unbindTeacherUser(tId) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除教师信息
|
||||
export function delTeacher(tId) {
|
||||
return request({
|
||||
url: '/train/teacher/' + tId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -107,7 +107,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<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:teacher:edit']"
|
||||
@@ -133,6 +133,14 @@
|
||||
style="color: #F56C6C;"
|
||||
@click="handleUnbind(scope.row)"
|
||||
>解绑账号</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:teacher:remove']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
style="color: #F56C6C;"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -257,7 +265,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listTeacher, getTeacher, addTeacher, updateTeacher, getTeacherBindableUsers, bindTeacherUser, unbindTeacherUser } from '@/api/train/teacher'
|
||||
import { listTeacher, getTeacher, delTeacher, addTeacher, updateTeacher, getTeacherBindableUsers, bindTeacherUser, unbindTeacherUser } from '@/api/train/teacher'
|
||||
|
||||
export default {
|
||||
name: 'Teacher',
|
||||
@@ -453,6 +461,17 @@ export default {
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const tId = row.tId;
|
||||
const tName = row.tName;
|
||||
this.$modal.confirm(`确定删除教师【${tName}】吗?`).then(() => {
|
||||
return delTeacher(tId);
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
},
|
||||
formatGender(gender) {
|
||||
if (gender === '0' || gender === 0 || gender === '男') return '男'
|
||||
if (gender === '1' || gender === 1 || gender === '女') return '女'
|
||||
|
||||
Reference in new issue
Block a user