简介:这份CRM客户管理系统功能清单以doc文档形式呈现,面向企业信息化选型人员、CRM产品经理及销售管理岗从业者,用于梳理客户管理系统的完整功能边界与模块划分。压缩包内共1个doc文件,约376KB,内容以功能条目与三级编码结构为主,便于直接对照查阅。文档按计划管理、销售合同管理、开票管理、帐款管理、业绩考核管理、市场信息、促销品广告、统计报表、公共关系管理九大模块展开,每个功能点均标注编号与操作说明,如销售计划维护、合同变更历史查询、调拨与税票对帐、月末结账与反结、业绩考核查询等,覆盖从计划、合同、开票到回款、费用、广告的完整业务链路。已有800人学习,适合作为CRM需求调研、功能对标或系统设计阶段的参考底稿。
1. 从一张 Excel 到一套系统:CRM 客户管理到底该有哪些功能
很多团队做 CRM 客户管理,起点都是一张共享 Excel:销售把客户名、电话、跟进状态填进去,主管每周导出一次做透视表。前三个月还能跑,一旦销售超过五个人、客户超过两千条,这张表就开始失控——两个人同时改一行会互相覆盖,字段越加越乱,谁最后跟进过全靠翻聊天记录。CRM 管理系统功能清单要解决的,正是把这张表升级成一套有权限、有流程、有数据约束的系统。它适合两类人:一类是正在选型、需要拿清单去比对产品的业务负责人;另一类是准备自研、需要知道每个模块背后数据模型怎么设计的开发者。下面我按一套可落地的功能清单往下拆,重点讲每个模块为什么存在、表怎么建、接口怎么定。
2. 客户主数据与联系人:功能清单的地基怎么打
CRM 里所有功能都挂在“客户”这个实体上,所以第一件事不是画页面,而是把客户主数据模型定死。很多系统后期改不动,就是因为一开始把客户和联系人揉在一张表里,一个客户多个联系人时只能加contact2_phone这种字段,越加越离谱。
2.1 客户、联系人、商机三张核心表的关系
常见做法是拆成三张表:客户(account)代表一家公司或一个购买主体,联系人(contact)代表具体的人,商机(opportunity)代表一次可能成交的机会。一个客户可以有多个联系人,一个客户也可以有多个商机,联系人和商机之间通过关联表挂接。这样拆的好处是:人员离职换联系人,不影响客户归属;一个客户复购,直接新建商机而不是复制客户。
下面是最小可用的建表语句,字段做了精简,但保留了关键约束:
-- 客户主表:一个购买主体一行 CREATE TABLE crm_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL, -- 客户名称,必填 owner_id BIGINT NOT NULL, -- 归属销售,决定数据权限 industry VARCHAR(64), -- 行业,用于分层 level TINYINT DEFAULT 1, -- 客户等级 1-5 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_name_owner (name, owner_id) -- 同一销售下客户名不重复 ); -- 联系人表:挂在客户下,一个客户可多条 CREATE TABLE crm_contact ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_id BIGINT NOT NULL, -- 外键指向客户 name VARCHAR(64) NOT NULL, phone VARCHAR(32), email VARCHAR(128), is_primary TINYINT DEFAULT 0, -- 是否主联系人 KEY idx_account (account_id) ); -- 商机表:一次成交机会 CREATE TABLE crm_opportunity ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_id BIGINT NOT NULL, contact_id BIGINT, -- 对接人,可为空 stage VARCHAR(32) NOT NULL, -- 阶段:初步/方案/报价/赢单/输单 amount DECIMAL(14,2) DEFAULT 0, -- 预计金额 owner_id BIGINT NOT NULL, KEY idx_account_stage (account_id, stage) );逻辑说明:owner_id是整套 CRM 数据权限的根,后面所有“销售只能看自己客户”的规则都靠它过滤。uk_name_owner这个唯一键是血泪经验——没有它,销售会重复录入同一个客户,后期合并数据非常痛苦。stage用字符串而不是枚举,是为了让业务能自己加阶段,代价是需要在应用层做校验。
参数上,level用 TINYINT 而不是字符串,方便做区间查询;amount用 DECIMAL 而不是 FLOAT,金额计算不能有浮点误差。联系人表里的is_primary保证一个客户在列表页能显示一个主联系方式,避免展示时随机取一条。
2.2 客户查重与合并:上线前必须做的两个接口
客户重复是 CRM 最常见的脏数据。功能清单里必须包含“查重”和“合并”两个动作,否则系统跑半年就得停机清洗。查重一般按名称、电话、税号做模糊匹配,合并则是把重复客户的联系人、商机、跟进记录迁移到主客户下,再软删除被合并方。
# 客户查重:按名称相似 + 电话精确双通道 def find_duplicate_accounts(name, phone, owner_id): # 通道一:同销售下电话完全一致,直接判定重复 exact = db.query( "SELECT id, name FROM crm_account " "WHERE owner_id = %s AND id IN " "(SELECT account_id FROM crm_contact WHERE phone = %s)", (owner_id, phone) ) if exact: return exact # 通道二:名称去空格后相似度大于 0.8 candidates = db.query( "SELECT id, name FROM crm_account WHERE owner_id = %s", (owner_id,) ) result = [] for row in candidates: if similar(strip(name), strip(row["name"])) > 0.8: result.append(row) return result逻辑说明:先走电话精确匹配,因为电话重复几乎可以确定是同一主体,速度快且误判低;再走名称相似度兜底,处理“某某科技”和“某某科技有限公司”这类情况。similar可以用编辑距离或分词后的 Jaccard 相似度,阈值 0.8 是我在几个项目里试出来比较平衡的值,调低会误报,调高会漏报。
合并接口要注意:不要物理删除被合并客户,而是打一个merged_into字段指向主客户,这样历史跟进记录还能追溯。合并时商机的account_id要批量更新,联系人的account_id同理,操作放在一个事务里,失败就整体回滚。
3. 销售流程与跟进记录:让功能清单跑起来
客户数据建好只是静态的,CRM 真正产生价值是在销售流程和跟进记录上。这一块的功能清单如果只写“支持跟进”,落地时一定扯皮——跟进到底记什么、阶段怎么流转、提醒什么时候发,都得有明确规则。
3.1 销售阶段流转:状态机比自由字段可靠
商机的stage字段如果允许销售随便改,数据就没法做预测。常见做法是定义一个状态机,规定只能从“初步”到“方案”到“报价”到“赢单/输单”,不能跳级也不能回退,回退需要主管审批。状态机可以用配置表驱动,避免硬编码。
// 阶段流转规则:key 是当前阶段,value 是允许的下一阶段 const STAGE_FLOW = { '初步': ['方案', '输单'], '方案': ['报价', '输单'], '报价': ['赢单', '输单'], '赢单': [], '输单': [] }; // 校验流转是否合法 function canTransfer(from, to) { const allowed = STAGE_FLOW[from] || []; return allowed.includes(to); } // 流转时写一条历史,便于统计各阶段停留时长 async function transferStage(oppId, toStage, operatorId) { const opp = await getOpportunity(oppId); if (!canTransfer(opp.stage, toStage)) { throw new Error(`不允许从 ${opp.stage} 流转到 ${toStage}`); } await db.transaction(async (tx) => { await tx.update('crm_opportunity', { stage: toStage }, { id: oppId }); await tx.insert('crm_stage_history', { opp_id: oppId, from_stage: opp.stage, to_stage: toStage, operator_id: operatorId }); }); }逻辑说明:STAGE_FLOW把规则集中在一处,业务要加阶段只改这个对象。每次流转写crm_stage_history,后期就能算“平均多少天从方案走到报价”,这是销售管理最想要的指标。参数上,operatorId必须记录,否则回退操作查不到责任人。
注意:状态机不要做成完全不可逆,实际业务里销售经常需要回退修改信息。我的做法是允许回退但要求填原因,原因字段进历史表,主管在报表里能看到回退次数异常的商机。
3.2 跟进记录:结构化字段比纯文本有用
跟进记录如果只给一个富文本框,后期没法分析。功能清单里应该把跟进拆成“跟进方式(电话/拜访/邮件)+ 跟进结果 + 下次跟进时间 + 备注”四个部分。其中“下次跟进时间”是驱动提醒的关键。
CREATE TABLE crm_follow_up ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_id BIGINT NOT NULL, opportunity_id BIGINT, way VARCHAR(16) NOT NULL, -- phone/visit/email result VARCHAR(32), -- 已接通/未接通/有意向等 next_time DATETIME, -- 下次跟进时间,用于提醒 remark TEXT, owner_id BIGINT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_next_time (next_time), -- 提醒任务扫描用 KEY idx_account (account_id) );逻辑说明:idx_next_time是给定时任务用的,每分钟扫一次next_time落在未来五分钟内的记录,推送给对应owner_id。way和result用短字符串而不是枚举,方便业务扩展,但要在应用层做白名单校验。
参数上,next_time允许为空,表示不需要提醒;opportunity_id允许为空,表示这次跟进只针对客户不针对具体商机。提醒推送建议做成站内信加邮件双通道,只做站内信的话销售不看系统就漏掉了。
4. 权限、报表与集成:功能清单里最容易被低估的部分
前几章讲的是业务功能,这一章讲支撑功能。很多 CRM 项目失败不是因为业务功能少,而是权限太粗、报表太慢、集成太封闭。这三块在功能清单里往往只有一行字,实际工作量能占一半。
4.1 数据权限:按角色和归属两个维度控制
CRM 的数据权限通常有两个维度:角色决定能看哪些字段,归属决定能看哪些行。比如销售只能看自己owner_id的客户,主管能看团队成员的,管理员看全部。字段级权限则控制金额、成本这类敏感字段对普通销售隐藏。
# 数据行权限:根据当前用户角色拼接 WHERE 条件 def build_owner_filter(user): if user.role == 'admin': return "1=1" # 管理员无限制 if user.role == 'manager': # 主管看自己和下属的 ids = [user.id] + get_subordinate_ids(user.id) return f"owner_id IN ({','.join(map(str, ids))})" return f"owner_id = {user.id}" # 销售只看自己逻辑说明:把权限拼成 SQL 条件是最直接的做法,但要注意ids必须来自可信来源,不能由前端传入,否则就是越权漏洞。字段级权限建议在序列化层做,返回给前端前把无权字段置空或删除,而不是靠前端隐藏。
参数上,get_subordinate_ids建议加缓存,团队大了之后每次查组织树会很慢。缓存失效时机是组织架构变更时,不要设固定过期时间。
4.2 报表与集成:先定指标口径再写 SQL
报表功能清单里最常被忽略的是“口径定义”。同一个“成交额”,是按商机创建时间算还是按赢单时间算,结果差很多。我的做法是每个报表指标都写一句口径说明,和 SQL 一起进版本管理。
-- 月度成交额:按赢单时间归属月份,只统计赢单商机 SELECT DATE_FORMAT(h.to_stage_time, '%Y-%m') AS month, SUM(o.amount) AS deal_amount FROM crm_opportunity o JOIN crm_stage_history h ON h.opp_id = o.id AND h.to_stage = '赢单' WHERE o.stage = '赢单' GROUP BY month ORDER BY month;逻辑说明:用crm_stage_history的赢单时间而不是商机的updated_at,因为商机赢单后可能还会被修改其他字段,updated_at会变,导致金额归属月份漂移。这是踩过的坑,报表对不上账时查了很久。
集成方面,功能清单里至少要留出 Webhook 和开放 API 两类出口。Webhook 用于客户创建、阶段变更时推送给外部系统;开放 API 用于外部系统反查客户。两者都要有签名和限流,否则容易被刷。
5. 避坑与排查:CRM 上线后最常见的五个问题
这一章按“现象 → 原因 → 解决”写,都是实际运维中反复出现的。
现象一:销售说看不到自己刚建的客户。原因通常是列表查询的权限过滤条件用了缓存的组织树,新建客户后缓存没失效,或者owner_id写入时取了当前登录人但登录态过期取了默认值。解决:检查owner_id落库值是否正确,再检查权限缓存失效逻辑,组织树缓存建议在用户登录时刷新。
现象二:客户列表翻到后面几页越来越慢。原因是用了LIMIT offset, size深分页,offset 很大时数据库要扫描大量行。解决:改成基于id的游标分页,WHERE id > last_id ORDER BY id LIMIT size,前端记录上一页最后一个 id。
现象三:跟进提醒重复推送。原因是定时任务扫描next_time时没有标记已推送,多实例部署时每个实例都扫到同一条。解决:加一个remind_status字段,推送前用UPDATE ... WHERE remind_status = 0抢占,影响行数为 1 才推送。
现象四:合并客户后商机金额统计翻倍。原因是合并时把被合并客户的商机account_id更新了,但报表按account_id关联时没排除被合并客户,导致同一批商机被算两次。解决:被合并客户打merged_into标记,所有报表查询加WHERE merged_into IS NULL。
现象五:导入 Excel 后客户名带不可见空格,查重失效。原因是 Excel 复制粘贴常带全角空格或零宽字符。解决:入库前统一做strip加不可见字符过滤,查重时也对两侧做同样处理,不要只在展示层处理。
6. 从功能清单到可验证的验收脚本
功能清单写得再全,不验证就是纸面功夫。我一般会在交付前写一组验收脚本,按模块跑一遍,确认每个功能真的能用。下面是一个用 Python 写的轻量验收脚本,覆盖客户创建、查重、阶段流转、权限过滤四个核心点。
import requests BASE = "http://localhost:8000/api" TOKEN = "test-token" # 测试环境固定 token HEADERS = {"Authorization": f"Bearer {TOKEN}"} def test_create_and_duplicate(): # 创建客户 r = requests.post(f"{BASE}/accounts", headers=HEADERS, json={"name": "测试客户A", "phone": "13800000000"}) assert r.status_code == 200, r.text acc_id = r.json()["id"] # 同电话再建一次,应触发查重提示 r2 = requests.post(f"{BASE}/accounts", headers=HEADERS, json={"name": "测试客户A副本", "phone": "13800000000"}) assert r2.status_code == 409, "查重未生效" return acc_id def test_stage_transfer(acc_id): # 建商机 r = requests.post(f"{BASE}/opportunities", headers=HEADERS, json={"account_id": acc_id, "stage": "初步", "amount": 1000}) opp_id = r.json()["id"] # 合法流转 r = requests.post(f"{BASE}/opportunities/{opp_id}/transfer", headers=HEADERS, json={"to_stage": "方案"}) assert r.status_code == 200 # 非法跳级,应被拒绝 r = requests.post(f"{BASE}/opportunities/{opp_id}/transfer", headers=HEADERS, json={"to_stage": "赢单"}) assert r.status_code == 400, "跳级未被拦截" def test_permission_filter(): # 用销售身份查列表,返回的 owner_id 应只有自己 r = requests.get(f"{BASE}/accounts", headers=HEADERS) for item in r.json()["list"]: assert item["owner_id"] == 1, "越权看到他人客户"逻辑说明:test_create_and_duplicate验证查重接口返回 409 而不是静默创建;test_stage_transfer验证状态机既放行合法流转也拦截跳级;test_permission_filter验证行级权限没有漏。参数上,BASE和TOKEN从环境变量读更好,这里为了可读性写死。
这套脚本我一般挂在 CI 里,每次改权限或状态机相关代码就跑一遍。教训是:CRM 的 bug 很少是崩溃式的,大多是“数据悄悄错了”,比如权限漏了一条、金额算重了一次,等业务发现时已经过去几个月。所以验收脚本要覆盖数据正确性,而不只是接口通不通。希望帮到你。
本文还有配套的精品资源,点击获取