Skip to content

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 多模块
WebSpring Boot、Spring MVC、内部 hc-starter-web
微服务服务发现、内部 HC Feign、@HcApiClient
持久化MyBatis-Plus、动态 SqlProvider、Mapper XML、PostgreSQL
多租户TenantContextHolder、MyBatis 租户插件、@IgnoreTenant
数据权限DataPermissionServerDataPermissionQueryUtil、资源编码
缓存与锁内部 hc-starter-cachehc-starter-lock
调度XXL-Job、Spring Scheduling
异步Spring @EnableAsync、导入异步任务
文件动态 OSS、文件预览 API、业务附件表
工作流流程中心 API、BPM 发起与回调
ExcelEasyExcel 风格导入处理、动态下拉、失败结果文件
对象处理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.json

3.1 根模块 hc-crm-center

pom.xmlpackaging=pom 的聚合模块,负责:

  • 聚合 hc-crm-center-apihc-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 文件,负责可复用契约:

目录文件数职责
api1对外或跨服务 API Client 契约
enums42跟进阶段、合同状态、审批状态、客户来源等公共业务枚举
form68Controller 入参、批量操作和流程回调参数
vo49详情、列表、树结构和跨模块返回对象

这个模块会被 Service 模块和其他微服务依赖,因此修改 Form、VO、枚举或 API Client 时要考虑向后兼容:

  • 不要随意删除字段或枚举值;
  • 新字段优先保持可选并给出默认行为;
  • 枚举编码一旦入库或对外传输,不要重新编号;
  • 跨服务对象的包名和序列化格式也是接口契约的一部分。

3.3 hc-crm-center-service

Service 模块是实际运行服务,共 381 个 Java 文件。主要包如下:

文件数主要职责
controller35Web 接口、内部 API、BPM 回调
service90业务接口与实现类
entity44MyBatis-Plus 数据实体
mapper44Mapper 接口
provider45动态分页和联表 SQL Provider
converter44Form、DTO、Entity、VO 转换
dto48Service 内部编排对象
component9认证、租户、流程、系统和集成中心适配
imports10Excel 导入 DTO、Converter、Processor
util7权限、Excel、校验、地区、变更和流程工具
handler2PostgreSQL 数组等类型处理
job1公海掉入 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 契约层160API Client、Form、VO、枚举和公共传输定义
接口接入层35Web Controller、内部 API 与审批回调
业务服务层91Service 接口、ServiceImpl 和公海任务
集成与传输模型层111外部组件、DTO、Converter 和导入模型
ORM 持久层179Entity、Mapper、Provider、XML 和类型处理器
公共工具层7权限、Excel、校验、地区、变更和流程工具
数据库与基础数据层95PostgreSQL/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.flowCallBack

6.4 合同

合同模块包含合同主表、客户关系、联系人、结算规则、业务模式、垫资用途、编号序列和附件。

ContractServiceImpl 的关键职责:

  • 验证客户和产品跟进资格;
  • 生成合同编号;
  • 保存主表及多张关联表;
  • 发起流程审批;
  • 处理流程回调;
  • 在首份合同通过时回写客户签约日期;
  • 调整产品跟进阶段和截止时间规则。

合同保存是典型的事务边界。数据库写入可以回滚,但已经成功的远程流程或文件操作不一定能随本地事务自动回滚,需要单独设计幂等和补偿。

6.5 试单申请

试单申请与项目评估、合同类似,也有:

  • Form/DTO/Entity;
  • 主表和产品关联表;
  • Web Controller 与内部回调 Controller;
  • 流程发起、审批状态和回调处理。

阅读 TrialOrderApplicationServiceImpl 可以学习一个规模相对可控的工作流业务实现。

6.6 Excel 批量导入

text
ImportRecordController
  -> ImportRecordService
  -> AbstractImportProcessor<T>
  -> CustomerInfoImportProcessor
  -> CustomerInfoService.saveBatchByImport

AbstractImportProcessor 使用模板方法模式,统一处理:

  • 文件与表头校验;
  • 导入任务创建与进度更新;
  • Bean Validation;
  • 分批持久化;
  • 成功、失败数量统计;
  • 失败原因和结果文件;
  • 模板下载与动态下拉。

异步导入必须显式关注用户和租户上下文传播,不能假设异步线程天然继承请求线程上下文。

7. 项目中的关键开发约定

7.1 Controller 保持薄层

Controller 主要做:

  • 接收并校验 Form;
  • 做少量快速前置判断;
  • 调用 Converter;
  • 调用 Service;
  • 使用 Result.data(...)Result.fail(...) 返回;
  • 使用 @Log 记录新增、编辑和删除操作。

复杂事务、跨表编排和权限判断应进入 ServiceImpl。

7.2 请求和返回对象分开

对象位置作用
FormAPI 模块Controller 请求入参和校验
VOAPI 模块返回给前端或跨服务调用方
DTOService 模块Service 内部编排和转换
EntityService 模块数据库表映射

不要直接把 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 通常:

  1. 设置默认 HeaderModel
  2. 通过 DataPermissionServer 注入权限条件;
  3. 使用 PageInfo.get() 取得分页上下文;
  4. 调用 Mapper 的 queryPageList
  5. 使用 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.*")

默认端口是 7001spring.application.name 使用 @artifactId@ Maven 资源过滤占位符,直接运行未过滤的源码资源时可能看到占位符没有替换。

当前 application.yml 只包含端口和应用名,数据库、缓存、服务发现等完整配置不在仓库中。新人应向团队确认这些配置由哪个环境、Starter 或配置中心提供,不要为了本地启动把生产连接信息硬编码进源码。

9. 数据库脚本要特别谨慎

resources/sql 同时存在:

文件方言/用途
init.sqlMySQL 风格,约 42 张表
pginit.sqlPostgreSQL 风格,约 46 张表
dictionary.sql系统字典初始化,当前写法偏 MySQL
pgdatainit.sqlPostgreSQL 产品、结算规则、行业等种子数据

注意:

  • 初始化脚本包含 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 结构,最稳妥的方式是复制 CustomerInfoIndustryDict 页面的真实请求,再替换字段,不要自行猜测内部动态查询协议。

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. 新增真实功能时的标准落点

  1. 先找相邻业务:选择最像的 Controller、ServiceImpl 和 Entity,不要只看类名。
  2. 确认数据库方言:当前主链路按 PostgreSQL 处理,不能复制 MySQL 专用语法。
  3. 定义 API 契约:Form、VO 和枚举放 API 模块。
  4. 定义持久化模型:Entity、Mapper、Provider、XML 放 Service 模块。
  5. 补转换边界:Form/DTO/Entity/VO 的每个字段都要核对。
  6. 写业务规则:权限、租户、事务、状态和重复校验放 ServiceImpl。
  7. 处理分页:默认表头、权限过滤、Provider 和返回字段保持一致。
  8. 检查横向联动:合同、评估、试单、跟进、公海和附件是否互相影响。
  9. 检查异步与远程调用:明确失败、重试、幂等和补偿。
  10. 补验证:优先覆盖状态、权限、跨租户和并发场景。
  11. 补发布材料:SQL、配置、资源权限、任务和回滚方案。

12. 复杂度热点

文件约行数风险点
CustomerProductFollowServiceImpl.java1,743跟进、保护、公海、期限、权限和统计集中在一个类
ExcelHelpUtil.java1,288Excel 样式、下拉、区域级联和大数据验证逻辑复杂
CustomerInfoServiceImpl.java1,081客户主数据与多张关联表、远程成员和合同数据聚合
CustomerProductPublicPoolServiceImpl.java652公海认领、掉海、状态变更与租户规则
ContractServiceImpl.java600合同多子表、编号、流程、附件和跟进联动
DataPermissionQueryUtil.java570用户、部门和自定义规则组合成动态条件树
CustomerFollowRecordServiceImpl.java432跟进记录、阶段变化、附件、评论和权限
CustomerBrandServiceImpl.java398品牌负责人、权限和客户关系联动
RegionDataUtil.java384四级地区树加载、匹配和导入校验
ProjectEvaluationServiceImpl.java384多张评估明细、流程与跟进状态
AbstractImportProcessor.java351异步任务、批量校验、结果文件和模板方法
pginit.sql2,37446 张表和索引集中在一个初始化脚本
region.json19,985超大静态地区树,不适合人工逐行维护

修改这些热点时建议:

  • 从具体 Controller、Job 或回调入口确定真实调用链;
  • 搜索接口、实现、Entity 和 Mapper 的全部引用;
  • 列出事务内外的数据库和远程副作用;
  • 用表格写清状态前置条件、目标状态和失败行为;
  • 优先提取小的领域服务,不继续向大类堆叠无关职责。

13. 当前项目的重要风险和坑

13.1 代码生成器含明文数据库连接信息

CodeGenerator.java 中存在硬编码数据库地址和凭据。不要把这些信息复制到文档、日志或新代码中;应尽快:

  1. 从源码移除;
  2. 改为环境变量或本地不入库配置;
  3. 对已暴露凭据执行轮换;
  4. 检查 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 新增成功但列表查不到字段

依次检查:

  1. Entity 是否有字段和 @TableField
  2. Converter 是否映射;
  3. Provider/SQL 是否选择该字段;
  4. HeaderModel.fieldCode 是否一致;
  5. VO 或 Map 返回键是否与前端一致;
  6. 数据权限和租户条件是否过滤掉记录。

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. 当前项目值得优先补齐的工程基础

  1. 移除 CodeGenerator 的明文数据库凭据并立即轮换。
  2. 在根 README 补充私服、配置、依赖服务、数据库、启动和调试说明。
  3. 建立正式数据库迁移机制,分离 MySQL/PostgreSQL 和初始化/增量脚本。
  4. 新增自动化测试,并让失败测试能够阻断构建。
  5. 为流程回调、公海任务和导入任务补幂等与可观测性。
  6. 拆分 CustomerProductFollowServiceImplCustomerInfoServiceImpl
  7. 修正或封装代码生成器,使输出目录符合 API/Service 模块边界。
  8. 统一依赖注入风格,优先使用构造器注入。
  9. 给关键资源编码、权限字段和状态机补架构说明。
  10. 将地区大 JSON 和基础字典纳入可版本化、可验证的数据发布流程。

新人完成前四个阅读阶段后,建议先接一个独立字典、查询字段或校验类需求,再进入产品跟进、公海、合同和审批等高耦合领域。

Lucking