SpringBoot+Vue3 实现 CRM 客户管理:公海流转 + 数据权限 + 负责人转移,一文讲透客户主数据全流程

0 阅读17分钟

SpringBoot+Vue3 实现 CRM 客户管理:公海流转 + 数据权限 + 负责人转移,一文讲透客户主数据全流程

📦 源码1ruoyi-office-vben |📦 源码2ruoyi-office |📦 源码3ruoyi-office

客户是 CRM 的"主数据"——联系人挂在客户下、商机围着客户转、合同因客户而签、回款随客户而来。但客户管理远不止"存个客户名"这么简单:销售之间会撞单,客户被一个人占着不跟进又不放手,离职时客户资源带不走,老板想看团队客户却发现权限一团乱。RuoYi Office 用 1 张主表承载 40+ 字段,配合公海机制 + 数据权限 + 负责人转移三大设计,把"客户资源"变成一套可协作、可追溯、防撞单的经营资产。本文完整拆解其设计思路与核心实现。 crm-customer-architecture.png

▲ 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 客户列表页

客户列表是销售的"主战场",提供 我负责的 / 我参与的 / 下属负责的 三个数据范围切换,配合客户名称、手机、电话、创建时间等多条件筛选: crm-customer-list.png ▲ 客户列表页:三种数据范围切换(我负责的/我参与的/下属负责的),展示客户来源、联系人、客户级别等关键信息,支持新增/导入/导出

设计要点:

  • 数据范围 Tab我负责的(owner 是我)、我参与的(被授予 READ/WRITE 权限)、下属负责的(主管看团队),对应后端 CrmSceneTypeEnum
  • 客户级别可视化:A(重点客户)、B(普通客户)、C(非优先)用不同颜色标签区分,一眼识别价值。
  • 批量导入:支持 Excel 导入,重名客户可选择"跳过"或"更新",导入结果分类返回成功/更新/失败清单。
  • 行内操作:编辑、删除(删除前校验下游引用)。

3.2 客户详情页(客户 360 视图)

点击客户名进入详情页,这是一个"客户 360 视图"——把客户的所有维度信息和关联业务聚合在一个页面: crm-customer-detail.png ▲ 客户详情页:顶部操作区(修改/转移/更改成交状态/锁定/放入公海)+ 多 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(核心字段节选)

字段类型说明
idbigint主键
namevarchar客户名称
follow_up_statusbit跟进状态
contact_last_timedatetime最后跟进时间
contact_next_timedatetime下次联系时间
owner_user_idbigint负责人用户编号(为空即在公海)
owner_timedatetime成为负责人时间
lock_statusbit锁定状态
deal_statusbit成交状态
level / source / industry_idint客户级别 / 来源 / 行业(字典)
customer_scale / customer_valueint客户规模 / 价值(字典)
invoice_title / tax_no / bank_accountvarchar发票抬头 / 税号 / 银行账户

设计要点:

  • owner_user_id 为空 = 公海:用一个字段优雅表达"是否在公海",无需额外状态字段。
  • lock_status 锁定:锁定的客户不会被自动回收公海,用于保护重点客户。
  • 字典化字段:级别、来源、行业、规模、价值都走字典,方便统计与扩展。

7.2 跟进记录表 crm_follow_up_record

字段类型说明
biz_type / biz_idint / bigint多态关联:跟进的对象类型与编号
typeint跟进类型(字典:电话/拜访/微信…)
contentvarchar跟进内容
next_timedatetime下次联系时间
pic_urls / file_urlsjson图片 / 附件(List 类型处理器)
business_ids / contact_idsjson关联的商机 / 联系人编号数组

八、技术亮点总结

设计要点实现方式价值
数据权限@CrmPermission 注解 + AOP + SpEL声明式控制,业务零侵入
公海流转owner_user_id 置空 + 权限清理客户资源可回收再分配
自动回收定时任务 autoPutCustomerPool杜绝"占着不跟进"
负责人转移客户 + 联系人/商机/合同连带迁移离职交接不留孤儿数据
拥有上限可配置 + 是否计入已成交防止销售囤客户
操作可追溯@LogRecord 自动记录变更领取/转移/修改全程留痕
多态跟进biz_type + biz_id一张表跟进所有业务
删除保护校验下游引用避免孤儿数据
批量导入Excel + 重名策略历史客户快速迁移

九、快速体验

  • 在线演示ruoyioffice.com/web/(账号 admin / admin123)
  • 操作路径:登录后台 → 顶部菜单 CRM → 左侧 客户管理
  • 推荐体验流程
    1. 客户管理 新增一个客户,填写基础、联系、工商信息
    2. 进入客户详情,点击 写跟进,设置下次联系时间
    3. 把客户 放入公海,再到 客户公海 菜单 领取 回来
    4. 点击 转移,体验把客户连同联系人/商机/合同一起转给同事
    5. 系统配置 设置公海规则与拥有上限,观察自动回收效果
    6. 查看详情页 操作日志 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)

📦 源码仓库GitCode | GitHub

💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」

如果觉得不错,请给个 Star 支持一下!