很多团队做 CRM,最后做成了"客户信息录入表"。这篇不讲技术框架,先讲清楚 CRM 的业务模型该怎么设计,再说怎么落地。
做过客户管理系统的开发者,大概率遇到过这种对话:
业务方:"我们要一个 CRM。" 开发:"好的,客户表、跟进记录表、成交记录表,三天搭完。" 上线一个月后,业务方:"这个 CRM 没人用,销售还是用 Excel。"
为什么?因为大部分 CRM 做成了"高级通讯录"——只能录客户信息,不能驱动销售行为。
真正能用的 CRM,核心不是"存客户数据",而是解决三个业务问题:
这篇就把这三个问题讲透,再给出技术落地方案。
先别急着建表,先想清楚 CRM 里有哪些核心实体。
客户(Customer)
├── 联系人(Contact) 一个客户多个联系人
├── 跟进记录(FollowUp) 一个客户多条跟进
├── 成交记录(Deal) 一个客户多笔成交
└── 状态变更日志(StatusLog)
| 实体 | 核心字段 | 说明 |
|---|---|---|
| 客户 | 名称、来源、行业、级别、状态、负责人ID、所属部门 | CRM 的主体 |
| 联系人 | 姓名、电话、职位、客户ID | 一个客户多个联系人 |
| 跟进记录 | 客户ID、跟进方式、跟进内容、跟进时间、下次跟进时间 | 驱动销售行为的核心 |
| 成交记录 | 客户ID、成交金额、成交时间、订单号 | 统计用 |
这张表看着简单,但很多团队在第一步就埋了坑:把所有信息塞进一张客户表。客户表 30 个字段,连联系人电话、最近一次跟进内容都塞进去。结果是:一个客户多个联系人时没法存,跟进记录无法追溯。
原则:客户表只存客户本身的属性,联系人和跟进记录独立成表。
这是区分"高级通讯录"和"真正 CRM"的分水岭。
为什么需要这个机制?
没有公海私海的 CRM 会出现两个极端:
公海私海的本质是:客户是公司的资产,不是个人的资产。公司可以把客户分配给销售,也可以收回。
客户表加三个字段:
customer_id BIGINT -- 客户ID
owner_id BIGINT -- 负责人ID(NULL=公海)
owner_time DATETIME -- 领取/分配时间
protect_expire DATETIME -- 保护期截止时间
owner_id IS NULL → 公海客户owner_id = 123 → 销售私海客户protect_expire → 保护期内不能被回收| 来源 | 场景 |
|---|---|
| 新客户录入无负责人 | 市场部导入线索,未分配 |
| 销售主动放弃 | 销售判断无法成交,退回公海 |
| 系统自动回收 | 超过 N 天未跟进,自动回公海 |
公海私海有了,但如果销售领了客户就放着不跟进,私海就变成了"囤货"。
所以需要一个自动回收机制:超过 N 天没跟进的客户,自动从私海回到公海。
如果 客户在私海(owner_id 不为空)
且 当前时间 - 最后跟进时间 > 回收阈值
且 不在保护期内
则 自动回收到公海(owner_id 置空)
回收阈值按客户级别区分:
| 客户级别 | 回收阈值 |
|---|---|
| A(重要) | 30 天 |
| B(普通) | 15 天 |
| C(一般) | 7 天 |
重要客户给销售更多时间,一般客户快速回收。
刚分配/领取的客户,给一个保护期(比如 3 天),保护期内即使没跟进也不回收。避免刚领就被别人抢走。
protect_expire = 领取时间 + 3天
两种方式:
建议用定时任务,稳定可控。核心 SQL(写在 Mapper XML 里):
UPDATE customer
SET owner_id = NULL, owner_time = NULL, protect_expire = NULL
WHERE owner_id IS NOT NULL
AND protect_expire < NOW()
AND id IN (
SELECT c.id FROM customer c
LEFT JOIN (
SELECT customer_id, MAX(follow_time) AS last_time
FROM customer_follow_up
GROUP BY customer_id
) f ON c.id = f.customer_id
WHERE COALESCE(f.last_time, c.owner_time) < DATE_SUB(NOW(), INTERVAL 15 DAY)
)
客户不是静态的,会从"线索"一路走到"成交"或"流失"。状态流转设计不好,销售就不知道该干什么。
线索 → 跟进中 → 意向 → 谈判 → 成交
↘ 流失
↘ 战败
| 状态 | 含义 | 销售该做什么 |
|---|---|---|
| 线索 | 刚录入,未确认有效性 | 联系客户,判断意向 |
| 跟进中 | 已联系,需求待挖掘 | 持续跟进,了解需求 |
| 意向 | 有明确需求 | 提供方案、报价 |
| 谈判 | 方案已报,在推进 | 谈条件、促成交 |
| 成交 | 已签约 | 交付、维护 |
| 流失 | 客户主动放弃 | 分析原因 |
| 战败 | 跟进失败 | 复盘、归档 |
状态流转要有约束。不能从"线索"直接跳到"成交"——中间至少要经过"跟进中"。
建议用状态机管理,每次状态变更记录日志:
-- 状态变更日志
INSERT INTO customer_status_log (customer_id, from_status, to_status, operator_id, remark)
VALUES (123, 'FOLLOWING', 'INTENTION', 456, '客户确认有采购需求');
这样三个月后老板问"这个客户怎么从线索变成成交的",有完整的轨迹可查。
CRM 的数据权限是硬需求:
如果用 if/else 在每个查询里判断,代码会很乱。这就是数据权限拦截器该干的事。
用 Forge 的 DataScope 机制,按 mapperMethod 配置:
| 角色 | 数据范围 | 效果 |
|---|---|---|
| 销售 | SELF(本人) | 只看 owner_id = 当前用户 的客户 |
| 主管 | ORG(本组织) | 看本部门所有销售负责的客户 |
| 总监 | ORG_AND_CHILD | 看本部门及子部门 |
| 老板 | ALL | 看全部 |
在 sys_data_scope_config 里配一行:
mapperMethod: com.xxx.mapper.CustomerMapper.selectPage
tableAlias: c
userIdColumn: owner_id
orgIdColumn: owner_dept_id
拦截器会在 SQL 的 WHERE 后自动追加 c.owner_id = 当前用户 或 c.owner_dept_id IN (...),业务代码完全不用管。
公海客户的查询单独配一个方法,不加数据权限:
// 私海客户列表(受数据权限控制)
List<Customer> selectPage(@Param("dto") CustomerQueryDTO dto);
// 公海客户列表(所有人可看,不拦截)
@IgnoreDataScope // 或不配 data_scope_config
List<Customer> selectPublicPool(@Param("dto") CustomerQueryDTO dto);
很多人把跟进记录做成"备注"——一个文本框,随便填。这是错的。
跟进记录是 CRM 里最重要的业务实体,它驱动销售行为,也决定客户是否被回收。
| 字段 | 说明 |
|---|---|
| 客户ID | 关联哪个客户 |
| 跟进方式 | 电话、微信、上门、邮件(字典管理) |
| 跟进内容 | 这次沟通了什么 |
| 跟进时间 | 什么时候跟进的 |
| 下次跟进时间 | 最重要的字段,驱动下一次行动 |
| 跟进结果 | 初步接触、需求明确、方案已发、等待反馈 |
每次跟进,销售必须填"下次跟进时间"。系统基于这个字段做两件事:
没有"下次跟进时间"的 CRM,销售跟完一次就忘了,客户就这么凉了。
跟进记录不是孤立的。一次"方案已发"的跟进,应该把客户状态从"跟进中"推进到"意向"。
public void addFollowUp(FollowUp followUp) {
// 1. 存跟进记录
followUpMapper.insert(followUp);
// 2. 更新客户的最后跟进时间和下次跟进时间
customerMapper.updateFollowInfo(
followUp.getCustomerId(),
followUp.getFollowTime(),
followUp.getNextFollowTime()
);
// 3. 根据跟进结果推进客户状态
if ("INTENTION".equals(followUp.getResult())) {
customerMapper.updateStatus(followUp.getCustomerId(), "INTENTION");
}
}
| 方式 | 场景 |
|---|---|
| 主动领取 | 销售从公海自己挑客户领 |
| 主管分配 | 主管把公海客户分配给指定销售 |
分配时的核心逻辑:
public void assign(Long customerId, Long ownerId) {
Customer c = customerMapper.selectById(customerId);
// 1. 检查是否在公海
if (c.getOwnerId() != null) {
throw new BizException("客户不在公海,无法分配");
}
// 2. 检查销售私海上限
int count = customerMapper.countByOwner(ownerId);
if (count >= maxPrivateSize) {
throw new BizException("该销售私海客户已达上限");
}
// 3. 分配 + 设置保护期
customerMapper.assign(customerId, ownerId, now(), now().plusDays(3));
}
私海上限很重要。没有上限,销售会把公海客户全领完,其他人无客户可跟。
销售离职时,客户不能跟着走。两种处理:
public void transferOnResign(Long fromUserId, Long toUserId) {
// 把离职销售的所有私海客户转给新人
customerMapper.transferOwner(fromUserId, toUserId);
// 记录转移日志
transferLogMapper.log(fromUserId, toUserId, "离职转移");
}
CRM 的最终目的是帮企业看清客户和成交情况。老板关心的几个数据:
| 指标 | 说明 |
|---|---|
| 本月新增客户数 | 市场获客能力 |
| 本月成交金额 | 销售转化能力 |
| 客户转化率 | 线索→成交的漏斗 |
| 销售排行榜 | 谁卖得好 |
| 公海客户数 | 待分配的库存 |
| 待跟进客户数 | 今天该做什么 |
| 超期未跟进数 | 风险预警 |
这些数据不用每个都做大屏,先做一张首页看板就够了:
┌─────────────┬─────────────┬─────────────┐
│ 本月新增客户 │ 本月成交金额 │ 转化率 │
│ 128 │ ¥356,000 │ 12.5% │
├─────────────┼─────────────┼─────────────┤
│ 公海客户 │ 待跟进今日 │ 超期未跟进 │
│ 45 │ 23 │ 7 │
└─────────────┴─────────────┴─────────────┘
技术实现上,这些就是几个 COUNT / SUM 查询,配合 ECharts 渲染。如果用 Forge 的大屏能力,拖几个图表组件、接好接口就能出看板。
讲完业务设计,说下技术选型。一个 CRM 涉及的能力和 Forge 的对应关系:
| CRM 能力 | Forge 的能力 |
|---|---|
| 客户/联系人/跟进记录页面 | 低代码 CRUD 页面搭建 |
| 客户数据权限(销售看自己) | DataScope 拦截器,按 owner_id 过滤 |
| 客户状态字典 | 字典管理 + DictSelect/DictTag |
| 自动回收定时任务 | 定时任务插件(Quartz) |
| 客户分配/转移 | 业务接口 + 操作日志 |
| 成交统计看板 | AI 数据大屏 / 首页看板 |
| 审批(大额折扣) | Flowable 工作流 |
| 移动端跟进 | H5 端,手机录跟进记录 |
其中最省事的是页面搭建和数据权限:
sys_data_scope_config 配一行,拦截器自动在 SQL 加 owner_id = ?DictTag 渲染标签真正需要手写的业务逻辑只有三块:
其他 80% 的工作(CRUD 页面、权限、字典、看板)用现成能力就能搞定。
同一家公司被不同销售录了两次,公海私海就乱了。录入时按公司名/统一社会信用代码查重,已存在则不允许重复录入,或走"认领"流程。
一个客户有多个联系人(采购、技术、老板),必须独立成表。塞进客户表会导致只能存一个联系人。
跟进记录是销售行为的证据。即使客户战败,记录也要保留,用于复盘。加个"仅本人和主管可看"的权限就行,不要做物理删除。
不同行业、不同客户级别的回收阈值不一样。别写死在代码里,做成字典或配置表,让业务方自己调。
公海是所有人都能看的,别给公海查询加数据权限拦截。私海受控、公海开放,这是规则。
回到开头的问题:为什么很多 CRM 做成了高级通讯录?
因为只做了"存客户数据",没做驱动销售行为的机制。
一个能用的 CRM,核心是这三件事:
| 机制 | 解决什么 |
|---|---|
| 公海私海 | 客户归谁,防止囤积和抢单 |
| 自动回收 | 逼着销售去跟进,不跟就收回 |
| 下次跟进时间 | 驱动销售每天该干什么 |
数据权限保证客户资产安全,状态流转让销售知道客户到哪一步了,统计看板让老板看清全局。
把这几个机制做对,CRM 才不会沦为"高级通讯录"。
体验完整的客户管理能力:
你做过的 CRM 踩过最大的坑是什么?公海私海、自动回收、还是数据权限?评论区聊聊。