Browse Source

评审收口: 类级@Tag与方法级tags互斥归正(ADR-0024)/读侧别名字典提拉一次(N+1)/客户换负责人同步逐项目写 TRANSFER 动态/过时注释刷新/对齐表与 Decisions 补记

master
luoweijian 14 hours ago
parent
commit
c9d6e75508
  1. 2
      .scratch/crm-project/endpoint-page-alignment.md
  2. 1
      .scratch/crm-project/tickets/README.md
  3. 2
      crm-project/src/main/java/com/crm/project/controller/ProjectBoardController.java
  4. 2
      crm-project/src/main/java/com/crm/project/controller/ProjectCreateController.java
  5. 2
      crm-project/src/main/java/com/crm/project/controller/ProjectTransitionController.java
  6. 11
      crm-project/src/main/java/com/crm/project/port/inbound/impl/ProjectOwnerSyncPortImpl.java
  7. 5
      crm-project/src/main/java/com/crm/project/query/impl/ProjectBoardQueryImpl.java
  8. 6
      crm-project/src/main/java/com/crm/project/query/impl/ProjectQuerySupport.java
  9. 4
      crm-project/src/main/java/com/crm/project/service/impl/ProjectDetailServiceImpl.java
  10. 11
      crm-project/src/test/java/com/crm/project/transfer/ProjectSeamsH2IntegrationTest.java

2
.scratch/crm-project/endpoint-page-alignment.md

@ -10,7 +10,7 @@
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| /api/project/board | GET | A5 项目管理/项目管理(看板视图)+ 分屏视图 ◐ | A5-1-1_项目管理(看板视图/分屏视图) | ✅ | | /api/project/board | GET | A5 项目管理/项目管理(看板视图)+ 分屏视图 ◐ | A5-1-1_项目管理(看板视图/分屏视图) | ✅ |
| /api/project/list | GET | A5 项目管理/项目管理(列表视图)+ 分屏视图 + 我的项目 ◐ | A5-1-1_项目管理(列表视图/分屏视图)、A5-1-2_我的项目 | ✅(列表+分屏共用一接口,spec §4 定稿) | | /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/detail | GET | A5 项目管理/项目详情 | A5-1-2-1_项目详情(页签详情与操作) | ✅ |
| /api/project/stage/advance | POST | 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(回退弹窗数据源) | ✅ | | /api/project/stage/rollback-targets | GET | 同上 | A5-1-2-2(回退弹窗数据源) | ✅ |

1
.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 拍板:**深 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 票 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-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 动态。
- (后续各票拍板追加于此) - (后续各票拍板追加于此)

2
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.domain.dto.ProjectBoardDTO;
import com.crm.project.query.ProjectBoardQuery; import com.crm.project.query.ProjectBoardQuery;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -14,7 +13,6 @@ import org.springframework.web.bind.annotation.RestController;
/** /**
* 项目看板接口A5 项目管理spec §4 * 项目看板接口A5 项目管理spec §4
*/ */
@Tag(name = "A5 项目管理/项目管理(看板视图)")
@RestController @RestController
@RequestMapping("/api/project") @RequestMapping("/api/project")
@RequiredArgsConstructor @RequiredArgsConstructor

2
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.domain.dto.ProjectDTO;
import com.crm.project.service.IProjectCreateService; import com.crm.project.service.IProjectCreateService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -18,7 +17,6 @@ import org.springframework.web.bind.annotation.RestController;
* <p>路由风格对齐 crm-opportunity/crm-lead POST动词路径参数表单绑定 * <p>路由风格对齐 crm-opportunity/crm-lead POST动词路径参数表单绑定
* ADR-0017 @RequestBody/@PathVariable</p> * ADR-0017 @RequestBody/@PathVariable</p>
*/ */
@Tag(name = "A5 项目管理/项目建档", description = "独立建项目(本期唯一可达建项目入口)")
@RestController @RestController
@RequestMapping("/api/project") @RequestMapping("/api/project")
@RequiredArgsConstructor @RequiredArgsConstructor

2
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.domain.dto.StageTargetDTO;
import com.crm.project.service.IProjectTransitionService; import com.crm.project.service.IProjectTransitionService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -26,7 +25,6 @@ import java.util.List;
* <p>路由风格对齐商机模块 POST 动词路径id @RequestParamADR-0017 * <p>路由风格对齐商机模块 POST 动词路径id @RequestParamADR-0017
* @PathVariable/@PutMapping/@DeleteMapping/@RequestBody</p> * @PathVariable/@PutMapping/@DeleteMapping/@RequestBody</p>
*/ */
@Tag(name = "A5 项目管理/项目详情/状态机")
@RestController @RestController
@RequestMapping("/api/project") @RequestMapping("/api/project")
@RequiredArgsConstructor @RequiredArgsConstructor

11
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.constant.ProjectConstants;
import com.crm.project.domain.entity.CrmProject; import com.crm.project.domain.entity.CrmProject;
import com.crm.project.mapper.CrmProjectMapper; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
@ -29,6 +31,7 @@ public class ProjectOwnerSyncPortImpl implements ProjectOwnerSyncPort {
private final CrmProjectMapper projectMapper; private final CrmProjectMapper projectMapper;
private final IAuthUserService authUserService; private final IAuthUserService authUserService;
private final ProjectOplogWriter oplogWriter;
@Override @Override
public int syncOwnerByCustomer(Long customerId, Long newOwnerId) { public int syncOwnerByCustomer(Long customerId, Long newOwnerId) {
@ -37,11 +40,19 @@ public class ProjectOwnerSyncPortImpl implements ProjectOwnerSyncPort {
throw new BusinessErrorException(ProjectConstants.CODE_PROJECT_INVALID, throw new BusinessErrorException(ProjectConstants.CODE_PROJECT_INVALID,
"项目 owner 同步接收人不存在"); "项目 owner 同步接收人不存在");
} }
// 先取命中项目(逐项目写动态留痕,评审 P3-6:ACTION_TRANSFER 不再是死代码)
java.util.List<CrmProject> affected = projectMapper.selectList(
new LambdaQueryWrapper<CrmProject>().eq(CrmProject::getCustomerId, customerId));
int updated = projectMapper.update(null, new LambdaUpdateWrapper<CrmProject>() int updated = projectMapper.update(null, new LambdaUpdateWrapper<CrmProject>()
.eq(CrmProject::getCustomerId, customerId) .eq(CrmProject::getCustomerId, customerId)
.set(CrmProject::getOwnerUserId, newOwnerId) .set(CrmProject::getOwnerUserId, newOwnerId)
.set(CrmProject::getOwnerNameSnapshot, receiver.getUsername()) .set(CrmProject::getOwnerNameSnapshot, receiver.getUsername())
.set(CrmProject::getOwnerDeptId, receiver.getDeptId())); .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) { if (updated > 0) {
log.info("[project-owner-sync] 客户 {} 换负责人 → 项目 owner 同步 {} 行", customerId, updated); log.info("[project-owner-sync] 客户 {} 换负责人 → 项目 owner 同步 {} 行", customerId, updated);
} }

5
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())) .filter(p -> ProjectStatus.STATUS_ACTIVE.getValue().equals(p.getProjectStatus()))
.count()); .count());
// 列头阶段名:字典名(project_stage 分组)→ 枚举名兜底;别名优先由票 13 接入 // 列头阶段名:别名(ProjectRule ①)→ 字典名 → 枚举名;别名字典整板拉取一次(评审 P2-4)
Map<String, String> dictStageNames = querySupport.stageDictNames( Map<String, String> dictStageNames = querySupport.stageDictNames(
java.util.Arrays.stream(ProjectStage.values()) java.util.Arrays.stream(ProjectStage.values())
.map(ProjectStage::getValue).collect(Collectors.toSet())); .map(ProjectStage::getValue).collect(Collectors.toSet()));
Map<String, String> aliasMap = querySupport.stageAliasMap();
List<ProjectBoardDTO.BoardColumn> columns = new ArrayList<>(); List<ProjectBoardDTO.BoardColumn> columns = new ArrayList<>();
for (ProjectStage stage : ProjectStage.values()) { for (ProjectStage stage : ProjectStage.values()) {
ProjectBoardDTO.BoardColumn column = new ProjectBoardDTO.BoardColumn(); ProjectBoardDTO.BoardColumn column = new ProjectBoardDTO.BoardColumn();
column.setStage(stage.getValue()); column.setStage(stage.getValue());
column.setStageName(querySupport.stageNameOf(stage.getValue(), dictStageNames)); column.setStageName(querySupport.stageNameOf(stage.getValue(), dictStageNames, aliasMap));
List<CrmProject> stageProjects = byStage.getOrDefault(stage, List.of()); List<CrmProject> stageProjects = byStage.getOrDefault(stage, List.of());
column.setCount((long) stageProjects.size()); column.setCount((long) stageProjects.size());
column.setCards(stageProjects.stream() column.setCards(stageProjects.stream()

6
crm-project/src/main/java/com/crm/project/query/impl/ProjectQuerySupport.java

@ -55,14 +55,16 @@ public class ProjectQuerySupport {
return wrapper; return wrapper;
} }
/** 行展示名批量回显:阶段名(字典名→枚举名兵底,别名优先接入点归票 13)+ 三轴枚举名 + 客户名 */ /** 行展示名批量回显阶段名别名字典名枚举名 13+ 三轴枚举名 + 客户名
* 别名字典请求内提拉一次评审 P2-4避免逐行走 ProjectRule 端口 */
public void fillDisplayNames(List<CrmProject> entities, List<ProjectDTO> dtos) { public void fillDisplayNames(List<CrmProject> entities, List<ProjectDTO> dtos) {
Map<String, String> dictStageNames = stageDictNames(entities.stream() Map<String, String> dictStageNames = stageDictNames(entities.stream()
.map(CrmProject::getProjectStage).collect(Collectors.toSet())); .map(CrmProject::getProjectStage).collect(Collectors.toSet()));
Map<String, String> aliasMap = stageAliasMap();
for (int i = 0; i < entities.size(); i++) { for (int i = 0; i < entities.size(); i++) {
CrmProject entity = entities.get(i); CrmProject entity = entities.get(i);
ProjectDTO dto = dtos.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()); ProjectStatus status = EnumUtils.getByValue(ProjectStatus.class, entity.getProjectStatus());
dto.setStatusName(status == null ? null : status.getLabel()); dto.setStatusName(status == null ? null : status.getLabel());
FilingStatus filing = EnumUtils.getByValue(FilingStatus.class, entity.getFilingStatus()); FilingStatus filing = EnumUtils.getByValue(FilingStatus.class, entity.getFilingStatus());

4
crm-project/src/main/java/com/crm/project/service/impl/ProjectDetailServiceImpl.java

@ -27,8 +27,8 @@ import java.util.Set;
/** /**
* 项目详情服务实现 02 * 项目详情服务实现 02
* *
* <p>阶段名回显ProjectRule 别名优先空回退字典名别名接入点票 13本实现先走 * <p>阶段名回显ProjectRule 别名优先 13 已接入 字典名 枚举名兜底
* 字典名 枚举名兜底状态/报备/赢输名走枚举词汇表客户名经 * 状态/报备/赢输名走枚举词汇表客户名经
* {@code CustomerCatalogPort} 回显客户不可用时不阻断详情名字留空</p> * {@code CustomerCatalogPort} 回显客户不可用时不阻断详情名字留空</p>
*/ */
@Slf4j @Slf4j

11
crm-project/src/test/java/com/crm/project/transfer/ProjectSeamsH2IntegrationTest.java

@ -90,6 +90,7 @@ class ProjectSeamsH2IntegrationTest {
.setIdentifierGenerator(DefaultIdentifierGenerator.getInstance()); .setIdentifierGenerator(DefaultIdentifierGenerator.getInstance());
configuration.addMapper(CrmProjectMapper.class); configuration.addMapper(CrmProjectMapper.class);
configuration.addMapper(com.crm.project.mapper.ProjectOplogMapper.class);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
try (SqlSession s = sqlSessionFactory.openSession(true)) { try (SqlSession s = sqlSessionFactory.openSession(true)) {
@ -119,7 +120,7 @@ class ProjectSeamsH2IntegrationTest {
Mockito.when(authUserService.getById(BOB)).thenReturn(bob); Mockito.when(authUserService.getById(BOB)).thenReturn(bob);
transferPort = new ProjectUserTransferPortImpl(projectMapper, authUserService); 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 @AfterEach
@ -207,6 +208,14 @@ class ProjectSeamsH2IntegrationTest {
close_type VARCHAR(32), close_reason VARCHAR(500), close_time DATETIME, close_type VARCHAR(32), close_reason VARCHAR(500), close_time DATETIME,
close_remark VARCHAR(500), close_remark VARCHAR(500),
version INT NOT NULL DEFAULT 0) 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)
""" """
}; };

Loading…
Cancel
Save