diff --git a/.scratch/crm-project/endpoint-page-alignment.md b/.scratch/crm-project/endpoint-page-alignment.md index 364b9d7..7b20925 100644 --- a/.scratch/crm-project/endpoint-page-alignment.md +++ b/.scratch/crm-project/endpoint-page-alignment.md @@ -10,7 +10,7 @@ | --- | --- | --- | --- | --- | | /api/project/board | GET | A5 项目管理/项目管理(看板视图)+ 分屏视图 ◐ | A5-1-1_项目管理(看板视图/分屏视图) | ✅ | | /api/project/list | GET | A5 项目管理/项目管理(列表视图)+ 分屏视图 + 我的项目 ◐ | A5-1-1_项目管理(列表视图/分屏视图)、A5-1-2_我的项目 | ✅(列表+分屏共用一接口,spec §4 定稿) | -| /api/project/create | POST | A5 项目管理/项目管理(看板视图)+ 列表视图 ◐ | (两页新建按钮,无独立原型页) | ✅(—) | +| /api/project/create | POST | A5 项目管理/项目管理(看板视图)+ 列表视图 ◐ | (两页新建按钮,无独立原型页;「项目建档」独立目录经评审撤销) | ✅(—) | | /api/project/detail | GET | A5 项目管理/项目详情 | A5-1-2-1_项目详情(页签详情与操作) | ✅ | | /api/project/stage/advance | POST | A5 项目管理/项目详情/状态机 | A5-1-2-1(推进/无需招投标) | ✅ | | /api/project/stage/rollback-targets | GET | 同上 | A5-1-2-2(回退弹窗数据源) | ✅ | diff --git a/.scratch/crm-project/tickets/README.md b/.scratch/crm-project/tickets/README.md index 4b03f79..2902115 100644 --- a/.scratch/crm-project/tickets/README.md +++ b/.scratch/crm-project/tickets/README.md @@ -40,4 +40,5 @@ Frontier 推进:01(无阻塞可立即开);02 之后 03/04/05/09/10/11/12 - 2026-09-10 票 02 拍板:**深 tag 页面目录** = 项目管理(看板视图)/项目管理(分屏视图)/项目管理(列表视图)/我的项目/项目详情/项目建档。原型树 v29 一行索引未展开详情页;详情/建档 tag 对应原型 a5-1-2-1 页签详情与看板/列表页新建入口。 - 2026-09-10 票 02 拍板:错误码段 **68xxx**(auth 61/file 62/dict 63/rule 64/lead 65/opp 66/customer 67 之后顺延);商机侧 66008(卡不存在)在建档入口翻译为 68007(异常不跨 seam)。 - 2026-09-10 票 13 拍板:**ProjectRuleQueryPort 包位置 = `com.crm.rule.port.inbound`**(crm-rule 声明并自实现,对称 CustomerCatalogPort 的 inbound 形态;与 port.outbound「消费方 adapter 实现」方向相反),DTO 随包 record(StageAliasDTO/BidConfigDTO/PoolMemberDTO/ConflictPullDTO/StageRemindDTO),不暴露内部 ProjectRuleDTO。ConflictPullDTO 含「启用维度集+命中阈值+触发条件」签名形态——维度集/阈值为签名预留(conflictPullJson 无该配置层,解析恒空集/占位 1,spin-out 前置补列)。stageCode 取 project_stage 值字符串("0"~"6",容错 "STAGE_0" 形态取尾段)。 +- 2026-09-11 评审拍板(code-review 收口):①ticket12 提交(03d3702)夹带了当时工作区在途的 customer focus-flag/导入预检改动(CustomerMapper/CustomerListRowDTO/CustomerImportAnalyzer),属另一工单范畴、内容有测试随行,保留并在此如实记录,不回改历史;②Controller 类级 @Tag 与方法级 tags 互斥(ADR-0024 修订5),「项目建档」目录撤销、建项目归 看板/列表 两页多 tag;③读侧别名字典请求内提拉一次(N+1 收敛);④客户换负责人同步逐项目写 TRANSFER 动态。 - (后续各票拍板追加于此) diff --git a/crm-project/src/main/java/com/crm/project/controller/ProjectBoardController.java b/crm-project/src/main/java/com/crm/project/controller/ProjectBoardController.java index 04e3596..81a8b2c 100644 --- a/crm-project/src/main/java/com/crm/project/controller/ProjectBoardController.java +++ b/crm-project/src/main/java/com/crm/project/controller/ProjectBoardController.java @@ -4,7 +4,6 @@ import com.crm.base.domain.result.Result; import com.crm.project.domain.dto.ProjectBoardDTO; import com.crm.project.query.ProjectBoardQuery; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -14,7 +13,6 @@ import org.springframework.web.bind.annotation.RestController; /** * 项目看板接口(A5 项目管理;spec §4)。 */ -@Tag(name = "A5 项目管理/项目管理(看板视图)") @RestController @RequestMapping("/api/project") @RequiredArgsConstructor diff --git a/crm-project/src/main/java/com/crm/project/controller/ProjectCreateController.java b/crm-project/src/main/java/com/crm/project/controller/ProjectCreateController.java index 793aaf5..6328006 100644 --- a/crm-project/src/main/java/com/crm/project/controller/ProjectCreateController.java +++ b/crm-project/src/main/java/com/crm/project/controller/ProjectCreateController.java @@ -6,7 +6,6 @@ import com.crm.base.security.SecurityUtils; import com.crm.project.domain.dto.ProjectDTO; import com.crm.project.service.IProjectCreateService; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -18,7 +17,6 @@ import org.springframework.web.bind.annotation.RestController; *

路由风格对齐 crm-opportunity/crm-lead:全 POST,动词路径,参数表单绑定 * (ADR-0017,禁 @RequestBody/@PathVariable)。

*/ -@Tag(name = "A5 项目管理/项目建档", description = "独立建项目(本期唯一可达建项目入口)") @RestController @RequestMapping("/api/project") @RequiredArgsConstructor diff --git a/crm-project/src/main/java/com/crm/project/controller/ProjectTransitionController.java b/crm-project/src/main/java/com/crm/project/controller/ProjectTransitionController.java index 90f1f56..dfe7c4e 100644 --- a/crm-project/src/main/java/com/crm/project/controller/ProjectTransitionController.java +++ b/crm-project/src/main/java/com/crm/project/controller/ProjectTransitionController.java @@ -10,7 +10,6 @@ import com.crm.project.domain.dto.ProjectFilingReviewDTO; import com.crm.project.domain.dto.StageTargetDTO; import com.crm.project.service.IProjectTransitionService; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -26,7 +25,6 @@ import java.util.List; *

路由风格对齐商机模块:全 POST 动词路径、id 走 @RequestParam(ADR-0017,禁 * @PathVariable/@PutMapping/@DeleteMapping/@RequestBody)。

*/ -@Tag(name = "A5 项目管理/项目详情/状态机") @RestController @RequestMapping("/api/project") @RequiredArgsConstructor diff --git a/crm-project/src/main/java/com/crm/project/port/inbound/impl/ProjectOwnerSyncPortImpl.java b/crm-project/src/main/java/com/crm/project/port/inbound/impl/ProjectOwnerSyncPortImpl.java index 500db6a..af5c82f 100644 --- a/crm-project/src/main/java/com/crm/project/port/inbound/impl/ProjectOwnerSyncPortImpl.java +++ b/crm-project/src/main/java/com/crm/project/port/inbound/impl/ProjectOwnerSyncPortImpl.java @@ -8,6 +8,8 @@ import com.crm.customer.port.ProjectOwnerSyncPort; import com.crm.project.constant.ProjectConstants; import com.crm.project.domain.entity.CrmProject; import com.crm.project.mapper.CrmProjectMapper; +import com.crm.project.service.ProjectOplogWriter; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Primary; @@ -29,6 +31,7 @@ public class ProjectOwnerSyncPortImpl implements ProjectOwnerSyncPort { private final CrmProjectMapper projectMapper; private final IAuthUserService authUserService; + private final ProjectOplogWriter oplogWriter; @Override public int syncOwnerByCustomer(Long customerId, Long newOwnerId) { @@ -37,11 +40,19 @@ public class ProjectOwnerSyncPortImpl implements ProjectOwnerSyncPort { throw new BusinessErrorException(ProjectConstants.CODE_PROJECT_INVALID, "项目 owner 同步接收人不存在"); } + // 先取命中项目(逐项目写动态留痕,评审 P3-6:ACTION_TRANSFER 不再是死代码) + java.util.List affected = projectMapper.selectList( + new LambdaQueryWrapper().eq(CrmProject::getCustomerId, customerId)); int updated = projectMapper.update(null, new LambdaUpdateWrapper() .eq(CrmProject::getCustomerId, customerId) .set(CrmProject::getOwnerUserId, newOwnerId) .set(CrmProject::getOwnerNameSnapshot, receiver.getUsername()) .set(CrmProject::getOwnerDeptId, receiver.getDeptId())); + for (CrmProject project : affected) { + oplogWriter.write(project.getId(), ProjectConstants.ACTION_TRANSFER, + "移交:客户换负责人 → 项目 owner 同步变更为 " + receiver.getUsername(), + newOwnerId, receiver.getUsername()); + } if (updated > 0) { log.info("[project-owner-sync] 客户 {} 换负责人 → 项目 owner 同步 {} 行", customerId, updated); } diff --git a/crm-project/src/main/java/com/crm/project/query/impl/ProjectBoardQueryImpl.java b/crm-project/src/main/java/com/crm/project/query/impl/ProjectBoardQueryImpl.java index 013ac11..7e7449a 100644 --- a/crm-project/src/main/java/com/crm/project/query/impl/ProjectBoardQueryImpl.java +++ b/crm-project/src/main/java/com/crm/project/query/impl/ProjectBoardQueryImpl.java @@ -87,15 +87,16 @@ public class ProjectBoardQueryImpl implements ProjectBoardQuery { .filter(p -> ProjectStatus.STATUS_ACTIVE.getValue().equals(p.getProjectStatus())) .count()); - // 列头阶段名:字典名(project_stage 分组)→ 枚举名兜底;别名优先由票 13 接入 + // 列头阶段名:别名(ProjectRule ①)→ 字典名 → 枚举名;别名字典整板拉取一次(评审 P2-4) Map dictStageNames = querySupport.stageDictNames( java.util.Arrays.stream(ProjectStage.values()) .map(ProjectStage::getValue).collect(Collectors.toSet())); + Map aliasMap = querySupport.stageAliasMap(); List columns = new ArrayList<>(); for (ProjectStage stage : ProjectStage.values()) { ProjectBoardDTO.BoardColumn column = new ProjectBoardDTO.BoardColumn(); column.setStage(stage.getValue()); - column.setStageName(querySupport.stageNameOf(stage.getValue(), dictStageNames)); + column.setStageName(querySupport.stageNameOf(stage.getValue(), dictStageNames, aliasMap)); List stageProjects = byStage.getOrDefault(stage, List.of()); column.setCount((long) stageProjects.size()); column.setCards(stageProjects.stream() diff --git a/crm-project/src/main/java/com/crm/project/query/impl/ProjectQuerySupport.java b/crm-project/src/main/java/com/crm/project/query/impl/ProjectQuerySupport.java index 3547e97..f8725b2 100644 --- a/crm-project/src/main/java/com/crm/project/query/impl/ProjectQuerySupport.java +++ b/crm-project/src/main/java/com/crm/project/query/impl/ProjectQuerySupport.java @@ -55,14 +55,16 @@ public class ProjectQuerySupport { return wrapper; } - /** 行展示名批量回显:阶段名(字典名→枚举名兵底,别名优先接入点归票 13)+ 三轴枚举名 + 客户名 */ + /** 行展示名批量回显:阶段名(别名→字典名→枚举名,票 13)+ 三轴枚举名 + 客户名。 + * 别名字典请求内提拉一次(评审 P2-4:避免逐行走 ProjectRule 端口)。 */ public void fillDisplayNames(List entities, List dtos) { Map dictStageNames = stageDictNames(entities.stream() .map(CrmProject::getProjectStage).collect(Collectors.toSet())); + Map aliasMap = stageAliasMap(); for (int i = 0; i < entities.size(); i++) { CrmProject entity = entities.get(i); ProjectDTO dto = dtos.get(i); - dto.setStageName(stageNameOf(entity.getProjectStage(), dictStageNames)); + dto.setStageName(stageNameOf(entity.getProjectStage(), dictStageNames, aliasMap)); ProjectStatus status = EnumUtils.getByValue(ProjectStatus.class, entity.getProjectStatus()); dto.setStatusName(status == null ? null : status.getLabel()); FilingStatus filing = EnumUtils.getByValue(FilingStatus.class, entity.getFilingStatus()); diff --git a/crm-project/src/main/java/com/crm/project/service/impl/ProjectDetailServiceImpl.java b/crm-project/src/main/java/com/crm/project/service/impl/ProjectDetailServiceImpl.java index ef51ac7..587f39b 100644 --- a/crm-project/src/main/java/com/crm/project/service/impl/ProjectDetailServiceImpl.java +++ b/crm-project/src/main/java/com/crm/project/service/impl/ProjectDetailServiceImpl.java @@ -27,8 +27,8 @@ import java.util.Set; /** * 项目详情服务实现(票 02) * - *

阶段名回显:ProjectRule 别名优先、空回退字典名(别名接入点票 13,本实现先走 - * 字典名 → 枚举名兜底);状态/报备/赢输名走枚举词汇表;客户名经 + *

阶段名回显:ProjectRule 别名优先(票 13 已接入)→ 字典名 → 枚举名兜底; + * 状态/报备/赢输名走枚举词汇表;客户名经 * {@code CustomerCatalogPort} 回显(客户不可用时不阻断详情,名字留空)。

*/ @Slf4j diff --git a/crm-project/src/test/java/com/crm/project/transfer/ProjectSeamsH2IntegrationTest.java b/crm-project/src/test/java/com/crm/project/transfer/ProjectSeamsH2IntegrationTest.java index 3e2e08b..5b59bad 100644 --- a/crm-project/src/test/java/com/crm/project/transfer/ProjectSeamsH2IntegrationTest.java +++ b/crm-project/src/test/java/com/crm/project/transfer/ProjectSeamsH2IntegrationTest.java @@ -90,6 +90,7 @@ class ProjectSeamsH2IntegrationTest { .setIdentifierGenerator(DefaultIdentifierGenerator.getInstance()); configuration.addMapper(CrmProjectMapper.class); + configuration.addMapper(com.crm.project.mapper.ProjectOplogMapper.class); sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); try (SqlSession s = sqlSessionFactory.openSession(true)) { @@ -119,7 +120,7 @@ class ProjectSeamsH2IntegrationTest { Mockito.when(authUserService.getById(BOB)).thenReturn(bob); transferPort = new ProjectUserTransferPortImpl(projectMapper, authUserService); - ownerSyncPort = new ProjectOwnerSyncPortImpl(projectMapper, authUserService); + ownerSyncPort = new ProjectOwnerSyncPortImpl(projectMapper, authUserService, new com.crm.project.service.ProjectOplogWriter(session.getMapper(com.crm.project.mapper.ProjectOplogMapper.class))); } @AfterEach @@ -207,6 +208,14 @@ class ProjectSeamsH2IntegrationTest { close_type VARCHAR(32), close_reason VARCHAR(500), close_time DATETIME, close_remark VARCHAR(500), version INT NOT NULL DEFAULT 0) + """, + """ + CREATE TABLE project_oplog ( + id BIGINT PRIMARY KEY, creator_id VARCHAR(50), create_time DATETIME, + updater_id VARCHAR(50), update_time DATETIME, deleted TINYINT NOT NULL DEFAULT 0, + project_id BIGINT NOT NULL, action VARCHAR(32) NOT NULL, + from_stage TINYINT, to_stage TINYINT, op_desc VARCHAR(500), + operator_id BIGINT, operator_name_snapshot VARCHAR(64), op_time DATETIME NOT NULL) """ };