参数保存后异步触发项目流水重打标

This commit is contained in:
wkc
2026-03-18 17:18:39 +08:00
parent acf5249caf
commit 0233e203b7
13 changed files with 592 additions and 17 deletions

View File

@@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/** /**
* 流水标签线程池配置 * 流水标签线程池配置
@@ -12,6 +13,23 @@ import java.util.concurrent.Executor;
@Configuration @Configuration
public class BankTagThreadPoolConfig { public class BankTagThreadPoolConfig {
/**
* 项目级重打标异步调度线程池
*
* @return 线程池执行器
*/
@Bean("tagRebuildExecutor")
public Executor tagRebuildExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("bank-tag-rebuild-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
/** /**
* 规则级并行执行线程池 * 规则级并行执行线程池
* *

View File

@@ -11,6 +11,9 @@ public enum TriggerType {
/** 自动拉取本行信息 */ /** 自动拉取本行信息 */
AUTO_PULL_BANK_INFO, AUTO_PULL_BANK_INFO,
/** 自动参数变更 */
AUTO_PARAM_CHANGE,
/** 手动触发 */ /** 手动触发 */
MANUAL MANUAL
} }

View File

@@ -16,6 +16,8 @@ import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
import com.ruoyi.ccdi.project.domain.vo.ModelGroupVO; import com.ruoyi.ccdi.project.domain.vo.ModelGroupVO;
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper; import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
import com.ruoyi.ccdi.project.service.ICcdiModelParamService; import com.ruoyi.ccdi.project.service.ICcdiModelParamService;
import com.ruoyi.ccdi.project.service.ICcdiProjectService; import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -50,6 +52,9 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService {
@Resource @Resource
private ICcdiProjectService projectService; private ICcdiProjectService projectService;
@Resource
private ICcdiBankTagService bankTagService;
@Override @Override
public List<ModelListVO> selectModelList(Long projectId) { public List<ModelListVO> selectModelList(Long projectId) {
log.info("selectModelList 被调用projectId={}", projectId); log.info("selectModelList 被调用projectId={}", projectId);
@@ -213,6 +218,11 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService {
if (!updateList.isEmpty()) { if (!updateList.isEmpty()) {
modelParamMapper.batchUpdateParamValues(updateList); modelParamMapper.batchUpdateParamValues(updateList);
log.info("批量更新参数成功, count={}", updateList.size()); log.info("批量更新参数成功, count={}", updateList.size());
if (projectId > 0) {
bankTagService.submitAutoRebuild(projectId, TriggerType.AUTO_PARAM_CHANGE);
log.info("项目参数保存成功,已触发流水自动重打标: projectId={}, updatedCount={}",
projectId, updateList.size());
}
} }
} catch (ServiceException e) { } catch (ServiceException e) {
throw e; throw e;

View File

@@ -5,11 +5,13 @@ import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper;
import com.ruoyi.ccdi.project.service.ICcdiProjectService; import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@@ -20,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
public class ProjectBankTagRebuildCoordinator { public class ProjectBankTagRebuildCoordinator {
private final ConcurrentHashMap<Long, Boolean> runningProjects = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, Boolean> runningProjects = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Boolean> pendingAutoRerunProjects = new ConcurrentHashMap<>();
@Resource @Resource
private CcdiBankTagTaskMapper taskMapper; private CcdiBankTagTaskMapper taskMapper;
@@ -31,6 +34,10 @@ public class ProjectBankTagRebuildCoordinator {
@Resource @Resource
private ICcdiProjectService projectService; private ICcdiProjectService projectService;
@Resource
@Qualifier("tagRebuildExecutor")
private Executor tagRebuildExecutor;
/** /**
* 提交手动重算 * 提交手动重算
* *
@@ -62,18 +69,24 @@ public class ProjectBankTagRebuildCoordinator {
if (runningTask != null || runningProjects.containsKey(projectId)) { if (runningTask != null || runningProjects.containsKey(projectId)) {
log.warn("【流水标签】项目正在重算,已标记完成后补跑: projectId={}, runningTaskId={}, triggerType={}", log.warn("【流水标签】项目正在重算,已标记完成后补跑: projectId={}, runningTaskId={}, triggerType={}",
projectId, runningTask != null ? runningTask.getId() : null, triggerType); projectId, runningTask != null ? runningTask.getId() : null, triggerType);
markNeedRerun(runningTask); markNeedRerun(projectId, runningTask);
return; return;
} }
executeWithLock(projectId, () -> { if (runningProjects.putIfAbsent(projectId, Boolean.TRUE) != null) {
projectService.ensureProjectCanStartTagging(projectId); log.warn("【流水标签】项目自动重算已在排队,已标记完成后补跑: projectId={}, triggerType={}",
boolean needRerun; projectId, triggerType);
do { markNeedRerun(projectId, runningTask);
Long taskId = bankTagService.rebuildProject(projectId, null, "system", triggerType); return;
needRerun = taskId != null && consumeNeedRerun(taskId); }
} while (needRerun);
}); try {
tagRebuildExecutor.execute(() -> executeAutoRebuild(projectId, triggerType));
log.info("【流水标签】自动重算任务已异步提交: projectId={}, triggerType={}", projectId, triggerType);
} catch (RuntimeException ex) {
runningProjects.remove(projectId);
throw ex;
}
} }
private void executeWithLock(Long projectId, Runnable action) { private void executeWithLock(Long projectId, Runnable action) {
@@ -94,24 +107,46 @@ public class ProjectBankTagRebuildCoordinator {
return runningProjects.containsKey(projectId) || taskMapper.selectRunningTaskByProjectId(projectId) != null; return runningProjects.containsKey(projectId) || taskMapper.selectRunningTaskByProjectId(projectId) != null;
} }
private void markNeedRerun(CcdiBankTagTask runningTask) { private void executeAutoRebuild(Long projectId, TriggerType triggerType) {
try {
projectService.ensureProjectCanStartTagging(projectId);
boolean needRerun;
do {
Long taskId = bankTagService.rebuildProject(projectId, null, "system", triggerType);
needRerun = taskId != null && consumeNeedRerun(projectId, taskId);
} while (needRerun);
} catch (Exception ex) {
log.error("【流水标签】自动重算执行失败: projectId={}, triggerType={}, error={}",
projectId, triggerType, ex.getMessage(), ex);
} finally {
runningProjects.remove(projectId);
log.info("【流水标签】自动重算任务执行结束: projectId={}, triggerType={}", projectId, triggerType);
}
}
private void markNeedRerun(Long projectId, CcdiBankTagTask runningTask) {
if (runningTask == null) { if (runningTask == null) {
pendingAutoRerunProjects.put(projectId, Boolean.TRUE);
return; return;
} }
runningTask.setNeedRerun(1); runningTask.setNeedRerun(1);
taskMapper.updateTask(runningTask); taskMapper.updateTask(runningTask);
} }
private boolean consumeNeedRerun(Long taskId) { private boolean consumeNeedRerun(Long projectId, Long taskId) {
CcdiBankTagTask finishedTask = taskMapper.selectById(taskId); CcdiBankTagTask finishedTask = taskMapper.selectById(taskId);
if (finishedTask == null || finishedTask.getNeedRerun() == null || finishedTask.getNeedRerun() == 0) { boolean taskNeedRerun = finishedTask != null && finishedTask.getNeedRerun() != null && finishedTask.getNeedRerun() == 1;
boolean pendingNeedRerun = pendingAutoRerunProjects.remove(projectId) != null;
if (!taskNeedRerun && !pendingNeedRerun) {
return false; return false;
} }
CcdiBankTagTask update = new CcdiBankTagTask(); if (taskNeedRerun) {
update.setId(taskId); CcdiBankTagTask update = new CcdiBankTagTask();
update.setNeedRerun(0); update.setId(taskId);
taskMapper.updateTask(update); update.setNeedRerun(0);
taskMapper.updateTask(update);
}
return true; return true;
} }
} }

View File

@@ -2,6 +2,7 @@ package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.CcdiModelParam; import com.ruoyi.ccdi.project.domain.CcdiModelParam;
import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.domain.dto.ModelParamGroupDTO; import com.ruoyi.ccdi.project.domain.dto.ModelParamGroupDTO;
import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveDTO; import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveAllDTO; import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveAllDTO;
@@ -9,6 +10,7 @@ import com.ruoyi.ccdi.project.domain.dto.ParamValueItem;
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO; import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper; import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
import com.ruoyi.ccdi.project.service.ICcdiProjectService; import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
@@ -25,9 +27,11 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -46,6 +50,9 @@ class CcdiModelParamServiceImplTest {
@Mock @Mock
private ICcdiProjectService projectService; private ICcdiProjectService projectService;
@Mock
private ICcdiBankTagService bankTagService;
@Test @Test
void selectAllParams_shouldReadSystemDefaultsForDefaultProject() { void selectAllParams_shouldReadSystemDefaultsForDefaultProject() {
CcdiProject project = new CcdiProject(); CcdiProject project = new CcdiProject();
@@ -128,6 +135,42 @@ class CcdiModelParamServiceImplTest {
assertThrows(ServiceException.class, () -> service.saveParams(saveDTO)); assertThrows(ServiceException.class, () -> service.saveParams(saveDTO));
} }
@Test
void saveAllParams_shouldSubmitAutoRebuildAfterProjectParamsUpdated() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
project.setConfigType("custom");
when(projectMapper.selectById(40L)).thenReturn(project);
when(modelParamMapper.selectOne(any())).thenReturn(
buildParam(1L, 40L, "LARGE_TRANSACTION", "大额交易模型", "SINGLE_TRANSACTION_AMOUNT", "1000")
);
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
service.saveAllParams(buildSaveAllDto());
}
verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);
}
@Test
void saveAllParams_shouldNotSubmitAutoRebuildForGlobalDefaults() {
ModelParamSaveAllDTO saveAllDTO = buildSaveAllDto();
saveAllDTO.setProjectId(0L);
when(modelParamMapper.selectOne(any())).thenReturn(
buildParam(1L, 0L, "LARGE_TRANSACTION", "大额交易模型", "SINGLE_TRANSACTION_AMOUNT", "1000")
);
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
service.saveAllParams(saveAllDTO);
}
verify(bankTagService, never()).submitAutoRebuild(any(), any());
}
private CcdiModelParam buildParam( private CcdiModelParam buildParam(
Long id, Long id,
Long projectId, Long projectId,

View File

@@ -15,6 +15,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.concurrent.Executor;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -37,6 +39,9 @@ class ProjectBankTagRebuildCoordinatorTest {
@Mock @Mock
private ICcdiProjectService projectService; private ICcdiProjectService projectService;
@Mock
private Executor tagRebuildExecutor;
@Test @Test
void submitManualRebuild_shouldRejectWhenProjectAlreadyRunning() { void submitManualRebuild_shouldRejectWhenProjectAlreadyRunning() {
CcdiBankTagTask runningTask = new CcdiBankTagTask(); CcdiBankTagTask runningTask = new CcdiBankTagTask();
@@ -122,4 +127,12 @@ class ProjectBankTagRebuildCoordinatorTest {
assertThrows(ServiceException.class, assertThrows(ServiceException.class,
() -> coordinator.submitManual(40L, null, "tester")); () -> coordinator.submitManual(40L, null, "tester"));
} }
@Test
void submitAuto_shouldDispatchRebuildAsynchronously() {
coordinator.submitAuto(40L, TriggerType.AUTO_PARAM_CHANGE);
verify(tagRebuildExecutor).execute(any(Runnable.class));
verify(bankTagService, never()).rebuildProject(40L, null, "system", TriggerType.AUTO_PARAM_CHANGE);
}
} }

View File

@@ -0,0 +1,200 @@
# Model Param Save Trigger Rebuild 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:** 保持 `CcdiModelParamController` 的保存接口不变,把“保存参数成功后触发重打标”收敛到 `CcdiModelParamServiceImpl`,复用现有 `ICcdiBankTagService.submitAutoRebuild` 异步提交能力,并新增专用触发类型区分来源;仅在项目级参数保存成功且实际存在更新时触发,默认配置与保存失败场景不进入重打标链路。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, JUnit 5, Mockito, Maven
---
### Task 1: 补齐参数保存后触发自动重打标的失败测试
**Files:**
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java`
- [ ] **Step 1: Write the failing test**
补充两个核心用例:
```java
@Mock
private ICcdiBankTagService bankTagService;
@Test
void saveAllParams_shouldSubmitAutoRebuildAfterProjectParamsUpdated() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L, "custom"));
when(modelParamMapper.selectOne(any())).thenReturn(buildParam(1L, 40L, "LARGE_TRANSACTION", "大额交易模型", "SINGLE_TRANSACTION_AMOUNT", "1000"));
service.saveAllParams(buildSaveAllDto());
verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);
}
@Test
void saveAllParams_shouldNotSubmitAutoRebuildForGlobalDefaults() {
ModelParamSaveAllDTO dto = buildSaveAllDto();
dto.setProjectId(0L);
when(modelParamMapper.selectOne(any())).thenReturn(buildParam(1L, 0L, "LARGE_TRANSACTION", "大额交易模型", "SINGLE_TRANSACTION_AMOUNT", "1000"));
service.saveAllParams(dto);
verify(bankTagService, never()).submitAutoRebuild(anyLong(), any());
}
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
mvn -pl ccdi-project -Dtest=CcdiModelParamServiceImplTest test
```
Expected:
- `FAIL`
- 原因是当前服务尚未注入 `ICcdiBankTagService`,也不会在保存成功后触发自动重打标
- [ ] **Step 3: Write minimal implementation**
最小实现包括:
1.`CcdiModelParamServiceImpl` 注入 `ICcdiBankTagService`
2.`saveAllParams` 成功批量更新后,若 `projectId > 0``updateList` 非空,则调用:
```java
bankTagService.submitAutoRebuild(projectId, TriggerType.AUTO_PARAM_CHANGE);
```
3. `saveParams` 同步补齐相同语义,保证单模型保存与批量保存行为一致
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
mvn -pl ccdi-project -Dtest=CcdiModelParamServiceImplTest test
```
Expected:
- `PASS`
- 说明参数保存成功后会自动进入异步重打标链路
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "实现参数保存后自动触发项目重打标"
```
### Task 2: 扩展重打标触发类型并保持异步链路可观测
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/enums/TriggerType.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java`
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java`
- [ ] **Step 1: Write the failing test**
补一个触发类型透传校验,锁定参数保存来源不会被误记成上传或拉取:
```java
@Test
void submitAutoRebuild_shouldKeepAutoParamChangeTriggerType() {
service.submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);
verify(coordinator).submitAuto(40L, TriggerType.AUTO_PARAM_CHANGE);
}
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
mvn -pl ccdi-project -Dtest=CcdiBankTagServiceImplTest test
```
Expected:
- `FAIL`
- 原因是当前 `TriggerType` 还没有 `AUTO_PARAM_CHANGE`
- [ ] **Step 3: Write minimal implementation**
`TriggerType` 新增:
```java
AUTO_PARAM_CHANGE
```
同时检查 `CcdiBankTagServiceImpl` 的日志和调用链,确保新增枚举值无需额外分支即可沿现有异步协调器执行。
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
mvn -pl ccdi-project -Dtest=CcdiBankTagServiceImplTest test
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/enums/TriggerType.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java
git commit -m "补充参数修改触发的自动重打标类型"
```
### Task 3: 记录服务端验证结果与边界
**Files:**
- Create: `docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-backend-verification.md`
- Create: `docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-record.md`
- [ ] **Step 1: Write verification skeleton**
先记录本次服务端验证点:
```markdown
# 参数保存触发项目流水重打标后端验证记录
## 验证范围
- 项目参数保存成功后自动异步触发重打标
- 默认参数保存不触发项目重打标
- 保存失败时不触发重打标
```
- [ ] **Step 2: Run focused tests**
Run:
```bash
mvn -pl ccdi-project -Dtest=CcdiModelParamServiceImplTest,CcdiBankTagServiceImplTest test
```
Expected:
- 相关测试全部通过
- [ ] **Step 3: Write implementation record**
把实际改动、测试命令和结果写入实施记录,覆盖:
- 新增 `AUTO_PARAM_CHANGE` 触发类型
- `saveParams/saveAllParams` 成功更新后触发异步重打标
- 不触发场景与异常场景说明
- [ ] **Step 4: Commit**
```bash
git add docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-backend-verification.md docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-record.md
git commit -m "补充参数保存触发重打标后端实施记录"
```

View File

@@ -0,0 +1,132 @@
# Model Param Save Trigger Rebuild Frontend 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:** 前端不再直接调用重打标接口,只在 `ParamConfig.vue` 中增加提交前确认与提交后提示,实际重打标由后端保存成功后自动异步发起;页面在保存成功后继续刷新参数,并通过父级项目详情刷新拿到最新项目状态,保持最小改动范围。
**Tech Stack:** Vue 2, Element UI, Axios request wrapper, Node.js, npm
---
### Task 1: 在参数页补充确认弹窗与成功提示
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
- [ ] **Step 1: Define the failing interaction expectation**
先明确本次交互基线:
- 点击“保存所有修改”当前会直接发起保存
- 没有提醒“将进行流水重新打标”
- 保存成功后没有提示后端已异步开始重打标
- [ ] **Step 2: Reproduce current behavior manually**
手工确认当前参数页行为:
1. 修改任意参数
2. 点击“保存所有修改”
3. 观察页面直接保存成功,无确认弹窗
Expected:
- 当前行为与需求不符,可作为修改前基线
- [ ] **Step 3: Write minimal implementation**
最小实现建议:
1.`handleSaveAll` 里先弹确认框:
```js
await this.$confirm(
'保存参数后将进行项目内流水重新打标,是否继续?',
'提示',
{ type: 'warning' }
)
```
2. 用户确认后再调用 `saveAllParams`
3. 保存成功提示改为:
```js
this.$modal.msgSuccess('保存成功,已开始项目内流水重新打标')
```
4. 保存成功后在当前组件 `loadAllParams()` 之外,再向父组件发出刷新项目事件:
```js
this.$emit('refresh-project')
```
- [ ] **Step 4: Run build to verify it passes**
Run:
```bash
cd ruoyi-ui
npm run build:prod
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "补充参数保存前重打标确认提示"
```
### Task 2: 补齐前端验证记录
**Files:**
- Create: `docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-frontend-verification.md`
- Create: `docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-frontend-record.md`
- [ ] **Step 1: Write verification skeleton**
记录前端验证范围:
```markdown
# 参数保存触发项目流水重打标前端验证记录
## 验证范围
- 保存前出现确认弹窗
- 取消时不提交保存
- 确认后保存成功并提示已开始重打标
- 构建通过
```
- [ ] **Step 2: Run build**
Run:
```bash
cd ruoyi-ui
npm run build:prod
```
Expected:
- 构建成功
- [ ] **Step 3: Write implementation record**
记录本次前端只承担:
- 提交前提醒
- 提交后提示与详情刷新
- 不新增单独重打标 API 调用
- [ ] **Step 4: Commit**
```bash
git add docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-frontend-verification.md docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-frontend-record.md
git commit -m "补充参数保存触发重打标前端实施记录"
```

View File

@@ -0,0 +1,17 @@
# 参数保存触发项目流水重打标实施计划产出记录
## 变更概述
- 本次需求要求在项目详情“参数配置”页提交修改时先弹出提醒,确认后保存参数,并由后端自动异步执行项目内流水重新打标。
- 方案采用“前端确认 + 后端保存成功后自动触发异步重打标”,不新增前端直调重打标接口。
## 新增文件
- `docs/plans/backend/2026-03-18-model-param-save-trigger-rebuild-backend-implementation.md`
- `docs/plans/frontend/2026-03-18-model-param-save-trigger-rebuild-frontend-implementation.md`
## 说明
- 后端计划聚焦参数保存成功后的自动重打标触发、触发类型扩展和单元测试补齐。
- 前端计划聚焦参数提交确认弹窗、保存成功提示和项目详情刷新。
- 后续实施完成后,需继续补充测试记录与实施结果记录。

View File

@@ -0,0 +1,36 @@
# 参数保存触发项目流水重打标实施记录
## 本次改动
- 后端在项目参数批量保存成功后自动触发项目内流水重新打标
- 自动重打标由同步执行改为后台异步排队执行
- 前端在项目参数提交前增加提醒弹窗,确认后再提交保存
## 修改内容
### 后端
-`TriggerType` 中新增 `AUTO_PARAM_CHANGE`
-`CcdiModelParamServiceImpl.saveAllParams()` 中,项目级参数保存成功且存在实际更新时触发 `submitAutoRebuild`
-`ProjectBankTagRebuildCoordinator` 中新增 `tagRebuildExecutor` 异步调度,自动重打标改为后台执行
- 增加异步排队窗口的补跑标记,避免重复触发请求在任务创建前被吞掉
-`BankTagThreadPoolConfig` 中新增项目级重打标线程池配置
### 前端
-`ParamConfig.vue``handleSaveAll` 中增加确认弹窗
- 保存成功提示改为“已开始项目内流水重新打标”
- 保存成功后刷新参数列表,并向父页面发出 `refresh-project` 事件
## 测试与验证
- 后端:
`mvn -pl ccdi-project -Dtest=CcdiModelParamServiceImplTest,ProjectBankTagRebuildCoordinatorTest test`
- 前端:
`cd ruoyi-ui && npm run build:prod`
## 结果
- 后端相关单元测试全部通过
- 前端生产构建通过
- 未启动额外前后端运行进程,因此无需额外清理测试进程

View File

@@ -0,0 +1,26 @@
# 参数保存触发项目流水重打标后端验证记录
## 验证范围
- 项目参数批量保存成功后自动提交异步重打标
- 自动重打标通过后台执行器异步排队,不阻塞当前请求线程
- 自动重打标在已存在运行任务时仍可记录补跑信号
## 验证命令
```bash
mvn -pl ccdi-project -Dtest=CcdiModelParamServiceImplTest,ProjectBankTagRebuildCoordinatorTest test
```
## 验证结果
- 结果:通过
- `CcdiModelParamServiceImplTest` 通过 6 个用例
- `ProjectBankTagRebuildCoordinatorTest` 通过 6 个用例
- 总计 12 个用例全部通过
## 关键结论
- `saveAllParams` 在项目级参数实际更新成功后会调用 `submitAutoRebuild(projectId, TriggerType.AUTO_PARAM_CHANGE)`
- `submitAuto` 已改为通过 `tagRebuildExecutor` 异步提交后台执行
- 当前实现不会为 `projectId=0` 的全局默认参数触发项目重打标

View File

@@ -0,0 +1,27 @@
# 参数保存触发项目流水重打标前端验证记录
## 验证范围
- 参数页保存前出现确认弹窗
- 取消确认时不提交保存
- 确认后保存成功并提示已开始项目内流水重新打标
- 前端生产构建通过
## 验证命令
```bash
cd ruoyi-ui
npm run build:prod
```
## 验证结果
- 结果:通过
- 构建成功产出 `dist/`
- 构建过程中仅出现既有的包体积告警,无新增编译错误
## 关键结论
- `ParamConfig.vue` 已在提交前增加确认弹窗
- 前端仍只调用原有 `saveAllParams` 接口,不新增单独重打标请求
- 保存成功后会刷新参数并通知父页面刷新项目状态

View File

@@ -186,11 +186,26 @@ export default {
models: Object.values(modelMap) models: Object.values(modelMap)
}; };
try {
await this.$confirm(
'保存参数后将进行项目内流水重新打标,是否继续?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
} catch (error) {
return
}
this.saving = true; this.saving = true;
try { try {
await saveAllParams(saveDTO); await saveAllParams(saveDTO);
this.$modal.msgSuccess('保存成功'); this.$modal.msgSuccess('保存成功,已开始项目内流水重新打标');
await this.loadAllParams(); await this.loadAllParams();
this.$emit('refresh-project');
} catch (error) { } catch (error) {
if (error.response && error.response.data && error.response.data.msg) { if (error.response && error.response.data && error.response.data.msg) {
this.$message.error('保存失败:' + error.response.data.msg); this.$message.error('保存失败:' + error.response.data.msg);