diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainTeacherController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainTeacherController.java
index 799586378..8dfaf5bb0 100644
--- a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainTeacherController.java
+++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainTeacherController.java
@@ -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();
diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainLessonMapper.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainLessonMapper.java
index 13686f494..902a819a0 100644
--- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainLessonMapper.java
+++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainLessonMapper.java
@@ -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);
}
diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainTeacherMapper.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainTeacherMapper.java
index a4618769c..a911fdfee 100644
--- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainTeacherMapper.java
+++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainTeacherMapper.java
@@ -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);
+
/**
* 批量删除教师信息
*
diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainTeacherServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainTeacherServiceImpl.java
index 5fbad20af..3430ed9e5 100644
--- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainTeacherServiceImpl.java
+++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainTeacherServiceImpl.java
@@ -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);
}
}
diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainLessonMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainLessonMapper.xml
index 526a2ad8a..9e9044250 100644
--- a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainLessonMapper.xml
+++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainLessonMapper.xml
@@ -390,4 +390,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select count(1) from train_lesson where class_room_id = #{classRoomId}
+
+
diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainTeacherMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainTeacherMapper.xml
index e04f7d89e..ba626c0e4 100644
--- a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainTeacherMapper.xml
+++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainTeacherMapper.xml
@@ -117,6 +117,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
update train_teacher set del_flag = '2' where t_id = #{tId}
+
+
+ delete from train_teacher where t_id = #{tId}
+
+
update train_teacher set del_flag = '2' where t_id in
diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/SchedulingAppServiceTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/SchedulingAppServiceTest.java
index 01f409dd9..4012bc450 100644
--- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/SchedulingAppServiceTest.java
+++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/SchedulingAppServiceTest.java
@@ -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;
+ }
}
/**
diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/TeacherZeroReferenceSafeDeleteTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/TeacherZeroReferenceSafeDeleteTest.java
new file mode 100644
index 000000000..9f0c3a81b
--- /dev/null
+++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/TeacherZeroReferenceSafeDeleteTest.java
@@ -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 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());
+ }
+ }
+}
diff --git a/build_ui/src/api/train/teacher.js b/build_ui/src/api/train/teacher.js
index 8ce6568f8..53442fce3 100644
--- a/build_ui/src/api/train/teacher.js
+++ b/build_ui/src/api/train/teacher.js
@@ -60,3 +60,11 @@ export function unbindTeacherUser(tId) {
method: 'delete'
})
}
+
+// 删除教师信息
+export function delTeacher(tId) {
+ return request({
+ url: '/train/teacher/' + tId,
+ method: 'delete'
+ })
+}
diff --git a/build_ui/src/views/train/teacher/index.vue b/build_ui/src/views/train/teacher/index.vue
index ea7f2280a..19a2395cc 100644
--- a/build_ui/src/views/train/teacher/index.vue
+++ b/build_ui/src/views/train/teacher/index.vue
@@ -107,7 +107,7 @@
-
+
解绑账号
+ 删除
@@ -257,7 +265,7 @@