---
req_id: REQ-MOD-003
date: 2026-04-29
spec_ref: docs/superpowers/specs/2026-04-29-REQ-MOD-003.md
---

# REQ-MOD-003 模块删除 Implementation Plan

> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task.

**Goal:** 在 MOD-001/002 已建工程基础上增量实现 `DELETE /api/mod/modules/{id}` 软删除接口，含目标存在性、子模块拦截两类校验，软删除后 `bDeleted=1` + 审计字段。

**Architecture:** 复用现有 `ModuleService` / `ModuleServiceImpl` / `ModuleController` / `ModuleMapper`；新增 `mapper.hasActiveChildren(id)` + `service.delete(id)` + controller `@DeleteMapping`。SecurityConfig 已对 `/api/mod/**` permitAll，无需改。`sDeletedBy` 取 JWT principal 或回退 stub（与 MOD-001 `sCreatedBy` 同策略）。**40902 外部引用拦截不实现**——docs/03 当前 schema 中 tModule 无引用方表。

**Tech Stack:** Spring Boot 3.3.5 / MyBatis-Plus / JUnit 5 + Mockito + TestRestTemplate（沿用）。

---

## Schema 改动

无（仅 UPDATE 软删除字段）。

## 文件变更清单

### 修改

- `backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java` — 追加 `findActiveChildFlag` + default `hasActiveChildren`
- `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` — 追加 `void delete(Integer id)` 方法
- `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` — 实现 `delete(...)`
- `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` — 追加 `@DeleteMapping("/modules/{id}")` 端点
- `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java` — 追加 5 用例
- `backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java` — 追加 1 用例
- `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` — 追加 6 用例

## 任务步骤

> 全局：每 commit `<type>(mod): <subject> REQ-MOD-003`；测试派发子会话；现有 41 用例全程绿。

### Task 1: Mapper#hasActiveChildren + IT

**Files:**
- Modify: `backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java`
- Modify: `backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java`

**API shape:**
- `@Select("SELECT 1 FROM tModule WHERE iParentId = #{parentId} AND bDeleted = 0 LIMIT 1")` `Integer findActiveChildFlag(@Param("parentId") Integer parentId)`
- `default boolean hasActiveChildren(Integer parentId) { return findActiveChildFlag(parentId) != null; }`

- [ ] **Step 1: 写失败测试 `ModuleMapperIT#hasActiveChildren_trueIfChildAliveExists_falseOtherwise`**
  - 准备：root（无 parent）；child1（parent=root, bDeleted=0）；child2（parent=root, bDeleted=1）
  - 断言：`hasActiveChildren(root.id) == true`
  - 用 JdbcTemplate `UPDATE tModule SET bDeleted=1 WHERE iIncrement=child1.id` 软删唯一活跃子节点
  - 再次断言：`hasActiveChildren(root.id) == false`
  - `hasActiveChildren(99999997) == false`

- [ ] **Step 2: 实现 mapper 方法**

- [ ] **Step 3: 子会话验证 PASS**
  - 命令：`cd backend && mvn -B test -Dtest=ModuleMapperIT`

- [ ] **Step 4: Commit**
  - `git commit -m "feat(mod): mapper#hasActiveChildren for delete check REQ-MOD-003"`

### Task 2: Service#delete + 单测

**Files:**
- Modify: `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java`
- Modify: `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java`
- Modify: `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java`

**API shape:**
- `ModuleService#delete(Integer id) : void`
- `ModuleServiceImpl#delete(Integer id)`：
  1. `Module original = moduleMapper.selectById(id)` → null 或 `bDeleted=true` → `BizException(40400, "模块不存在或已删除")`
  2. `moduleMapper.hasActiveChildren(id)` → true → `BizException(40901, "模块仍有未删除子节点")`
  3. 构造 `Module entity`：`setIIncrement(id)` / `setBDeleted(true)` / `setTDeletedDate(LocalDateTime.now())` / `setSDeletedBy(SecurityContextHelper.currentUserNo() ?: stub.getStubUserNo())`；其他字段 null
  4. `moduleMapper.updateById(entity)`

- [ ] **Step 1: 写失败测试（5 用例）**
  - `deleteWithValidId_softDeletes_andSetsAuditFields`：mock `selectById(10)=alive`、`hasActiveChildren(10)=false`、`updateById(any)=1`；ArgumentCaptor 抓 entity；断言 `iIncrement=10` / `bDeleted=true` / `tDeletedDate != null` / `sDeletedBy="STUB_ADMIN"` / 其他字段 null
  - `deleteWithTargetNotFound_throws40400`：`selectById(99)=null`；`updateById` 永不调用
  - `deleteWithTargetAlreadyDeleted_throws40400`：`selectById(10)` 返回 `bDeleted=true` 的 Module
  - `deleteWithActiveChildren_throws40901`：`hasActiveChildren(10)=true`
  - `deleteSetsDeletedByFromAuthenticatedUser`：SecurityContextHolder 注入 principal "BOB"；ArgumentCaptor `sDeletedBy="BOB"`
  - 子会话先跑 → 5 用例 FAIL

- [ ] **Step 2: 实现 service**
  - 严格按 API shape 顺序

- [ ] **Step 3: 子会话验证 PASS**
  - 命令：`cd backend && mvn -B test -Dtest=ModuleServiceImplTest`
  - 期望：13 (前) + 5 = 18 用例全绿

- [ ] **Step 4: Commit**
  - `git commit -m "feat(mod): module delete service + soft delete REQ-MOD-003"`

### Task 3: Controller DELETE + 6 IT 用例

**Files:**
- Modify: `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java`
- Modify: `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java`

**API shape:**
- `@DeleteMapping("/modules/{id}") public Result<Void> delete(@PathVariable Integer id)`
- 调 `moduleService.delete(id)`；返回 `Result.ok()`

- [ ] **Step 1: 写失败测试（6 用例）**
  - `deleteValidId_with_jwt_returns200_andSoftDeletes`：JdbcTemplate 直插 alive 行；DELETE 带 JWT="ADMIN001"；期望 `code=0` / `data=null`；JdbcTemplate 查 `bDeleted=1` / `sDeletedBy="ADMIN001"` / `tDeletedDate IS NOT NULL` / `sProcedureName` 不变 / `sCreatedBy` 不变
  - `deleteNonExistentId_returns40400`：DELETE `/api/mod/modules/99999996` → `code=40400`
  - `deleteAlreadyDeletedId_returns40400`：JdbcTemplate 直插 `bDeleted=1` 行；DELETE → `code=40400`
  - `deleteWithActiveChildren_returns40901`：JdbcTemplate 直插 root + child（bDeleted=0, parent=root）；DELETE root → `code=40901`；JdbcTemplate 查 root 仍 `bDeleted=0`
  - `deleteWithoutJwt_permitAllStub_returns200_andDeletedByIsSTUB`：JdbcTemplate 直插 alive；无 token DELETE；DB 查 `sDeletedBy="STUB_ADMIN"` / `bDeleted=1`
  - `deleteTamperedJwt_returns20001`：JdbcTemplate 直插 alive；Authorization "Bearer not.a.real.jwt" DELETE；`code=20001`；DB 查行 `bDeleted=0`（filter 短路，service 未触发）
  - 6 用例先跑 → FAIL（controller 不存在 → 405/404）

- [ ] **Step 2: 实现 controller DELETE**

- [ ] **Step 3: 子会话跑全量回归**
  - 命令：`cd backend && mvn -B test`
  - 期望：MOD-001 26 + MOD-002 15 + MOD-003 新增 1(mapperIT) + 5(svc) + 6(it) = 53 用例全绿

- [ ] **Step 4: Commit**
  - `git commit -m "test(mod): module delete integration coverage REQ-MOD-003"`

## 提交计划

| commit | 覆盖 |
|---|---|
| `feat(mod): mapper#hasActiveChildren for delete check REQ-MOD-003` | Task 1 |
| `feat(mod): module delete service + soft delete REQ-MOD-003` | Task 2 |
| `test(mod): module delete integration coverage REQ-MOD-003` | Task 3 |
