SpringBoot+Vue3 实现 CRM 客户管理:公海流转 + 数据权限 + 负责人转移,一文讲透客户主数据全流程
📦 源码1:ruoyi-office-vben |📦 源码2:ruoyi-office |📦 源码3:ruoyi-office
客户是 CRM 的"主数据"——联系人挂在客户下、商机围着客户转、合同因客户而签、回款随客户而来。但客户管理远不止"存个客户名"这么简单:销售之间会撞单,客户被一个人占着不跟进又不放手,离职时客户资源带不走,老板想看团队客户却发现权限一团乱。RuoYi Office 用 1 张主表承载 40+ 字段,配合公海机制 + 数据权限 + 负责人转移三大设计,把"客户资源"变成一套可协作、可追溯、防撞单的经营资产。本文完整拆解其设计思路与核心实现。
▲ CRM 客户管理功能架构全景:数据模型(40+字段)、客户流转(公海闭环)、下游引用(联系人/商机/合同/回款)、四大设计亮点一图看懂
引言:客户管理到底难在哪?
"不就是一张客户表加个增删改查吗?"——这是很多人第一次做 CRM 时的想法。但真正落地到一个有销售团队的企业,客户管理的复杂度立刻暴露:
销售撞单,归属不清:A 销售刚加了客户微信,B 销售也在跟同一家公司,最后客户问"你们公司怎么好几个人联系我?"——没有"负责人 + 数据权限"机制,撞单无法避免。
客户被占用却不跟进:销售把客户领到名下就"躺平",既不跟进也不放手,导致大量客户资源沉睡。需要一套"公海"机制:长期不跟进的客户自动回收,谁有能力谁来领。
离职带走客户:销售离职时,名下几十上百个客户怎么办?必须能一键转移给新负责人,而且联系人、商机、合同要跟着一起转,不能只转个空壳。
权限混乱:销售只能看自己的客户,主管能看下属的,老板能看全部——这种"数据权限"如果靠 if-else 硬写,代码会爆炸。需要一套可复用的数据权限框架。
信息维度多:一个客户不只有名字电话,还有所属行业、客户级别、客户来源、工商信息、开票信息(发票抬头/税号/开户行)、跟进状态……光主表就 40+ 字段。
本文以 RuoYi Office 的 CRM 客户管理模块为例,完整拆解其数据建模、公海流转、数据权限、负责人转移的设计方案。
一、业务设计:客户作为"主数据"的定位
1.1 客户在 CRM 中的中心地位
在 CRM 体系里,客户是连接一切的"主数据",其他实体都依附于它:
┌─────────────┐
线索清洗 → │ 客户 Customer │ ← 公海/领取/转移
└──────┬──────┘
┌──────────┬───┴────┬──────────┐
▼ ▼ ▼ ▼
联系人 商机 合同 回款
(多对一) (销售机会) (成交落地) (分期回款)`
| 实体 | 与客户的关系 | 说明 |
|---|---|---|
| 联系人 | 一客户多联系人 | 决策人、对接人、财务对接等不同角色 |
| 商机 | 一客户多商机 | 一个客户可能有多个销售机会,各自推进 |
| 合同 | 一客户多合同 | 成交后签订,关联客户与商机 |
| 回款 | 挂在合同/客户下 | 分期回款 + 回款计划到期提醒 |
| 跟进记录 | 多态关联 | 既可跟进客户,也可跟进联系人/商机 |
正因为客户是主数据,删除客户前必须校验引用:存在联系人、商机或合同时禁止删除,避免下游数据变成"孤儿"。
1.2 客户的四类信息
RuoYi Office 用 1 张主表 crm_customer 承载客户的全部信息,按语义分为四类:
| 信息类别 | 核心字段 | 特点 |
|---|---|---|
| 基础与跟进 | 客户名称/来源/行业/级别/规模/价值、跟进状态、最后跟进时间、下次联系时间 | 决定客户画像与跟进节奏 |
| 联系信息 | 联系人/手机/电话/QQ/微信/邮箱、所在地区、详细地址、网址 | 触达客户的多种渠道 |
| 工商开票信息 | 发票抬头/税号/公司地址、开户行/银行账户、增值税税率/税种/结算方式 | 成交开票时直接复用 |
| 归属与状态 | 负责人 ownerUserId、成为负责人时间、锁定状态 lockStatus、成交状态 dealStatus | 公海流转与数据权限的基础 |
1.3 客户的流转生命周期
客户不是"录进来就归某人所有",而是在公海与负责人之间动态流转:
客户公海(无负责人) → 领取/分配(设负责人) → 跟进中 → 已成交
▲ │
└──────── 长期不跟进/主动放弃,自动回收 ◀────┘
| 阶段 | 关键状态 | 触发动作 |
|---|---|---|
| 公海 | ownerUserId = null | 新客户未分配 / 被退回 |
| 领取 | 设置 ownerUserId + 数据权限 | 销售主动领取或管理员分配 |
| 跟进中 | followUpStatus = true | 写跟进、设下次联系时间 |
| 已成交 | dealStatus = true | 标记成交,可不计入拥有上限 |
| 回收公海 | ownerUserId 置空 | 手动放入 / 定时任务自动回收 |
二、系统设计:模块职责与核心决策
2.1 模块定位
客户管理位于 CRM → 客户管理 目录下,是整个 CRM 的数据中枢:
| 定位 | 说明 |
|---|---|
| 功能 | 客户增删改查、公海流转、负责人转移、锁定、跟进、导入导出 |
| 消费方 | 联系人、商机、合同、回款模块都通过 ownerUserId 和数据权限关联客户 |
| 数据来源 | 手动新增 + 线索转化 + Excel 批量导入 + 公海领取 |
| 面向角色 | 销售(自己负责的)、销售主管(下属负责的)、管理员(全部) |
2.2 核心设计决策
| 决策点 | 方案 | 理由 |
|---|---|---|
| 数据权限 | @CrmPermission 注解 + AOP 拦截 | 声明式控制,业务代码无需写权限判断 |
| 客户归属 | 公海 + 负责人模型 | 避免撞单,资源可回收再分配 |
| 负责人转移 | 客户 + 联系人/商机/合同连带转移 | 离职交接一次完成,不留孤儿数据 |
| 拥有上限 | 可配置 + 是否计入已成交 | 防止销售"占着茅坑"囤客户 |
| 操作可追溯 | @LogRecord 记录每一次变更 | 谁领的、谁转的、改了啥,全程留痕 |
| 删除保护 | 校验下游引用 | 有联系人/商机/合同时禁止删除 |
三、PC 端功能实现
3.1 客户列表页
客户列表是销售的"主战场",提供 我负责的 / 我参与的 / 下属负责的 三个数据范围切换,配合客户名称、手机、电话、创建时间等多条件筛选:
▲ 客户列表页:三种数据范围切换(我负责的/我参与的/下属负责的),展示客户来源、联系人、客户级别等关键信息,支持新增/导入/导出
设计要点:
- 数据范围 Tab:
我负责的(owner 是我)、我参与的(被授予 READ/WRITE 权限)、下属负责的(主管看团队),对应后端CrmSceneTypeEnum。 - 客户级别可视化:A(重点客户)、B(普通客户)、C(非优先)用不同颜色标签区分,一眼识别价值。
- 批量导入:支持 Excel 导入,重名客户可选择"跳过"或"更新",导入结果分类返回成功/更新/失败清单。
- 行内操作:编辑、删除(删除前校验下游引用)。
3.2 客户详情页(客户 360 视图)
点击客户名进入详情页,这是一个"客户 360 视图"——把客户的所有维度信息和关联业务聚合在一个页面:
▲ 客户详情页:顶部操作区(修改/转移/更改成交状态/锁定/放入公海)+ 多 Tab(跟进记录/基本信息/联系人/团队成员/商机/合同/回款/操作日志),财务信息与系统信息分区清晰
详情页结构:
| 区域 | 内容 |
|---|---|
| 顶部操作区 | 修改、转移、更改成交状态、锁定/解锁、放入公海 |
| 概要栏 | 客户级别、成交状态、负责人、创建时间 |
| 跟进记录 Tab | 时间线展示每一次跟进,支持写跟进、设下次联系时间 |
| 基本信息 Tab | 基础信息 + 财务信息 + 系统信息三个分区 |
| 联系人 / 商机 / 合同 / 回款 Tab | 该客户下的关联业务,直接在详情页内联查看 |
| 团队成员 Tab | 协作授权:把客户只读/读写权限授予其他同事 |
| 操作日志 Tab | 谁在什么时间改了哪个字段,从空改成什么值,全程留痕 |
3.3 关键交互:写跟进
"写跟进"是销售每天的高频操作。跟进记录被设计成多态关联(既能跟进客户,也能跟进联系人/商机),并支持关联商机、关联联系人、上传图片和附件。提交跟进后,会自动回写客户的"最后跟进时间"和"下次联系时间",驱动列表的跟进提醒。
四、数据权限:CRM 的灵魂设计
4.1 为什么 CRM 必须有数据权限
普通 CRUD 的权限是"功能权限"(能不能点这个按钮),而 CRM 的核心是数据权限(能看到/操作哪些客户)。RuoYi Office 用一个自定义注解 @CrmPermission 优雅地解决了这个问题:
// 更新客户:要求当前用户对该客户有 WRITE 级别权限
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER,
bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
public void updateCustomer(CrmCustomerSaveReqVO updateReqVO) {
// 进入方法时,AOP 已校验过权限,业务代码无需关心
CrmCustomerDO oldCustomer = validateCustomerExists(updateReqVO.getId());
customerMapper.updateById(BeanUtils.toBean(updateReqVO, CrmCustomerDO.class));
}
bizId 用 SpEL 表达式 #updateReqVO.id 动态取参数中的客户 ID,AOP 切面在方法执行前自动校验,业务代码里看不到一行权限判断。
4.2 三级权限模型
| 权限级别 | 枚举 | 能做什么 |
|---|---|---|
| 负责人 | OWNER | 完全控制:修改、删除、转移、放入公海 |
| 读写 | WRITE | 可查看 + 可编辑,不能转移/删除 |
| 只读 | READ | 仅可查看 |
新建客户时,自动给创建人授予 OWNER 权限:
public Long createCustomer(CrmCustomerSaveReqVO createReqVO, Long userId) {
// 1. 校验拥有客户是否到达上限
validateCustomerExceedOwnerLimit(createReqVO.getOwnerUserId(), 1);
// 2. 插入客户
CrmCustomerDO customer = initCustomer(createReqVO, userId);
customerMapper.insert(customer);
// 3. 创建数据权限:把当前操作人设为负责人
permissionService.createPermission(new CrmPermissionCreateReqBO()
.setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
.setBizId(customer.getId()).setUserId(userId)
.setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
return customer.getId();
}
五、后端核心实现
5.1 公海机制:放入与领取
公海是 CRM 客户管理的精髓。放入公海本质是把负责人置空 + 清理数据权限:
@Transactional(rollbackFor = Exception.class)
protected void putCustomerPool(CrmCustomerDO customer) {
// 1. 设置负责人为 NULL(即进入公海)
int updated = customerMapper.updateOwnerUserIdById(customer.getId(), null);
if (updated == 0) {
throw exception(CUSTOMER_UPDATE_OWNER_USER_FAIL);
}
// 2. 联系人的负责人也置空(领取时会一起带过来)
contactService.updateOwnerUserIdByCustomerId(customer.getId(), null);
// 3. 删除负责人的数据权限(必须放在联系人之后,否则权限已删无法操作)
permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(),
customer.getId(), CrmPermissionLevelEnum.OWNER.getLevel());
}
领取公海客户则是反向操作——批量设置负责人 + 创建数据权限,并校验各种约束:
public void receiveCustomer(List<Long> ids, Long ownerUserId, Boolean isReceive) {
List<CrmCustomerDO> customers = customerMapper.selectByIds(ids);
// 逐个校验:是否已有负责人、是否锁定、是否已成交
customers.forEach(customer -> {
validateCustomerOwnerExists(customer, false); // 必须是公海数据
validateCustomerIsLocked(customer, false); // 不能是锁定客户
validateCustomerDeal(customer); // 不能是已成交客户
});
// 校验负责人是否超过拥有上限
validateCustomerExceedOwnerLimit(ownerUserId, customers.size());
// 批量设置负责人 + 创建数据权限
customerMapper.updateBatch(/* 设置 ownerUserId + ownerTime */);
permissionService.createPermissionBatch(/* OWNER 权限 */);
}
5.2 定时任务:自动回收公海
光有手动放入还不够,关键在于自动回收——让长期不跟进的客户自动掉回公海。系统通过定时任务扫描符合"掉海规则"的客户(如超过 N 天未跟进),逐个放入公海:
public int autoPutCustomerPool() {
CrmCustomerPoolConfigDO poolConfig = customerPoolConfigService.getCustomerPoolConfig();
if (poolConfig == null || !poolConfig.getEnabled()) {
return 0; // 公海规则未启用,直接跳过
}
// 1. 按规则查出需要回收的客户(如超期未跟进)
List<CrmCustomerDO> customerList = customerMapper.selectListByAutoPool(poolConfig);
// 2. 逐个放入公海,单个失败不影响整体
int count = 0;
for (CrmCustomerDO customer : customerList) {
try {
getSelf().putCustomerPool(customer);
count++;
} catch (Throwable e) {
log.error("[autoPutCustomerPool][客户({}) 放入公海异常]", customer.getId(), e);
}
}
return count;
}
这就实现了"资源不躺平":你领了客户却不跟进,到期就会被系统收回公海,别人可以重新领取。
5.3 负责人转移:连带迁移
销售离职或调岗,名下客户需要转移。转移不只转客户本身,还能连带转移其联系人、商机、合同:
@Transactional(rollbackFor = Exception.class)
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#reqVO.id",
level = CrmPermissionLevelEnum.OWNER)
public void transferCustomer(CrmCustomerTransferReqVO reqVO, Long userId) {
CrmCustomerDO customer = validateCustomerExists(reqVO.getId());
// 1. 校验新负责人是否超过拥有上限
validateCustomerExceedOwnerLimit(reqVO.getNewOwnerUserId(), 1);
// 2. 转移数据权限 + 重设负责人
permissionService.transferPermission(/* 旧负责人权限处理 + 新负责人 */);
customerMapper.updateById(new CrmCustomerDO().setId(reqVO.getId())
.setOwnerUserId(reqVO.getNewOwnerUserId()).setOwnerTime(LocalDateTime.now()));
// 3. 按需连带转移联系人/商机/合同
if (CollUtil.isNotEmpty(reqVO.getToBizTypes())) {
transfer(reqVO, userId);
}
}
toBizTypes 让用户勾选"要不要把这个客户的联系人/商机/合同也一起转过去",实现真正的"客户资产打包交接"。
5.4 拥有上限:防止囤客户
为避免销售无限囤积客户,系统支持配置"拥有上限",还能选择是否把已成交客户计入:
private void validateCustomerExceedOwnerLimit(Long userId, int newCount) {
List<CrmCustomerLimitConfigDO> limitConfigs =
customerLimitConfigService.getCustomerLimitConfigListByUserId(
CUSTOMER_OWNER_LIMIT.getType(), userId);
if (CollUtil.isEmpty(limitConfigs)) {
return; // 未配置上限
}
Long ownerCount = customerMapper.selectCountByDealStatusAndOwnerUserId(null, userId);
Long dealCount = customerMapper.selectCountByDealStatusAndOwnerUserId(true, userId);
limitConfigs.forEach(config -> {
// dealCountEnabled 决定已成交客户是否占用名额
long nowCount = config.getDealCountEnabled() ? ownerCount : ownerCount - dealCount;
if (nowCount + newCount > config.getMaxCount()) {
throw exception(CUSTOMER_OWNER_EXCEED_LIMIT);
}
});
}
5.5 删除保护:校验下游引用
客户是主数据,删除前必须确认没有被联系人、商机、合同引用:
private void validateCustomerReference(Long id) {
if (contactService.getContactCountByCustomerId(id) > 0) {
throw exception(CUSTOMER_DELETE_FAIL_HAVE_REFERENCE, CrmBizTypeEnum.CRM_CONTACT.getName());
}
if (businessService.getBusinessCountByCustomerId(id) > 0) {
throw exception(CUSTOMER_DELETE_FAIL_HAVE_REFERENCE, CrmBizTypeEnum.CRM_BUSINESS.getName());
}
if (contractService.getContractCountByCustomerId(id) > 0) {
throw exception(CUSTOMER_DELETE_FAIL_HAVE_REFERENCE, CrmBizTypeEnum.CRM_CONTRACT.getName());
}
}
六、RuoYi Office 的创新设计
6.1 声明式数据权限:注解即权限
把"能操作哪些数据"从业务代码中彻底剥离,用一个 @CrmPermission 注解 + SpEL 表达式声明,AOP 自动拦截。新增一个需要权限控制的方法,只需加一行注解,零侵入、可复用、易维护。这套思路同样适用于任何"按归属隔离数据"的业务。
6.2 公海机制:让客户资源流动起来
传统 CRM 客户一旦分配就"私有化",导致大量沉睡资源。公海机制把客户视为企业公共资产:能跟进就领,跟不动就还,到期自动收。配合"拥有上限",从制度上杜绝"占着不干活"。
6.3 操作日志:每一次变更都可追溯
借助 @LogRecord 注解,客户的创建、修改、转移、领取、放入公海等操作全部自动记录,并能解析"字段从旧值改成新值"。详情页的"操作日志"Tab 直接展示,管理者一目了然。
@LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_TRANSFER_SUB_TYPE,
bizNo = "{{#reqVO.id}}", success = CRM_CUSTOMER_TRANSFER_SUCCESS)
public void transferCustomer(...) { ... }
6.4 多态跟进记录:一张表跟进所有业务
跟进记录用 bizType + bizId 多态关联,既能跟进客户,也能跟进联系人、商机,还能在一条跟进里关联多个商机和联系人、上传图片附件,复用一张 crm_follow_up_record 表,避免为每种业务都建一张跟进表。
七、数据结构
7.1 客户主表 crm_customer(核心字段节选)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
name | varchar | 客户名称 |
follow_up_status | bit | 跟进状态 |
contact_last_time | datetime | 最后跟进时间 |
contact_next_time | datetime | 下次联系时间 |
owner_user_id | bigint | 负责人用户编号(为空即在公海) |
owner_time | datetime | 成为负责人时间 |
lock_status | bit | 锁定状态 |
deal_status | bit | 成交状态 |
level / source / industry_id | int | 客户级别 / 来源 / 行业(字典) |
customer_scale / customer_value | int | 客户规模 / 价值(字典) |
invoice_title / tax_no / bank_account | varchar | 发票抬头 / 税号 / 银行账户 |
设计要点:
owner_user_id为空 = 公海:用一个字段优雅表达"是否在公海",无需额外状态字段。lock_status锁定:锁定的客户不会被自动回收公海,用于保护重点客户。- 字典化字段:级别、来源、行业、规模、价值都走字典,方便统计与扩展。
7.2 跟进记录表 crm_follow_up_record
| 字段 | 类型 | 说明 |
|---|---|---|
biz_type / biz_id | int / bigint | 多态关联:跟进的对象类型与编号 |
type | int | 跟进类型(字典:电话/拜访/微信…) |
content | varchar | 跟进内容 |
next_time | datetime | 下次联系时间 |
pic_urls / file_urls | json | 图片 / 附件(List 类型处理器) |
business_ids / contact_ids | json | 关联的商机 / 联系人编号数组 |
八、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 数据权限 | @CrmPermission 注解 + AOP + SpEL | 声明式控制,业务零侵入 |
| 公海流转 | owner_user_id 置空 + 权限清理 | 客户资源可回收再分配 |
| 自动回收 | 定时任务 autoPutCustomerPool | 杜绝"占着不跟进" |
| 负责人转移 | 客户 + 联系人/商机/合同连带迁移 | 离职交接不留孤儿数据 |
| 拥有上限 | 可配置 + 是否计入已成交 | 防止销售囤客户 |
| 操作可追溯 | @LogRecord 自动记录变更 | 领取/转移/修改全程留痕 |
| 多态跟进 | biz_type + biz_id | 一张表跟进所有业务 |
| 删除保护 | 校验下游引用 | 避免孤儿数据 |
| 批量导入 | Excel + 重名策略 | 历史客户快速迁移 |
九、快速体验
- 在线演示:ruoyioffice.com/web/(账号 admin / admin123)
- 操作路径:登录后台 → 顶部菜单
CRM→ 左侧客户管理 - 推荐体验流程:
- 在
客户管理新增一个客户,填写基础、联系、工商信息 - 进入客户详情,点击
写跟进,设置下次联系时间 - 把客户
放入公海,再到客户公海菜单领取回来 - 点击
转移,体验把客户连同联系人/商机/合同一起转给同事 - 在
系统配置设置公海规则与拥有上限,观察自动回收效果 - 查看详情页
操作日志Tab,回放刚才的每一步操作
- 在
源码仓库:
| 仓库 | 地址 |
|---|---|
| 后端(GitCode) | gitcode.com/zhouzhongya… |
| 前端(GitCode) | gitcode.com/zhouzhongya… |
| 后端(GitHub) | github.com/yuqing2026/… |
结语
CRM 客户管理的本质,是把"客户"从一条数据库记录,升级为一份可协作、可流转、可追溯、防撞单的企业资产。RuoYi Office 用"公海机制 + 数据权限 + 负责人转移"三大设计,回答了销售管理中最头疼的几个问题:撞单怎么防、客户被占用怎么办、离职怎么交接、权限怎么隔离。
这套设计思路不止适用于 CRM——任何"按归属隔离数据 + 资源池化流转"的业务(如工单派发、线索分配、商机争抢)都可以复用。如果你正在做销售管理、客户经营相关的系统,不妨参考 RuoYi Office 的实现。
常见问题(FAQ)
RuoYi Office 的 CRM 客户管理是开源的吗?
RuoYi Office 基于 RuoYi-Vue-Pro / Yudao 架构深度定制,后端 Spring Boot 3.5 + 前端 Vue3,提供社区版与商业版两种选择,具体版本与功能边界可访问官网了解。
CRM 的数据权限是怎么实现的,会不会很难维护?
通过自定义注解 @CrmPermission + AOP + SpEL 表达式实现声明式权限控制,业务方法只需加一行注解即可,新增受控接口几乎零成本,比手写 if-else 权限判断好维护得多。
客户公海的"自动回收"是如何触发的?
通过定时任务 autoPutCustomerPool 按公海规则(如超期未跟进天数)定期扫描,把符合条件的客户负责人置空、清理数据权限,自动回收到公海,可被他人重新领取;锁定状态的客户不会被回收。
销售离职时,名下客户怎么交接?
使用"转移"功能,可把客户连同其联系人、商机、合同一起转移给新负责人,并自动迁移数据权限,避免出现"转了客户但联系人还在原人名下"的孤儿数据。
和钉钉/纷享销客这类 SaaS CRM 比,自建有什么优势?
源码可控、数据自主、可深度二开,能和企业已有的 OA、合同、ERP 等模块打通形成一体化平台;适合有定制需求或对数据私有化有要求的企业。
💡 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:ruoyioffice.com/web/(账号 admin / admin123)
💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!