Appearance
hc-crm-center 项目结构与新手上手指南
适用项目:
/Users/admin/code/hc-project/api-java/hc-crm-center
分析日期:2026-07-16
Git 基线:1b6b88e35e6b38ad3d893f25e64a6141c5652729
项目规模:595 个有效项目文件,其中 541 个 Java 文件、49 个配置/XML/JSON/YAML 文件、4 个 SQL 文件、1 个说明文档
图谱结果:1,861 个节点、3,632 条关系、8 个架构层、14 步推荐阅读路线
构建验证:本机执行mvn -DskipTests validate时被公司 Maven 私服401阻断,因此未完成源码编译验证;这属于凭证/权限问题,不代表当前源码编译通过或失败。
1. 先用一句话理解项目
hc-crm-center 是欢创客户管理中心后端服务,负责把客户从基础资料录入,推进到产品跟进、公海保护、项目评估、合同审批和试单申请,并向租户、认证、流程、系统、集成和文件等内部中心获取支撑能力。
项目的主要业务链可以概括为:
text
客户资料
-> 产品跟进与跟进记录
-> 客户保护 / 转移 / 分享 / 公海认领
-> 项目评估或试单申请
-> 合同创建与审批
-> 首单签约日期、跟进阶段和截止时间联动它不是一个脱离公司基础设施即可独立运行的示例项目。项目依赖欢创内部父 POM、Starter、Maven 私服、网关上下文、租户与用户上下文,以及认证、租户、流程、系统、集成、支持中心等内部服务。
2. 技术栈速览
| 类别 | 项目中的实际技术 |
|---|---|
| 语言与构建 | Java 8、Maven 多模块 |
| Web | Spring Boot、Spring MVC、内部 hc-starter-web |
| 微服务 | 服务发现、内部 HC Feign、@HcApiClient |
| 持久化 | MyBatis-Plus、动态 SqlProvider、Mapper XML、PostgreSQL |
| 多租户 | TenantContextHolder、MyBatis 租户插件、@IgnoreTenant |
| 数据权限 | DataPermissionServer、DataPermissionQueryUtil、资源编码 |
| 缓存与锁 | 内部 hc-starter-cache、hc-starter-lock |
| 调度 | XXL-Job、Spring Scheduling |
| 异步 | Spring @EnableAsync、导入异步任务 |
| 文件 | 动态 OSS、文件预览 API、业务附件表 |
| 工作流 | 流程中心 API、BPM 发起与回调 |
| Excel | EasyExcel 风格导入处理、动态下拉、失败结果文件 |
| 对象处理 | Lombok、手写 Converter、Orika、内部 ObjectUtils |
| 接口文档 | Swagger 2 注解 |
| 测试与覆盖率 | Maven Surefire、JaCoCo;当前没有 src/test 自动化测试源码 |
Spring Boot Maven 插件在 Service 模块中固定为 2.3.0.RELEASE,但实际依赖版本主要由内部父 POM hc-boot-starter 管理,不能只根据子模块 POM 判断完整版本树。
3. Maven 模块与职责
text
hc-crm-center/
├── pom.xml
├── README.md
├── hc-crm-center-api/
│ ├── pom.xml
│ └── src/main/java/com/hc/crm/
│ ├── api/
│ ├── enums/
│ ├── form/
│ └── vo/
└── hc-crm-center-service/
├── pom.xml
└── src/main/
├── java/com/hc/crm/
└── resources/
├── application.yml
├── mapper/
├── sql/
└── static/region.json3.1 根模块 hc-crm-center
根 pom.xml 是 packaging=pom 的聚合模块,负责:
- 聚合
hc-crm-center-api和hc-crm-center-service; - 指定 Java 8;
- 继承
com.hcframework.boot:hc-boot-starter:1.0.0-SNAPSHOT; - 配置公司阿里云 Maven 私服和发布仓库。
构建一开始就报父 POM 无法解析时,通常先检查公司网络、Maven settings.xml 和私服账号,而不是修改业务代码。
3.2 hc-crm-center-api
API 模块有 160 个 Java 文件,负责可复用契约:
| 目录 | 文件数 | 职责 |
|---|---|---|
api | 1 | 对外或跨服务 API Client 契约 |
enums | 42 | 跟进阶段、合同状态、审批状态、客户来源等公共业务枚举 |
form | 68 | Controller 入参、批量操作和流程回调参数 |
vo | 49 | 详情、列表、树结构和跨模块返回对象 |
这个模块会被 Service 模块和其他微服务依赖,因此修改 Form、VO、枚举或 API Client 时要考虑向后兼容:
- 不要随意删除字段或枚举值;
- 新字段优先保持可选并给出默认行为;
- 枚举编码一旦入库或对外传输,不要重新编号;
- 跨服务对象的包名和序列化格式也是接口契约的一部分。
3.3 hc-crm-center-service
Service 模块是实际运行服务,共 381 个 Java 文件。主要包如下:
| 包 | 文件数 | 主要职责 |
|---|---|---|
controller | 35 | Web 接口、内部 API、BPM 回调 |
service | 90 | 业务接口与实现类 |
entity | 44 | MyBatis-Plus 数据实体 |
mapper | 44 | Mapper 接口 |
provider | 45 | 动态分页和联表 SQL Provider |
converter | 44 | Form、DTO、Entity、VO 转换 |
dto | 48 | Service 内部编排对象 |
component | 9 | 认证、租户、流程、系统和集成中心适配 |
imports | 10 | Excel 导入 DTO、Converter、Processor |
util | 7 | 权限、Excel、校验、地区、变更和流程工具 |
handler | 2 | PostgreSQL 数组等类型处理 |
job | 1 | 公海掉入 XXL-Job |
4. 运行时架构
mermaid
flowchart LR
Client[前端或调用方] --> Gateway[网关、认证、租户上下文]
Gateway --> Controller[Web/API Controller]
Controller --> Service[业务 Service]
Service --> Permission[数据权限组件]
Service --> Mapper[Mapper / Provider]
Mapper --> PG[(PostgreSQL)]
Service --> Auth[认证中心]
Service --> Tenant[租户中心]
Service --> Process[流程中心]
Service --> System[系统中心]
Service --> Integrated[集成中心]
Service --> Support[支持/文件中心]
Service --> OSS[(动态 OSS)]
Service --> Cache[(缓存与分布式锁)]
Job[XXL-Job 公海任务] --> Service
Import[Excel 异步导入] --> Service典型同步 HTTP 请求遵循:
text
Form
-> Controller
-> Converter
-> Service 接口
-> ServiceImpl
-> Mapper / Provider
-> PostgreSQL
-> VO 或 QueryResultModel
-> Result<T>复杂写操作还会在 ServiceImpl 中联动:
text
远程流程 / 租户成员 / 数据权限 / OSS 文件 / 多张业务子表 / 跟进期限规则5. 八个架构层
知识图谱把项目归为以下 8 层:
| 层 | 节点数 | 说明 |
|---|---|---|
| API 契约层 | 160 | API Client、Form、VO、枚举和公共传输定义 |
| 接口接入层 | 35 | Web Controller、内部 API 与审批回调 |
| 业务服务层 | 91 | Service 接口、ServiceImpl 和公海任务 |
| 集成与传输模型层 | 111 | 外部组件、DTO、Converter 和导入模型 |
| ORM 持久层 | 179 | Entity、Mapper、Provider、XML 和类型处理器 |
| 公共工具层 | 7 | 权限、Excel、校验、地区、变更和流程工具 |
| 数据库与基础数据层 | 95 | PostgreSQL/MySQL SQL、表节点、字典和地区数据 |
| 启动构建与项目支持层 | 7 | 启动类、POM、YAML、代码生成器和 README |
新人开发时应保持依赖方向:
text
Controller -> Service -> Mapper/Provider -> Entity/Database
| |
| +-> Component / Util / Remote API
+-> Form / VO / Enum避免出现:
- Controller 直接调用 Mapper;
- Entity 依赖 Controller 或 Form;
- Provider 承载业务判断;
- 公共 API 模块依赖 Service 实现模块;
- 为了复用而在多个层之间形成循环依赖。
6. 核心业务域怎么读
6.1 客户主数据
入口链路:
text
CustomerInfoController
-> CustomerInfoService
-> CustomerInfoServiceImpl
-> CustomerInfoMapper / CustomerInfoProvider
-> CustomerInfo客户主数据不是一张孤立表,详情和列表会聚合:
- 客户工商信息;
- 品牌和品牌负责人;
- 行业、地区;
- 联系人、银行和开票信息;
- 产品跟进、商务人员和最近跟进时间;
- 合同和成交产品;
- 附件及变更日志。
CustomerInfoServiceImpl 约 1,081 行,修改前要特别确认:名称去重、统一社会信用代码、删除权限、关联数据清理、数据权限和首单签约日期联动。
6.2 产品跟进、保护与公海
核心类:
text
CustomerProductFollowController
CustomerProductFollowServiceImpl
CustomerFollowRecordServiceImpl
CustomerProductPublicPoolServiceImpl
CustomerProtectConfigurationServiceImpl
PublicPoolJob这个领域负责:
- 一个客户针对不同产品的跟进关系;
- 跟进人、跟进阶段、首次和最近跟进时间;
- 保护容量与保护截止时间;
- 分享、转移和认领;
- 掉入公海与再次认领;
- 跟进记录、评论和附件;
- 合同或审批事件引起的截止时间变化。
CustomerProductFollowServiceImpl 约 1,743 行,是全项目最大的业务类。它同时处理权限、规则、时间、状态和多表写入,新人不宜把第一个需求直接放在这里。
6.3 项目评估
项目评估把跟进中的机会推进到风险与交付可行性判断,包含:
- 服务需求;
- 人员类型;
- 结算规则;
- 垫资用途;
- 保险说明;
- 风险内容;
- 附件;
- BPM 流程发起和回调。
链路:
text
ProjectEvaluationController
-> ProjectEvaluationServiceImpl
-> 多个 ProjectEvaluation* 子服务
-> SaasBpmModelServer
-> 流程中心
-> ProjectEvaluationApiController.flowCallBack6.4 合同
合同模块包含合同主表、客户关系、联系人、结算规则、业务模式、垫资用途、编号序列和附件。
ContractServiceImpl 的关键职责:
- 验证客户和产品跟进资格;
- 生成合同编号;
- 保存主表及多张关联表;
- 发起流程审批;
- 处理流程回调;
- 在首份合同通过时回写客户签约日期;
- 调整产品跟进阶段和截止时间规则。
合同保存是典型的事务边界。数据库写入可以回滚,但已经成功的远程流程或文件操作不一定能随本地事务自动回滚,需要单独设计幂等和补偿。
6.5 试单申请
试单申请与项目评估、合同类似,也有:
- Form/DTO/Entity;
- 主表和产品关联表;
- Web Controller 与内部回调 Controller;
- 流程发起、审批状态和回调处理。
阅读 TrialOrderApplicationServiceImpl 可以学习一个规模相对可控的工作流业务实现。
6.6 Excel 批量导入
text
ImportRecordController
-> ImportRecordService
-> AbstractImportProcessor<T>
-> CustomerInfoImportProcessor
-> CustomerInfoService.saveBatchByImportAbstractImportProcessor 使用模板方法模式,统一处理:
- 文件与表头校验;
- 导入任务创建与进度更新;
- Bean Validation;
- 分批持久化;
- 成功、失败数量统计;
- 失败原因和结果文件;
- 模板下载与动态下拉。
异步导入必须显式关注用户和租户上下文传播,不能假设异步线程天然继承请求线程上下文。
7. 项目中的关键开发约定
7.1 Controller 保持薄层
Controller 主要做:
- 接收并校验 Form;
- 做少量快速前置判断;
- 调用 Converter;
- 调用 Service;
- 使用
Result.data(...)或Result.fail(...)返回; - 使用
@Log记录新增、编辑和删除操作。
复杂事务、跨表编排和权限判断应进入 ServiceImpl。
7.2 请求和返回对象分开
| 对象 | 位置 | 作用 |
|---|---|---|
| Form | API 模块 | Controller 请求入参和校验 |
| VO | API 模块 | 返回给前端或跨服务调用方 |
| DTO | Service 模块 | Service 内部编排和转换 |
| Entity | Service 模块 | 数据库表映射 |
不要直接把 Entity 作为复杂新增/编辑接口入参,否则数据库字段、审计字段和接口契约会被绑在一起。
7.3 Converter 是显式边界
项目大量使用手写 Converter:
text
Form -> DTO
Form -> Entity
Entity -> VO新增字段后必须搜索同业务 Converter。手写映射漏字段不会自动报错,尤其要核对列表、详情、导入和流程回调对象。
7.4 分页查询依赖 QueryModel
常见写法:
java
@PostMapping("/query/page")
@QueryPage
public Result<QueryResultModel> queryPage(@RequestBody QueryModel model) {
return Result.data(service.queryPage(model));
}ServiceImpl 通常:
- 设置默认
HeaderModel; - 通过
DataPermissionServer注入权限条件; - 使用
PageInfo.get()取得分页上下文; - 调用 Mapper 的
queryPageList; - 使用
QueryResultUtil组装统一分页返回。
7.5 动态查询主要在 Provider
Mapper 常见形式:
java
@SelectProvider(type = XxxProvider.class, method = "queryPageList")
IPage<Map<String, Object>> queryPageList(
IPage<Map<String, Object>> page,
@Param("model") QueryModel model);Provider 继承内部 SqlProvider,通过实体元数据和 QueryModel 构造动态 SQL。部分 Mapper XML 只有 namespace,不能因为 XML 为空就认为 Mapper 没有查询逻辑。
7.6 多租户与数据权限不是一回事
- 多租户限制当前请求只能访问当前
tenant_id; - 数据权限继续限制当前用户能看到哪些创建人、部门或自定义范围的数据;
@IgnoreTenant会绕过租户过滤,必须确认该数据确实是全局字典或由代码自行补充租户条件;- 定时任务遍历租户时必须设置并最终清理
TenantContextHolder。
7.7 事务只覆盖本地资源
@Transactional(rollbackFor = Exception.class) 可以保证本地数据库多表写入一致性,但不能自动撤销:
- 已发起的 BPM 流程;
- 已上传的 OSS 文件;
- 已完成的远程 Feign 调用;
- 已发送的异步任务。
包含这些操作的需求要额外考虑幂等键、状态表、补偿和重试。
8. 启动与构建
8.1 基础环境
建议准备:
- JDK 8;
- Maven 3.6+;
- 可访问公司 Maven 私服的网络与凭证;
- 对应环境的配置中心、数据库、缓存和内部服务访问权限;
- 开发环境网关 Token、租户和用户身份。
8.2 常用命令
bash
# 构建全部模块
mvn clean package -DskipTests
# 只构建 Service,并自动构建其依赖模块
mvn -pl hc-crm-center-service -am package -DskipTests
# Maven 依赖和配置准备完成后启动
mvn -pl hc-crm-center-service -am spring-boot:run如果团队有统一启动参数、配置中心 namespace 或 IDE Run Configuration,应优先使用团队配置。
8.3 启动入口
text
com.hc.crm.CRMCenterApplication启动类启用了:
text
@SpringBootApplication
@EnableScheduling
@EnableHcFeign
@EnableAsync
@EnableDiscoveryClient
@ComponentScan("com.hc.*")默认端口是 7001。spring.application.name 使用 @artifactId@ Maven 资源过滤占位符,直接运行未过滤的源码资源时可能看到占位符没有替换。
当前 application.yml 只包含端口和应用名,数据库、缓存、服务发现等完整配置不在仓库中。新人应向团队确认这些配置由哪个环境、Starter 或配置中心提供,不要为了本地启动把生产连接信息硬编码进源码。
9. 数据库脚本要特别谨慎
resources/sql 同时存在:
| 文件 | 方言/用途 |
|---|---|
init.sql | MySQL 风格,约 42 张表 |
pginit.sql | PostgreSQL 风格,约 46 张表 |
dictionary.sql | 系统字典初始化,当前写法偏 MySQL |
pgdatainit.sql | PostgreSQL 产品、结算规则、行业等种子数据 |
注意:
- 初始化脚本包含
DROP TABLE,不能直接在共享环境执行; - MySQL 和 PostgreSQL 脚本并存,执行前先确认目标数据库;
- 脚本不是严格的增量迁移体系,不能仅凭文件名判断已执行版本;
- 新需求应提交独立、可审计、尽量幂等的增量 SQL;
- 发布说明要写清正向 SQL、回滚 SQL、执行顺序和数据修复策略。
10. 模拟开发用例:新增“客户标签管理”
下面用一个影响范围较小的模拟需求演示如何按项目现有分层开发。
10.1 模拟需求
新增租户级客户标签管理:
- 标签字段:名称、颜色、状态、排序;
- 同一租户下,未删除标签名称不能重复;
- 支持新增、编辑、删除、详情和分页查询;
- 本期只维护标签字典,不直接修改复杂的客户跟进主流程;
- 后续如要给客户绑定标签,再单独增加
customer_tag_relation关系表。
接口建议:
text
POST /customerTag/save
POST /customerTag/edit
POST /customerTag/del/batch
POST /customerTag/findById
POST /customerTag/query/page实际访问地址还要叠加网关和内部 @WebClientRestController 的统一前缀。
10.2 需要新增的文件
text
hc-crm-center-api/
└── src/main/java/com/hc/crm/
├── form/CustomerTagForm.java
└── vo/CustomerTagVo.java
hc-crm-center-service/
└── src/main/
├── java/com/hc/crm/
│ ├── controller/web/client/CustomerTagController.java
│ ├── converter/CustomerTagConverter.java
│ ├── dto/CustomerTagDto.java
│ ├── entity/CustomerTag.java
│ ├── mapper/CustomerTagMapper.java
│ ├── provider/CustomerTagProvider.java
│ ├── service/CustomerTagService.java
│ └── service/impl/CustomerTagServiceImpl.java
└── resources/mapper/CustomerTagMapper.xml另提交一份独立 PostgreSQL 增量脚本,不要直接改写并重新执行完整 pginit.sql。
10.3 PostgreSQL 增量 SQL
sql
CREATE TABLE IF NOT EXISTS customer_tag (
id BIGINT NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tag_name VARCHAR(50) NOT NULL,
color VARCHAR(16),
status SMALLINT NOT NULL DEFAULT 0,
sort INTEGER NOT NULL DEFAULT 0,
create_by BIGINT,
create_name VARCHAR(50),
create_time TIMESTAMP,
update_by BIGINT,
update_name VARCHAR(50),
update_time TIMESTAMP,
deleted SMALLINT NOT NULL DEFAULT 0,
CONSTRAINT pk_customer_tag PRIMARY KEY (id)
);
COMMENT ON TABLE customer_tag IS '客户标签';
COMMENT ON COLUMN customer_tag.tag_name IS '标签名称';
COMMENT ON COLUMN customer_tag.color IS '展示颜色';
COMMENT ON COLUMN customer_tag.status IS '状态:0启用,1停用';
CREATE INDEX IF NOT EXISTS idx_customer_tag_tenant
ON customer_tag (tenant_id);
CREATE UNIQUE INDEX IF NOT EXISTS uk_customer_tag_tenant_name
ON customer_tag (tenant_id, tag_name)
WHERE deleted = 0;唯一索引是最终一致性保护,Service 中仍应提前检查并返回友好的业务错误。
10.4 API Form
java
package com.hc.crm.form;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Data
@Api(tags = "客户标签")
public class CustomerTagForm {
@ApiModelProperty("id,编辑时必填")
private Long id;
@NotBlank(message = "标签名称不能为空")
@Size(max = 50, message = "标签名称不能超过50个字符")
@ApiModelProperty("标签名称")
private String tagName;
@Size(max = 16, message = "颜色值不能超过16个字符")
@ApiModelProperty("颜色,例如 #FF4D4F")
private String color;
@NotNull(message = "状态不能为空")
@ApiModelProperty("状态:0启用,1停用")
private Integer status;
@ApiModelProperty("排序")
private Integer sort;
}不要让前端传 tenantId 并直接信任。租户应从 TenantContextHolder 获取,避免越权写入其他租户。
10.5 Entity
java
package com.hc.crm.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hc.mybatis.model.BaseModel;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName("customer_tag")
public class CustomerTag extends BaseModel {
@TableField("tenant_id")
private String tenantId;
@TableField("tag_name")
private String tagName;
@TableField("color")
private String color;
@TableField("status")
private Integer status;
@TableField("sort")
private Integer sort;
}BaseModel 已承载项目通用主键、审计和软删除约定时,不要在子类重复声明同名字段。
10.6 Converter
java
public final class CustomerTagConverter {
private CustomerTagConverter() {
}
public static CustomerTagDto convertFormToDto(CustomerTagForm form) {
CustomerTagDto dto = new CustomerTagDto();
dto.setId(form.getId());
dto.setTagName(form.getTagName());
dto.setColor(form.getColor());
dto.setStatus(form.getStatus());
dto.setSort(form.getSort());
return dto;
}
public static CustomerTagVo convertEntityToVo(CustomerTag entity) {
CustomerTagVo vo = new CustomerTagVo();
vo.setId(entity.getId());
vo.setTagName(entity.getTagName());
vo.setColor(entity.getColor());
vo.setStatus(entity.getStatus());
vo.setSort(entity.getSort());
return vo;
}
}10.7 Mapper 与 Provider
java
public interface CustomerTagMapper extends BaseMapper<CustomerTag> {
@SelectProvider(type = CustomerTagProvider.class, method = "queryPageList")
IPage<Map<String, Object>> queryPageList(
IPage<Map<String, Object>> page,
@Param("model") QueryModel model);
}java
public class CustomerTagProvider extends SqlProvider {
public String queryPageList(@Param("model") QueryModel model) {
return super.getMainSql(model, CustomerTag.class).toString();
}
}CustomerTagMapper.xml 至少保留正确 namespace:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hc.crm.mapper.CustomerTagMapper">
</mapper>10.8 Service 与业务规则
java
public interface CustomerTagService extends IService<CustomerTag> {
boolean saveTag(CustomerTagDto dto);
boolean updateTag(CustomerTagDto dto);
QueryResultModel queryPage(QueryModel model);
}java
@Service
public class CustomerTagServiceImpl
extends ServiceImpl<CustomerTagMapper, CustomerTag>
implements CustomerTagService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveTag(CustomerTagDto dto) {
String tenantId = TenantContextHolder.getTenantId();
checkNameUnique(tenantId, dto.getTagName(), null);
CustomerTag entity = new CustomerTag();
entity.setTenantId(tenantId);
entity.setTagName(dto.getTagName().trim());
entity.setColor(dto.getColor());
entity.setStatus(dto.getStatus());
entity.setSort(dto.getSort() == null ? 0 : dto.getSort());
return save(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateTag(CustomerTagDto dto) {
CustomerTag old = getById(dto.getId());
if (old == null) {
throw new BusinessException("客户标签不存在或已删除");
}
String tenantId = TenantContextHolder.getTenantId();
checkNameUnique(tenantId, dto.getTagName(), dto.getId());
old.setTagName(dto.getTagName().trim());
old.setColor(dto.getColor());
old.setStatus(dto.getStatus());
old.setSort(dto.getSort() == null ? 0 : dto.getSort());
return updateById(old);
}
private void checkNameUnique(String tenantId, String tagName, Long excludeId) {
long count = lambdaQuery()
.eq(CustomerTag::getTenantId, tenantId)
.eq(CustomerTag::getTagName, tagName.trim())
.ne(excludeId != null, CustomerTag::getId, excludeId)
.count();
if (count > 0) {
throw new BusinessException("同一租户下标签名称不能重复");
}
}
}示例中的异常类型和构造方法要以当前父 Starter 实际提供的 API 为准。如果项目统一使用其他校验工具,应复用现有方式。
10.9 Controller
java
@Slf4j
@RequiredArgsConstructor
@Api(tags = "客户标签")
@WebClientRestController("/customerTag")
public class CustomerTagController {
private final CustomerTagService customerTagService;
@Log(systemModule = "客户标签", operatorType = OperatorType.INSERT)
@ApiOperation("新增")
@PostMapping("/save")
public Result<Boolean> save(@Valid @RequestBody CustomerTagForm form) {
return Result.data(customerTagService.saveTag(
CustomerTagConverter.convertFormToDto(form)));
}
@Log(systemModule = "客户标签", operatorType = OperatorType.UPDATE)
@ApiOperation("编辑")
@PostMapping("/edit")
public Result<Boolean> edit(@Valid @RequestBody CustomerTagForm form) {
return Result.data(customerTagService.updateTag(
CustomerTagConverter.convertFormToDto(form)));
}
@ApiOperation("分页查询")
@PostMapping("/query/page")
@QueryPage
public Result<QueryResultModel> queryPage(@RequestBody QueryModel model) {
return Result.data(customerTagService.queryPage(model));
}
}删除和详情接口可以参考 IndustryDictController 的项目现有写法补齐。
10.10 模拟请求
http
POST /customerTag/save
Content-Type: application/json
{
"tagName": "重点客户",
"color": "#FF4D4F",
"status": 0,
"sort": 10
}分页请求体应沿用前端现有 QueryModel 结构,最稳妥的方式是复制 CustomerInfo 或 IndustryDict 页面的真实请求,再替换字段,不要自行猜测内部动态查询协议。
10.11 验证用例
| 场景 | 预期结果 |
|---|---|
| 当前租户首次新增“重点客户” | 成功 |
| 当前租户再次新增同名标签 | 业务失败,提示名称重复 |
| 另一租户新增同名标签 | 成功 |
| 编辑时名称保持不变 | 成功,不把自身判为重复 |
| 编辑不存在或已删除 ID | 失败 |
| 标签名为空或超过 50 字符 | Bean Validation 失败 |
| 不经过网关、缺少租户上下文 | 请求失败,不允许写入空租户数据 |
| 批量删除后再次查询 | 软删除数据不可见 |
| 并发创建同名标签 | 仅一个成功,唯一索引阻止重复数据 |
10.12 发布检查
- [ ] 增量 SQL 已评审并在空库、存量库验证。
- [ ] 唯一索引考虑了
tenant_id和软删除。 - [ ] Form、VO、DTO、Entity、Converter 字段完整一致。
- [ ] Controller 使用统一 Result、日志和路由注解。
- [ ] Service 从上下文取租户,不信任前端 tenantId。
- [ ] Mapper/Provider 分页能够按现有 QueryModel 查询。
- [ ] 至少补充重复名称、跨租户和并发唯一性测试。
- [ ] 权限资源如需纳入后台菜单,已补资源编码和授权配置。
- [ ] 发布说明包含正向 SQL、回滚方式和接口清单。
11. 新增真实功能时的标准落点
- 先找相邻业务:选择最像的 Controller、ServiceImpl 和 Entity,不要只看类名。
- 确认数据库方言:当前主链路按 PostgreSQL 处理,不能复制 MySQL 专用语法。
- 定义 API 契约:Form、VO 和枚举放 API 模块。
- 定义持久化模型:Entity、Mapper、Provider、XML 放 Service 模块。
- 补转换边界:Form/DTO/Entity/VO 的每个字段都要核对。
- 写业务规则:权限、租户、事务、状态和重复校验放 ServiceImpl。
- 处理分页:默认表头、权限过滤、Provider 和返回字段保持一致。
- 检查横向联动:合同、评估、试单、跟进、公海和附件是否互相影响。
- 检查异步与远程调用:明确失败、重试、幂等和补偿。
- 补验证:优先覆盖状态、权限、跨租户和并发场景。
- 补发布材料:SQL、配置、资源权限、任务和回滚方案。
12. 复杂度热点
| 文件 | 约行数 | 风险点 |
|---|---|---|
CustomerProductFollowServiceImpl.java | 1,743 | 跟进、保护、公海、期限、权限和统计集中在一个类 |
ExcelHelpUtil.java | 1,288 | Excel 样式、下拉、区域级联和大数据验证逻辑复杂 |
CustomerInfoServiceImpl.java | 1,081 | 客户主数据与多张关联表、远程成员和合同数据聚合 |
CustomerProductPublicPoolServiceImpl.java | 652 | 公海认领、掉海、状态变更与租户规则 |
ContractServiceImpl.java | 600 | 合同多子表、编号、流程、附件和跟进联动 |
DataPermissionQueryUtil.java | 570 | 用户、部门和自定义规则组合成动态条件树 |
CustomerFollowRecordServiceImpl.java | 432 | 跟进记录、阶段变化、附件、评论和权限 |
CustomerBrandServiceImpl.java | 398 | 品牌负责人、权限和客户关系联动 |
RegionDataUtil.java | 384 | 四级地区树加载、匹配和导入校验 |
ProjectEvaluationServiceImpl.java | 384 | 多张评估明细、流程与跟进状态 |
AbstractImportProcessor.java | 351 | 异步任务、批量校验、结果文件和模板方法 |
pginit.sql | 2,374 | 46 张表和索引集中在一个初始化脚本 |
region.json | 19,985 | 超大静态地区树,不适合人工逐行维护 |
修改这些热点时建议:
- 从具体 Controller、Job 或回调入口确定真实调用链;
- 搜索接口、实现、Entity 和 Mapper 的全部引用;
- 列出事务内外的数据库和远程副作用;
- 用表格写清状态前置条件、目标状态和失败行为;
- 优先提取小的领域服务,不继续向大类堆叠无关职责。
13. 当前项目的重要风险和坑
13.1 代码生成器含明文数据库连接信息
CodeGenerator.java 中存在硬编码数据库地址和凭据。不要把这些信息复制到文档、日志或新代码中;应尽快:
- 从源码移除;
- 改为环境变量或本地不入库配置;
- 对已暴露凭据执行轮换;
- 检查 Git 历史和相关环境访问记录。
13.2 代码生成器输出位置与当前模块边界不完全一致
当前实际约定是 Form/VO/Enum 放 API 模块,Entity/Mapper/Service 放 Service 模块;但 CodeGenerator 的自定义模板路径会把部分 DTO、VO、Form 输出到 Service 模块。
不要直接运行生成器并提交全部结果。应先:
- 修改表名和输出目录;
- 使用开发数据库;
- 禁止覆盖已有业务类;
- 把公共契约移动到 API 模块;
- 人工校对租户、审计、逻辑删除、校验和 Converter。
13.3 当前没有自动化测试源码
POM 配置了 Surefire 和 JaCoCo,但仓库没有 src/test 测试文件。并且 Surefire 配置了 testFailureIgnore=true,即使以后有测试,也要确认 CI 是否会因为失败测试正确阻断构建。
优先补测试的区域:
- 客户名称和企业标识查重;
- 跟进阶段、保护期限和公海流转;
- 合同、评估和试单审批回调幂等;
- 数据权限和跨租户隔离;
- Excel 导入重复数据与失败结果;
- 首单签约日期联动。
13.4 SQL 初始化脚本不是安全迁移脚本
完整初始化脚本包含删表,且 MySQL/PostgreSQL 方言并存。生产变更必须使用独立增量 SQL 和团队正式数据库变更流程。
13.5 README 几乎为空
根 README 只有“客户管理”标题,没有私服、配置中心、数据库、启动、网关、依赖服务、SQL 和测试说明。本文可作为上手材料,但正式仓库仍应补一个简洁 README。
13.6 启动类捕获顶层异常
main 捕获异常后只打印堆栈,可能让进程退出语义和监控判断不够清晰。通常让 Spring Boot 启动异常直接向上抛出更容易被容器或发布平台识别。
13.7 大型 Service 职责过多
跟进和客户 Service 已超过 1,000 行。新增复杂规则时优先提取:
- 保护规则服务;
- 公海流转服务;
- 截止时间计算服务;
- 客户详情聚合器;
- 合同审批编排器;
- 外部中心 Gateway/Adapter。
14. 常见问题排查
14.1 Maven 报父 POM无法解析、401 或依赖不存在
检查:
- 公司网络或 VPN;
settings.xml的私服 server id;- 私服账号权限;
- Snapshot 更新策略;
- 是否误用了公共 Maven 镜像覆盖公司仓库。
14.2 应用名是 @artifactId@
这是 Maven resource filtering 没有执行。使用 Maven 构建后的资源启动,或检查 IDE 是否启用了 Maven 资源过滤。
14.3 接口未登录、无租户或结果为空
确认请求是否经过正确网关,并建立:
- 用户上下文;
- 租户上下文;
- 当前成员和部门信息;
- 目标资源的数据权限。
数据库有数据但页面为空时,还要检查 DataPermissionServer 是否向 QueryModel 注入了限制条件。
14.4 新增成功但列表查不到字段
依次检查:
- Entity 是否有字段和
@TableField; - Converter 是否映射;
- Provider/SQL 是否选择该字段;
HeaderModel.fieldCode是否一致;- VO 或 Map 返回键是否与前端一致;
- 数据权限和租户条件是否过滤掉记录。
14.5 流程回调重复或状态不一致
检查:
flowId与业务记录是否正确绑定;- 回调是否可能重复投递;
- 终态是否通过
FlowUtil判断; - 本地状态更新是否幂等;
- 流程已成功、本地事务失败时是否有补偿。
14.6 公海任务跨租户或重复执行
检查:
- 每个租户执行前是否设置
TenantContextHolder; finally是否清理上下文;- 分布式锁 key 是否包含租户;
- 锁过期时间是否覆盖任务最长执行时间;
- 掉海更新是否本身幂等。
14.7 Excel 导入失败或下拉失效
检查:
- 模板表头是否与 DTO 注解一致;
- 枚举 Converter 是否支持中文 label;
- 地区和行业级联数据是否完整;
- 异步线程是否有用户、租户上下文;
- 批次大小和失败结果文件是否正确;
- 大数据下拉是否超过 Excel 限制。
15. 推荐的新手阅读路线
第 1 阶段:项目与启动
text
README.md
pom.xml
hc-crm-center-api/pom.xml
hc-crm-center-service/pom.xml
CRMCenterApplication.java
application.yml目标:知道项目为什么依赖公司环境,以及两个模块分别放什么。
第 2 阶段:学习一条简单 CRUD
text
IndustryDictController
IndustryDictService
IndustryDictServiceImpl
IndustryDictMapper
IndustryDictProvider
IndustryDict
IndustryDictConverter
IndustryDictForm / IndustryDictVo目标:理解 Form、VO、DTO、Entity、Converter、Service、Mapper 和 Provider。
第 3 阶段:客户主数据
text
CustomerInfoController
CustomerInfoServiceImpl
CustomerInfoMapper
CustomerInfoProvider
CustomerInfo
CustomerInfoConverter目标:理解列表聚合、多子表、远程成员信息和数据权限。
第 4 阶段:横切能力
text
DataPermissionServer
DataPermissionQueryUtil
ResourceConstants
TenantContextHolder 的使用位置
SaasDeptMemberServer
SaasBpmModelServer目标:理解租户、用户、部门权限和外部中心适配。
第 5 阶段:核心跟进流程
text
CustomerProductFollowServiceImpl
CustomerFollowRecordServiceImpl
CustomerProductPublicPoolServiceImpl
CustomerProtectConfigurationServiceImpl
PublicPoolJob目标:理解阶段、保护、转移、认领、截止时间和公海。
第 6 阶段:审批业务
text
ProjectEvaluationServiceImpl
TrialOrderApplicationServiceImpl
ContractServiceImpl
对应 ApiController.flowCallBack
FlowUtil目标:理解业务状态和外部 BPM 状态如何保持一致。
第 7 阶段:批量导入
text
ImportRecordController
AbstractImportProcessor
CustomerInfoImportProcessor
ImportValidatorUtil
ExcelHelpUtil
RegionDataUtil目标:理解异步批量任务、校验、级联下拉和失败结果文件。
第 8 阶段:数据库结构
text
pginit.sql
pgdatainit.sql
dictionary.sql
region.json目标:把 Entity 和业务服务还原成实际表关系、索引和基础数据。
16. 接手第一个需求前的检查清单
- [ ] Maven 私服依赖可以完整解析。
- [ ] 明确开发环境配置来源和启动参数。
- [ ] 知道请求如何建立用户、租户和部门上下文。
- [ ] 能从一个 Controller 跟到 ServiceImpl、Mapper、Provider 和 Entity。
- [ ] 明确目标数据是否需要资源级数据权限。
- [ ] 明确是否存在
@IgnoreTenant或手工租户条件。 - [ ] 明确是否涉及流程、文件、远程服务、异步任务或分布式锁。
- [ ] 明确事务能回滚哪些操作、不能回滚哪些操作。
- [ ] 确认使用 PostgreSQL 还是 MySQL SQL 方言。
- [ ] 核对 Form、DTO、Entity、VO 和 Converter 全链路字段。
- [ ] 为状态、权限、租户和并发场景准备可重复验证。
- [ ] 发布说明包含 SQL、配置、资源权限、任务和回滚方案。
17. 当前项目值得优先补齐的工程基础
- 移除
CodeGenerator的明文数据库凭据并立即轮换。 - 在根 README 补充私服、配置、依赖服务、数据库、启动和调试说明。
- 建立正式数据库迁移机制,分离 MySQL/PostgreSQL 和初始化/增量脚本。
- 新增自动化测试,并让失败测试能够阻断构建。
- 为流程回调、公海任务和导入任务补幂等与可观测性。
- 拆分
CustomerProductFollowServiceImpl和CustomerInfoServiceImpl。 - 修正或封装代码生成器,使输出目录符合 API/Service 模块边界。
- 统一依赖注入风格,优先使用构造器注入。
- 给关键资源编码、权限字段和状态机补架构说明。
- 将地区大 JSON 和基础字典纳入可版本化、可验证的数据发布流程。
新人完成前四个阅读阶段后,建议先接一个独立字典、查询字段或校验类需求,再进入产品跟进、公海、合同和审批等高耦合领域。
