From acf5249caf01a949f49f028ee4d6b6bc9ba7baf8 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 18 Mar 2026 17:03:23 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E9=A1=B9=E7=9B=AE=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=8F=98=E6=9B=B4=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/CcdiProjectServiceImpl.java | 27 +++++ .../impl/CcdiProjectServiceImplTest.java | 103 ++++++++++++++++++ .../2026-03-18-项目状态变更日志实施计划.md | 84 ++++++++++++++ .../2026-03-18-项目状态变更日志实施记录.md | 26 +++++ 4 files changed, 240 insertions(+) create mode 100644 docs/plans/backend/2026-03-18-项目状态变更日志实施计划.md create mode 100644 docs/reports/implementation/2026-03-18-项目状态变更日志实施记录.md diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java index 03be10de..25308559 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java @@ -15,17 +15,21 @@ import com.ruoyi.lsfx.client.LsfxAnalysisClient; import com.ruoyi.lsfx.domain.request.GetTokenRequest; import com.ruoyi.lsfx.domain.response.GetTokenResponse; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import java.util.Date; +import java.util.Objects; /** * 项目Service实现类 * * @author ruoyi */ +@Slf4j @Service public class CcdiProjectServiceImpl implements ICcdiProjectService { @@ -56,6 +60,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService { // 4. 保存到数据库 projectMapper.insert(project); + log.info("【项目】项目状态初始化: projectId={}, projectName={}, newStatus={}, newStatusLabel={}, operator={}", + project.getProjectId(), project.getProjectName(), project.getStatus(), resolveStatusLabel(project.getStatus()), + resolveOperator(project.getCreateBy())); // 5. 返回VO CcdiProjectVO vo = new CcdiProjectVO(); @@ -148,6 +155,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService { @Override public void updateProjectStatus(Long projectId, String status, String operator) { CcdiProject project = getRequiredProject(projectId); + String oldStatus = project.getStatus(); if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus()) && !CcdiProjectStatusConstants.ARCHIVED.equals(status)) { throw new ServiceException("已归档项目不允许重新进入打标流程"); @@ -156,6 +164,11 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService { project.setUpdateBy(operator); project.setUpdateTime(new Date()); projectMapper.updateById(project); + if (!Objects.equals(oldStatus, status)) { + log.info("【项目】项目状态变更: projectId={}, projectName={}, oldStatus={}, oldStatusLabel={}, newStatus={}, newStatusLabel={}, operator={}", + project.getProjectId(), project.getProjectName(), oldStatus, resolveStatusLabel(oldStatus), + status, resolveStatusLabel(status), resolveOperator(operator)); + } } @Override @@ -182,6 +195,20 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService { return project; } + private String resolveStatusLabel(String status) { + return switch (status) { + case CcdiProjectStatusConstants.PROCESSING -> "进行中"; + case CcdiProjectStatusConstants.COMPLETED -> "已完成"; + case CcdiProjectStatusConstants.ARCHIVED -> "已归档"; + case CcdiProjectStatusConstants.TAGGING -> "打标中"; + default -> "未知"; + }; + } + + private String resolveOperator(String operator) { + return StringUtils.hasText(operator) ? operator : "system"; + } + /** * 调用流水分析平台获取projectId * diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java index e5a09493..52b5e739 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java @@ -1,19 +1,28 @@ package com.ruoyi.ccdi.project.service.impl; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.lsfx.client.LsfxAnalysisClient; +import com.ruoyi.lsfx.domain.response.GetTokenResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.LoggerFactory; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; 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.Mockito.doAnswer; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -58,4 +67,98 @@ class CcdiProjectServiceImplTest { assertThrows(ServiceException.class, () -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标,暂不允许修改参数")); } + + @Test + void shouldLogProjectInitialStatusWhenProjectIsCreated() { + CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO(); + dto.setProjectName("专案A"); + dto.setDescription("测试项目"); + dto.setConfigType("default"); + + when(lsfxAnalysisClient.getToken(any())).thenReturn(buildTokenResponse(2001)); + doAnswer(invocation -> { + CcdiProject project = invocation.getArgument(0); + project.setProjectId(88L); + return 1; + }).when(projectMapper).insert(any(CcdiProject.class)); + + Logger logger = (Logger) LoggerFactory.getLogger(CcdiProjectServiceImpl.class); + ListAppender logAppender = new ListAppender<>(); + logAppender.start(); + logger.addAppender(logAppender); + + try { + service.createProject(dto); + + assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage) + .anyMatch(message -> message.contains("项目状态初始化") + && message.contains("projectId=88") + && message.contains("projectName=专案A") + && message.contains("newStatus=0"))); + } finally { + logger.detachAppender(logAppender); + } + } + + @Test + void shouldLogProjectStatusTransitionWhenStatusChanges() { + CcdiProject project = new CcdiProject(); + project.setProjectId(40L); + project.setProjectName("专案B"); + project.setStatus("0"); + when(projectMapper.selectById(40L)).thenReturn(project); + + Logger logger = (Logger) LoggerFactory.getLogger(CcdiProjectServiceImpl.class); + ListAppender logAppender = new ListAppender<>(); + logAppender.start(); + logger.addAppender(logAppender); + + try { + service.updateProjectStatus(40L, "3", "tester"); + + assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage) + .anyMatch(message -> message.contains("项目状态变更") + && message.contains("projectId=40") + && message.contains("projectName=专案B") + && message.contains("oldStatus=0") + && message.contains("newStatus=3") + && message.contains("operator=tester"))); + } finally { + logger.detachAppender(logAppender); + } + } + + @Test + void shouldNotLogProjectStatusTransitionWhenStatusDoesNotChange() { + CcdiProject project = new CcdiProject(); + project.setProjectId(40L); + project.setProjectName("专案C"); + project.setStatus("3"); + when(projectMapper.selectById(40L)).thenReturn(project); + + Logger logger = (Logger) LoggerFactory.getLogger(CcdiProjectServiceImpl.class); + ListAppender logAppender = new ListAppender<>(); + logAppender.start(); + logger.addAppender(logAppender); + + try { + service.updateProjectStatus(40L, "3", "tester"); + + assertFalse(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage) + .anyMatch(message -> message.contains("项目状态变更") + && message.contains("projectId=40"))); + } finally { + logger.detachAppender(logAppender); + } + } + + private GetTokenResponse buildTokenResponse(Integer projectId) { + GetTokenResponse response = new GetTokenResponse(); + response.setCode("200"); + + GetTokenResponse.TokenData data = new GetTokenResponse.TokenData(); + data.setProjectId(projectId); + response.setData(data); + return response; + } } diff --git a/docs/plans/backend/2026-03-18-项目状态变更日志实施计划.md b/docs/plans/backend/2026-03-18-项目状态变更日志实施计划.md new file mode 100644 index 00000000..0609b070 --- /dev/null +++ b/docs/plans/backend/2026-03-18-项目状态变更日志实施计划.md @@ -0,0 +1,84 @@ +# 项目状态变更日志实施计划 + +> **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:** 统一在 `CcdiProjectServiceImpl` 中收口项目状态日志。项目创建时记录初始状态日志,后续通过 `updateProjectStatus` 处理的状态变更统一记录“变更前/变更后/操作人”日志,并在状态未变化时避免重复输出。 + +**Tech Stack:** Java 21、Spring Boot 3、MyBatis Plus、SLF4J/Logback、JUnit 5、Mockito + +--- + +### Task 1: 明确状态变更入口 + +**Files:** +- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java` +- Check: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java` + +- [ ] **Step 1: 盘点项目状态变更入口** + +确认项目状态仅在项目创建默认置为“进行中”以及 `updateProjectStatus` 方法中发生持久化变更。 + +- [ ] **Step 2: 确认日志收口位置** + +确保打标流程等调用方继续复用 `updateProjectStatus`,不在调用方分散新增重复日志。 + +### Task 2: 先补失败测试 + +**Files:** +- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java` + +- [ ] **Step 1: 为项目创建补日志测试** + +编写测试验证创建项目成功后输出初始状态日志,日志内容包含 `projectId`、`projectName`、状态和值。 + +- [ ] **Step 2: 为状态切换补日志测试** + +编写测试验证 `updateProjectStatus` 在状态真实变化时输出状态变更日志,并包含旧状态、新状态、操作人。 + +- [ ] **Step 3: 为重复状态写入补日志约束测试** + +编写测试验证当目标状态与当前状态一致时,不重复输出状态变更日志。 + +- [ ] **Step 4: 运行单测确认先失败** + +Run: `mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest test` + +Expected: 新增日志相关断言失败,证明测试覆盖到新增行为。 + +### Task 3: 实现统一日志 + +**Files:** +- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java` + +- [ ] **Step 1: 为服务类补日志能力** + +引入 `@Slf4j` 或等效 Logger,保持与模块现有日志风格一致。 + +- [ ] **Step 2: 在项目创建后记录初始状态** + +在项目持久化成功后输出“项目状态初始化”日志。 + +- [ ] **Step 3: 在状态变更时记录统一日志** + +在 `updateProjectStatus` 中记录“项目状态变更”日志,打印项目标识、项目名称、旧状态、新状态、操作人。 + +- [ ] **Step 4: 避免无效重复日志** + +当旧状态与新状态一致时,不输出状态变更日志,但保留现有更新时间写入行为。 + +### Task 4: 补实施记录并验证 + +**Files:** +- Create: `docs/reports/implementation/2026-03-18-项目状态变更日志实施记录.md` + +- [ ] **Step 1: 记录本次实施内容** + +补充实施记录,说明状态日志覆盖范围、代码修改点和测试结果。 + +- [ ] **Step 2: 运行最终验证** + +Run: `mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest,CcdiBankTagServiceImplTest test` + +Expected: 相关测试全部通过,确认状态日志不影响现有打标状态流转。 diff --git a/docs/reports/implementation/2026-03-18-项目状态变更日志实施记录.md b/docs/reports/implementation/2026-03-18-项目状态变更日志实施记录.md new file mode 100644 index 00000000..d5430199 --- /dev/null +++ b/docs/reports/implementation/2026-03-18-项目状态变更日志实施记录.md @@ -0,0 +1,26 @@ +# 项目状态变更日志实施记录 + +## 变更背景 + +根据需求,为项目状态发生变更的所有后端入口补充日志,便于排查项目生命周期中的状态切换过程。 + +## 实施内容 + +1. 在 `CcdiProjectServiceImpl` 中新增统一项目状态日志能力。 +2. 项目创建成功后,记录项目初始状态日志,覆盖“默认进入进行中”场景。 +3. 在 `updateProjectStatus` 中记录项目状态变更日志,输出项目ID、项目名称、变更前状态、变更后状态、操作人。 +4. 当状态未发生变化时,不重复输出“项目状态变更”日志,避免无效噪音。 +5. 在 `CcdiProjectServiceImplTest` 中补充日志相关单测,覆盖创建、状态变化、状态不变化三种场景。 + +## 涉及文件 + +- `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` +- `docs/plans/backend/2026-03-18-项目状态变更日志实施计划.md` + +## 验证情况 + +- `mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest test` + - 结果:通过 +- `mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest,CcdiBankTagServiceImplTest test` + - 结果:通过