# Project Bank Tag Status Lock Backend Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 让项目在银行流水打标开始时进入“打标中”，在成功后进入“已完成”、失败后回退为“进行中”，并由后端统一拦截打标期间的上传、拉取本行信息和参数修改操作。

**Architecture:** 以 `ccdi_project.status` 作为唯一业务状态源，在项目模块中补充统一状态更新能力，由 `CcdiBankTagServiceImpl` 和 `ProjectBankTagRebuildCoordinator` 复用；上传与参数服务在进入写操作前统一校验项目状态；SQL 通过增量脚本补齐状态字典，并同步更新初始化脚本和单元测试。

**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, JUnit 5, Mockito, Maven, MySQL SQL migration

---

### Task 1: 补齐项目状态 SQL 与字典基线

**Files:**
- Modify: `sql/ccdi_project.sql`
- Create: `sql/migration/2026-03-18-add-project-tagging-status.sql`
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiProjectStatusSqlTest.java`

- [ ] **Step 1: Write the failing test**

先新增 `CcdiProjectStatusSqlTest`，锁定初始化脚本和增量脚本都必须包含 `3-打标中`：

```java
class CcdiProjectStatusSqlTest {

    @Test
    void shouldContainTaggingStatusInInitAndMigrationSql() throws Exception {
        String initSql = Files.readString(Path.of("sql/ccdi_project.sql"));
        String migrationSql = Files.readString(Path.of("sql/migration/2026-03-18-add-project-tagging-status.sql"));

        assertTrue(initSql.contains("打标中"));
        assertTrue(initSql.contains("'3'"));
        assertTrue(migrationSql.contains("ccdi_project_status"));
        assertTrue(migrationSql.contains("打标中"));
        assertTrue(migrationSql.contains("'3'"));
    }
}
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectStatusSqlTest test
```

Expected:

- `FAIL`
- 原因是测试类和增量 SQL 还不存在，初始化脚本也尚未包含状态 `3`

- [ ] **Step 3: Write minimal implementation**

最小实现包括两部分：

1. 在 `sql/ccdi_project.sql` 中把状态注释和状态字典更新为：

```sql
`status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '项目状态：0-进行中，1-已完成，2-已归档，3-打标中'
```

```sql
(4, '打标中', '3', 'ccdi_project_status', '', 'warning', 'N', '0', 'admin', NOW());
```

2. 新增增量脚本 `sql/migration/2026-03-18-add-project-tagging-status.sql`，包含：

```sql
ALTER TABLE ccdi_project
    MODIFY COLUMN status CHAR(1) NOT NULL DEFAULT '0' COMMENT '项目状态：0-进行中，1-已完成，2-已归档，3-打标中';

INSERT INTO sys_dict_data (...)
SELECT ...
WHERE NOT EXISTS (
    SELECT 1 FROM sys_dict_data WHERE dict_type = 'ccdi_project_status' AND dict_value = '3'
);
```

- [ ] **Step 4: Run test to verify it passes**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectStatusSqlTest test
```

Expected:

- `PASS`
- 说明新环境脚本和增量脚本都已统一包含“打标中”

- [ ] **Step 5: Commit**

```bash
git add sql/ccdi_project.sql sql/migration/2026-03-18-add-project-tagging-status.sql ccdi-project/src/test/java/com/ruoyi/ccdi/project/sql/CcdiProjectStatusSqlTest.java
git commit -m "补充项目打标中状态SQL基线"
```

### Task 2: 增加项目状态统一更新能力、写保护能力与状态统计扩展

**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/constants/CcdiProjectStatusConstants.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/CcdiProject.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java`

- [ ] **Step 1: Write the failing test**

新增 `CcdiProjectServiceImplTest`，先锁定三个核心行为：状态统计要包含 `status3`，已归档项目不能重新进入打标，打标中项目不能继续写入上传/参数数据。

```java
@ExtendWith(MockitoExtension.class)
class CcdiProjectServiceImplTest {

    @InjectMocks
    private CcdiProjectServiceImpl service;

    @Mock
    private CcdiProjectMapper projectMapper;

    @Test
    void shouldCountTaggingProjectsSeparately() {
        when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L);

        CcdiProjectStatusCountsVO counts = service.getStatusCounts();

        assertEquals(1L, counts.getStatus3());
    }

    @Test
    void shouldRejectUpdatingArchivedProjectToTagging() {
        CcdiProject archived = new CcdiProject();
        archived.setProjectId(99L);
        archived.setStatus("2");
        when(projectMapper.selectById(99L)).thenReturn(archived);

        assertThrows(ServiceException.class,
            () -> service.updateProjectStatus(99L, "3", "system"));
    }

    @Test
    void shouldRejectWritingWhenProjectIsTagging() {
        CcdiProject tagging = new CcdiProject();
        tagging.setProjectId(40L);
        tagging.setStatus("3");
        when(projectMapper.selectById(40L)).thenReturn(tagging);

        assertThrows(ServiceException.class,
            () -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标，暂不允许修改参数"));
    }
}
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest test
```

Expected:

- `FAIL`
- 原因是 `status3` 统计字段和统一状态更新方法尚未实现

- [ ] **Step 3: Write minimal implementation**

最小实现建议：

1. 新增状态常量类：

```java
public final class CcdiProjectStatusConstants {
    public static final String PROCESSING = "0";
    public static final String COMPLETED = "1";
    public static final String ARCHIVED = "2";
    public static final String TAGGING = "3";
}
```

2. 在 `ICcdiProjectService` / `CcdiProjectServiceImpl` 中新增 3 个统一能力：

```java
void updateProjectStatus(Long projectId, String status, String operator);
void ensureProjectCanStartTagging(Long projectId);
void ensureProjectWritable(Long projectId, String message);
```

```java
public void updateProjectStatus(Long projectId, String status, String operator) {
    CcdiProject project = getRequiredProject(projectId);
    if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())
        && !CcdiProjectStatusConstants.ARCHIVED.equals(status)) {
        throw new ServiceException("已归档项目不允许重新进入打标流程");
    }
    project.setStatus(status);
    project.setUpdateBy(operator);
    projectMapper.updateById(project);
}
```

```java
public void ensureProjectCanStartTagging(Long projectId) {
    CcdiProject project = getRequiredProject(projectId);
    if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
        throw new ServiceException("已归档项目不允许重新进入打标流程");
    }
}
```

```java
public void ensureProjectWritable(Long projectId, String message) {
    CcdiProject project = getRequiredProject(projectId);
    if (CcdiProjectStatusConstants.TAGGING.equals(project.getStatus())) {
        throw new ServiceException(message);
    }
}
```

3. 扩展状态统计：

```java
private Long status3;
```

```java
vo.setStatus3(projectMapper.selectCount(
    new LambdaQueryWrapper<CcdiProject>().eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAGGING)
));
```

- [ ] **Step 4: Run test to verify it passes**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest test
```

Expected:

- `PASS`
- 说明统一状态更新能力和 `打标中` 统计口径已成立

- [ ] **Step 5: Commit**

```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/constants/CcdiProjectStatusConstants.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/CcdiProject.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java
git commit -m "新增项目状态统一更新能力"
```

### Task 3: 将打标任务状态流转接入项目状态

**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java`
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java`
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java`

- [ ] **Step 1: Write the failing test**

先在 `CcdiBankTagServiceImplTest` 中锁定成功与失败两条状态流转，再在协调器测试里锁定已归档项目不能重入：

```java
@Test
void shouldMarkProjectTaggingBeforeExecutingAndCompletedAfterSuccess() {
    ...
    service.rebuildProject(40L, null, "tester", TriggerType.MANUAL);

    InOrder inOrder = inOrder(projectService, taskMapper);
    inOrder.verify(projectService).updateProjectStatus(40L, "3", "tester");
    inOrder.verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus())));
    inOrder.verify(projectService).updateProjectStatus(40L, "1", "tester");
}

@Test
void shouldRollbackProjectStatusToProcessingWhenRebuildFails() {
    ...
    assertThrows(RuntimeException.class,
        () -> service.rebuildProject(40L, null, "tester", TriggerType.MANUAL));

    verify(projectService).updateProjectStatus(40L, "0", "tester");
}
```

```java
@Test
void shouldRejectSubmittingRebuildForArchivedProject() {
    doThrow(new ServiceException("已归档项目不允许重新进入打标流程"))
        .when(projectService).ensureProjectCanStartTagging(40L);

    assertThrows(ServiceException.class,
        () -> coordinator.submitManual(40L, null, "tester"));
}
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiBankTagServiceImplTest,ProjectBankTagRebuildCoordinatorTest test
```

Expected:

- `FAIL`
- 原因是打标服务还没有调用项目状态更新能力

- [ ] **Step 3: Write minimal implementation**

实现要点：

1. 在 `CcdiBankTagServiceImpl` 注入项目服务，并在 `rebuildProject(...)` 中按顺序调用：

```java
projectService.updateProjectStatus(projectId, CcdiProjectStatusConstants.TAGGING, operator);
```

```java
projectService.updateProjectStatus(projectId, CcdiProjectStatusConstants.COMPLETED, operator);
```

```java
projectService.updateProjectStatus(projectId, CcdiProjectStatusConstants.PROCESSING, operator);
```

2. 在 `ProjectBankTagRebuildCoordinator` 中增加前置校验，避免已归档项目进入重算：

```java
projectService.ensureProjectCanStartTagging(projectId);
```

3. 保持 `needRerun` 机制不变，但不要在补跑期间把状态提前切回非 `3`

- [ ] **Step 4: Run test to verify it passes**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiBankTagServiceImplTest,ProjectBankTagRebuildCoordinatorTest test
```

Expected:

- `PASS`
- 说明手工与自动打标共用的重算主链路已经接入项目状态流转

- [ ] **Step 5: Commit**

```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java
git commit -m "接入项目打标状态流转"
```

### Task 4: 在上传与参数保存链路中拦截“打标中”写操作

**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java`

- [ ] **Step 1: Write the failing test**

先补两组服务层测试：

```java
@Test
void shouldRejectPullBankInfoWhenProjectIsTagging() {
    CcdiProject project = new CcdiProject();
    project.setProjectId(40L);
    project.setStatus("3");
    when(projectMapper.selectById(40L)).thenReturn(project);

    assertThrows(ServiceException.class,
        () -> service.submitPullBankInfo(40L, List.of("3301"), "2026-01-01", "2026-01-31", 1L, "tester"));
}
```

```java
@Test
void shouldRejectSaveAllParamsWhenProjectIsTagging() {
    CcdiProject project = new CcdiProject();
    project.setProjectId(40L);
    project.setStatus("3");
    when(projectMapper.selectById(40L)).thenReturn(project);

    assertThrows(ServiceException.class, () -> service.saveAllParams(buildSaveAllDto()));
}
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest,CcdiModelParamServiceImplTest test
```

Expected:

- `FAIL`
- 原因是当前上传和参数服务没有“打标中”拦截

- [ ] **Step 3: Write minimal implementation**

在两个服务中增加统一状态校验，优先复用项目服务：

```java
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标，暂不允许上传或拉取数据");
```

```java
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标，暂不允许修改参数");
```

若不额外新增专门方法，至少要抽一个私有校验函数，避免状态字符串判断散落在多个类中。

- [ ] **Step 4: Run test to verify it passes**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest,CcdiModelParamServiceImplTest test
```

Expected:

- `PASS`
- 说明后端已经能兜底拒绝打标期间的输入变更

- [ ] **Step 5: Commit**

```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java
git commit -m "拦截项目打标中的上传与参数修改"
```

### Task 5: 完成回归验证、SQL 执行说明与实施记录

**Files:**
- Create: `docs/tests/records/2026-03-18-project-bank-tag-status-lock-backend-verification.md`
- Create: `docs/reports/implementation/2026-03-18-project-bank-tag-status-lock-backend-implementation.md`

- [ ] **Step 1: Prepare backend verification record**

先创建验证记录，至少包含以下检查项：

```markdown
# 项目打标状态联动后端验证记录

## 验证项
- [ ] 状态 `3-打标中` SQL 已同步
- [ ] 打标成功后状态为 `1`
- [ ] 打标失败后状态回退为 `0`
- [ ] 打标中拒绝上传/拉取本行信息
- [ ] 打标中拒绝参数保存
```

- [ ] **Step 2: Run focused backend regression**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest,CcdiBankTagServiceImplTest,ProjectBankTagRebuildCoordinatorTest,CcdiFileUploadServiceImplTest,CcdiModelParamServiceImplTest,CcdiProjectStatusSqlTest test
```

Expected:

- 所有相关测试 `PASS`

如需在联调环境执行 SQL，必须使用：

```bash
bin/mysql_utf8_exec.sh sql/migration/2026-03-18-add-project-tagging-status.sql
```

- [ ] **Step 3: Write implementation report**

在实施报告中记录：

- 新增状态 `3-打标中`
- 打标状态流转接入的主链路
- 上传/参数保存拦截点
- 测试结果与 SQL 执行方式

- [ ] **Step 4: Commit**

```bash
git add docs/tests/records/2026-03-18-project-bank-tag-status-lock-backend-verification.md docs/reports/implementation/2026-03-18-project-bank-tag-status-lock-backend-implementation.md
git commit -m "补充项目打标状态联动后端验证记录"
```

## Backend Exit Criteria

- `ccdi_project.status` 和 `ccdi_project_status` 字典都支持 `3-打标中`
- 所有银行流水打标入口都会先把项目置为 `3`
- 打标成功后落为 `1`，失败后回退为 `0`
- 打标期间上传、拉取本行信息、参数保存都会被后端拒绝
- 后端测试、SQL 说明、实施记录都已补齐
