diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/config/BankTagThreadPoolConfig.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/config/BankTagThreadPoolConfig.java index 477ba05b..543944dd 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/config/BankTagThreadPoolConfig.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/config/BankTagThreadPoolConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; /** * 流水标签线程池配置 @@ -12,6 +13,23 @@ import java.util.concurrent.Executor; @Configuration 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; + } + /** * 规则级并行执行线程池 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/enums/TriggerType.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/enums/TriggerType.java index 26668748..10b445d0 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/enums/TriggerType.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/enums/TriggerType.java @@ -11,6 +11,9 @@ public enum TriggerType { /** 自动拉取本行信息 */ AUTO_PULL_BANK_INFO, + /** 自动参数变更 */ + AUTO_PARAM_CHANGE, + /** 手动触发 */ MANUAL } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java index fd64202e..df96ce41 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java @@ -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.mapper.CcdiModelParamMapper; 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.ICcdiProjectService; import org.apache.commons.lang3.StringUtils; @@ -50,6 +52,9 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService { @Resource private ICcdiProjectService projectService; + @Resource + private ICcdiBankTagService bankTagService; + @Override public List selectModelList(Long projectId) { log.info("selectModelList 被调用,projectId={}", projectId); @@ -213,6 +218,11 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService { if (!updateList.isEmpty()) { modelParamMapper.batchUpdateParamValues(updateList); 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) { throw e; diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java index f6b4d03a..97af32dd 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java @@ -5,11 +5,13 @@ import com.ruoyi.ccdi.project.domain.enums.TriggerType; import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper; import com.ruoyi.ccdi.project.service.ICcdiProjectService; import com.ruoyi.common.exception.ServiceException; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import lombok.extern.slf4j.Slf4j; /** @@ -20,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; public class ProjectBankTagRebuildCoordinator { private final ConcurrentHashMap runningProjects = new ConcurrentHashMap<>(); + private final ConcurrentHashMap pendingAutoRerunProjects = new ConcurrentHashMap<>(); @Resource private CcdiBankTagTaskMapper taskMapper; @@ -31,6 +34,10 @@ public class ProjectBankTagRebuildCoordinator { @Resource private ICcdiProjectService projectService; + @Resource + @Qualifier("tagRebuildExecutor") + private Executor tagRebuildExecutor; + /** * 提交手动重算 * @@ -62,18 +69,24 @@ public class ProjectBankTagRebuildCoordinator { if (runningTask != null || runningProjects.containsKey(projectId)) { log.warn("【流水标签】项目正在重算,已标记完成后补跑: projectId={}, runningTaskId={}, triggerType={}", projectId, runningTask != null ? runningTask.getId() : null, triggerType); - markNeedRerun(runningTask); + markNeedRerun(projectId, runningTask); return; } - executeWithLock(projectId, () -> { - projectService.ensureProjectCanStartTagging(projectId); - boolean needRerun; - do { - Long taskId = bankTagService.rebuildProject(projectId, null, "system", triggerType); - needRerun = taskId != null && consumeNeedRerun(taskId); - } while (needRerun); - }); + if (runningProjects.putIfAbsent(projectId, Boolean.TRUE) != null) { + log.warn("【流水标签】项目自动重算已在排队,已标记完成后补跑: projectId={}, triggerType={}", + projectId, triggerType); + markNeedRerun(projectId, runningTask); + return; + } + + 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) { @@ -94,24 +107,46 @@ public class ProjectBankTagRebuildCoordinator { 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) { + pendingAutoRerunProjects.put(projectId, Boolean.TRUE); return; } runningTask.setNeedRerun(1); taskMapper.updateTask(runningTask); } - private boolean consumeNeedRerun(Long taskId) { + private boolean consumeNeedRerun(Long projectId, Long 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; } - CcdiBankTagTask update = new CcdiBankTagTask(); - update.setId(taskId); - update.setNeedRerun(0); - taskMapper.updateTask(update); + if (taskNeedRerun) { + CcdiBankTagTask update = new CcdiBankTagTask(); + update.setId(taskId); + update.setNeedRerun(0); + taskMapper.updateTask(update); + } return true; } } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java index 73613f24..d6e2aafd 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImplTest.java @@ -2,6 +2,7 @@ package com.ruoyi.ccdi.project.service.impl; import com.ruoyi.ccdi.project.domain.CcdiModelParam; 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.ModelParamSaveDTO; 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.mapper.CcdiModelParamMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; +import com.ruoyi.ccdi.project.service.ICcdiBankTagService; import com.ruoyi.ccdi.project.service.ICcdiProjectService; import com.ruoyi.common.exception.ServiceException; 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.assertThrows; 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.eq; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -46,6 +50,9 @@ class CcdiModelParamServiceImplTest { @Mock private ICcdiProjectService projectService; + @Mock + private ICcdiBankTagService bankTagService; + @Test void selectAllParams_shouldReadSystemDefaultsForDefaultProject() { CcdiProject project = new CcdiProject(); @@ -128,6 +135,42 @@ class CcdiModelParamServiceImplTest { 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 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 mocked = mockStatic(SecurityUtils.class)) { + mocked.when(SecurityUtils::getUsername).thenReturn("admin"); + + service.saveAllParams(saveAllDTO); + } + + verify(bankTagService, never()).submitAutoRebuild(any(), any()); + } + private CcdiModelParam buildParam( Long id, Long projectId, diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java index 97887720..3012ddc3 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java @@ -15,6 +15,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; +import java.util.concurrent.Executor; + import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -37,6 +39,9 @@ class ProjectBankTagRebuildCoordinatorTest { @Mock private ICcdiProjectService projectService; + @Mock + private Executor tagRebuildExecutor; + @Test void submitManualRebuild_shouldRejectWhenProjectAlreadyRunning() { CcdiBankTagTask runningTask = new CcdiBankTagTask(); @@ -122,4 +127,12 @@ class ProjectBankTagRebuildCoordinatorTest { assertThrows(ServiceException.class, () -> 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); + } } diff --git a/docs/plans/backend/2026-03-18-model-param-save-trigger-rebuild-backend-implementation.md b/docs/plans/backend/2026-03-18-model-param-save-trigger-rebuild-backend-implementation.md new file mode 100644 index 00000000..bd17c642 --- /dev/null +++ b/docs/plans/backend/2026-03-18-model-param-save-trigger-rebuild-backend-implementation.md @@ -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 "补充参数保存触发重打标后端实施记录" +``` diff --git a/docs/plans/frontend/2026-03-18-model-param-save-trigger-rebuild-frontend-implementation.md b/docs/plans/frontend/2026-03-18-model-param-save-trigger-rebuild-frontend-implementation.md new file mode 100644 index 00000000..7706063d --- /dev/null +++ b/docs/plans/frontend/2026-03-18-model-param-save-trigger-rebuild-frontend-implementation.md @@ -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 "补充参数保存触发重打标前端实施记录" +``` diff --git a/docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-plan-record.md b/docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-plan-record.md new file mode 100644 index 00000000..845abe4a --- /dev/null +++ b/docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-plan-record.md @@ -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` + +## 说明 + +- 后端计划聚焦参数保存成功后的自动重打标触发、触发类型扩展和单元测试补齐。 +- 前端计划聚焦参数提交确认弹窗、保存成功提示和项目详情刷新。 +- 后续实施完成后,需继续补充测试记录与实施结果记录。 diff --git a/docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-record.md b/docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-record.md new file mode 100644 index 00000000..bc78749a --- /dev/null +++ b/docs/reports/implementation/2026-03-18-model-param-save-trigger-rebuild-record.md @@ -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` + +## 结果 + +- 后端相关单元测试全部通过 +- 前端生产构建通过 +- 未启动额外前后端运行进程,因此无需额外清理测试进程 diff --git a/docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-backend-verification.md b/docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-backend-verification.md new file mode 100644 index 00000000..dde3a368 --- /dev/null +++ b/docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-backend-verification.md @@ -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` 的全局默认参数触发项目重打标 diff --git a/docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-frontend-verification.md b/docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-frontend-verification.md new file mode 100644 index 00000000..57629e8f --- /dev/null +++ b/docs/tests/records/2026-03-18-model-param-save-trigger-rebuild-frontend-verification.md @@ -0,0 +1,27 @@ +# 参数保存触发项目流水重打标前端验证记录 + +## 验证范围 + +- 参数页保存前出现确认弹窗 +- 取消确认时不提交保存 +- 确认后保存成功并提示已开始项目内流水重新打标 +- 前端生产构建通过 + +## 验证命令 + +```bash +cd ruoyi-ui +npm run build:prod +``` + +## 验证结果 + +- 结果:通过 +- 构建成功产出 `dist/` +- 构建过程中仅出现既有的包体积告警,无新增编译错误 + +## 关键结论 + +- `ParamConfig.vue` 已在提交前增加确认弹窗 +- 前端仍只调用原有 `saveAllParams` 接口,不新增单独重打标请求 +- 保存成功后会刷新参数并通知父页面刷新项目状态 diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue index 50e19828..47324a15 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue @@ -186,11 +186,26 @@ export default { models: Object.values(modelMap) }; + try { + await this.$confirm( + '保存参数后将进行项目内流水重新打标,是否继续?', + '提示', + { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + } + ) + } catch (error) { + return + } + this.saving = true; try { await saveAllParams(saveDTO); - this.$modal.msgSuccess('保存成功'); + this.$modal.msgSuccess('保存成功,已开始项目内流水重新打标'); await this.loadAllParams(); + this.$emit('refresh-project'); } catch (error) { if (error.response && error.response.data && error.response.data.msg) { this.$message.error('保存失败:' + error.response.data.msg);