简介:这份PDF是面向产品经理、需求分析师及CRM系统开发团队的客户管理软件需求说明书,聚焦企业如何通过信息化手段优化销售、营销与服务流程,提升客户满意度与忠诚度。文档由周旭丽编写、蜂网供应链管理(上海)有限公司于2017年8月正式发布,内容覆盖项目背景与目标、目标顾客、平台政策、竞品分析,并系统梳理客户档案、交互历史、销售漏斗等核心概念,以及客户信息录入、联系人跟踪、销售预测、市场营销自动化、客户服务与报表分析等功能模块的业务流程、界面设计与性能指标,同时涉及数据集成、移动支持、安全接口与变更控制等要求。资源包共1个PDF文件,大小约3.11MB,页面结构完整、目录清晰,便于按章节检索。已有316人学习,适合需要参考完整需求文档范式、梳理CRM功能边界或撰写同类PRD的读者借鉴使用。
1. 拿到一份 54 页的 CRM 客户管理 PRD,我第一件事是拆字段
2017 年 8 月定稿的这份《客户关系管理 CRM_客户管理_软件需求说明书 V1.0》,54 页里真正能落成代码的东西,其实集中在第 3 章「功能点:客户」——客户列表、新增、编辑、详情、合并、导入导出、团队成员、层级关系,外加一张关键数据项表。它不是一份讲 CRM 理念的科普材料,而是一份可以直接对着建表、对着画原型、对着写接口的需求底稿。适合谁?适合正在做 CRM 客户主数据模块的后端、需要把 PRD 翻译成接口文档的产品技术、以及想拿一份真实需求练手做二次开发的人。我拆它的顺序从来不是从头读,而是先抓数据项,再倒推页面和流程,因为字段定不下来,后面全是返工。
2. 需求底稿怎么读:从功能列表到客户主数据的映射
2.1 文档骨架与优先级约定
这份文档的结构是典型的 PRD 写法:项目介绍、方案概述、产品详细需求、预置数据、遗留问题。真正有信息量的是第 3 章,前面两章更多是背景交代。读的时候我一般先看 2.5 功能列表,因为它用 P1/P2/P3 标了优先级,P1 是最高优先,P2、P3 视开发资源和上线时间可以推到下一期。这份文档里客户菜单整体标的是 P1,意味着客户主数据是第一期必须交付的东西,没有商量余地。
再看 1.3 项目目标,一期明确要做的是两件事:客户资料管理,以及客户销售活动、计划安排、联系人、商机的管理。二期才考虑邮件电话短信自动读取、线索获取分配、统计分析、销售漏斗。这个分期很关键,它决定了你建表时哪些字段可以先留空、哪些关联表可以晚点建。我见过不少团队一上来就把销售漏斗、统计分析全铺开,结果一期连客户列表都没跑顺,血泪经验就是先把 P1 做扎实。
文档里还有几个容易被跳过的点:1.1 需求来源说明这是「直接为 PRD 等级」,不需要 BRD 或 MRD 支撑;1.4 目标顾客写的是内部销售人员加入驻商家两类角色。这两条直接影响到后面的权限设计——内部销售和商家看到的客户范围、能改的字段是不一样的,这一点在 3.1.3.3.4 权限那一节还会再提。
2.2 客户主数据的关键字段拆解
3.1.3.1.2.3 那张「客户列表-默认关键数据项」表,是整份文档最值钱的部分。它把每个字段的类型、取值范围、是否可空、可编辑性、来源都写清楚了,这种颗粒度在 PRD 里算相当良心。我把它整理成下面这张表,方便直接对着建库:
| 字段 | 类型 | 可空 | 可编辑 | 来源 | 备注 |
|---|---|---|---|---|---|
| 客户名称 | 字符 | 非空 | 可编辑 | 手工输入 | 不超过 200 字符 |
| 上级客户 | 参照 | 可空 | 可编辑 | 手工输入 | 不允许互为上下级 |
| 行业 | 枚举 | 可空 | 可编辑 | 数据字典 | 取已启用行业 |
| 公司人数 | 字符 | 可空 | 可编辑 | 手工输入 | — |
| 电话 / 传真 / 邮编 | 字符 | 可空 | 可编辑 | 手工输入 | — |
| 省市区 | 参照 | 可空 | 可编辑 | 行政区划 | — |
| 地址 | 字符 | 可空 | 可编辑 | 手工输入 | — |
| 业务类型 | 枚举 | 可空 | 可编辑 | 数据字典 | 可多选 |
| 客户级别 | 枚举 | 可空 | 可编辑 | 数据字典 | — |
| 工商注册 | 布尔 | 非空 | 不可编辑 | 系统判定 | 系统操作状态判断 |
| 负责人 | 参照 | 非空 | 可编辑 | 选人 | — |
| 所属部门 | 参照 | 非空 | 可编辑 | 默认当前用户部门 | — |
| 所属组织 | 参照 | 非空 | 不可编辑 | 系统自动 | 默认当前操作员组织 |
| 成交状态 | 枚举 | 非空 | 可编辑 | 手工输入 | 默认未成交,未成交/已成交 |
| 创建人 / 创建时间 | — | 非空 | 不可编辑 | 系统自动 | 新建时写入 |
| 最后修改人 / 时间 | — | 非空 | 不可编辑 | 系统自动 | 修改时写入 |
| 最新活动记录时间 | 日期时间 | — | 不可编辑 | 系统自动 | 活动记录保存时更新 |
这张表里有三个地方值得单独拎出来说。第一是「上级客户」的约束——不允许互为上下级,这意味着你在写保存逻辑时必须做一次环检测,不能只校验「上级不能等于自己」。第二是「工商注册」标了系统判定、不可编辑,说明这个字段不是用户填的,而是系统根据某种状态算出来的,具体算法文档没写,常见做法是拿客户名称去比对工商注册库或者内部白名单,命中就置 true。第三是「所属组织」不可编辑但非空,默认取当前操作员所属组织,这其实是多租户隔离的雏形,后面做数据权限时全靠它。
2.3 查询方案与列表交互的落地含义
3.1.3.1.2.4 列了五个预置查询方案:全部客户、我负责的客户、我参与的客户、我关注的客户、七日未跟进客户。这五个方案不是随便列的,它们对应五种不同的数据过滤逻辑,直接决定你 SQL 里 where 条件怎么写:
- 全部客户:客户所属组织 = 我所在组织
- 我负责的客户:客户负责人 = 我本人
- 我参与的客户:团队成员包含我,且负责人不等于我
- 我关注的客户:我已加关注
- 七日未跟进客户:未跟进天数 > 7
前四个是纯关联查询,第五个需要算「未跟进天数」,也就是当前时间减去最新活动记录时间。这里有个坑:如果客户从来没有活动记录,最新活动记录时间是空的,那这个客户算不算「七日未跟进」?文档没明说,我一般按「创建时间超过 7 天且无活动记录」来处理,否则新客户一建出来就进未跟进列表,销售会炸。
列表交互上还有几条细节:默认按创建时间倒序;列表显示列可自定义,但客户名称是必选字段;客户名称搜索是对全部可见数据做模糊搜索;鼠标移到某字段上出现编辑按钮,失焦保存。最后这条意味着列表页要做行内编辑,不是点进详情才能改,这对前端是个不小的活,接口上要支持单字段 PATCH,而不是整行 PUT。
3. 把 PRD 跑成原型:客户列表、新增与详情的实现路径
3.1 客户列表接口与查询方案的分支逻辑
列表页是整个模块的入口,3.1.3.1.2.1 和 3.1.3.1.2.2 把交互和按钮都写全了:新增、编辑、删除(支持批量)、更改负责人(支持批量)、导入、高级查询、导出、搜索。其中编辑不可批量,删除和更改负责人可批量,这个差异要在接口设计上体现出来——批量删除走一个 ids 数组,单行编辑走单条记录。
查询方案的分支我一般放在服务层,用一个枚举区分,避免把一堆 if 塞进 controller:
# 查询方案枚举与过滤条件构造(示意) from enum import Enum class QueryScheme(Enum): ALL = "all" # 全部客户:所属组织 = 我所在组织 MINE = "mine" # 我负责的:负责人 = 我 JOINED = "joined" # 我参与的:团队成员含我且负责人不是我 FOLLOWED = "followed" # 我关注的:我已加关注 STALE_7D = "stale_7d" # 七日未跟进:未跟进天数 > 7 def build_filters(scheme, current_user): base = [("org_id", "=", current_user.org_id)] # 组织隔离是底线 if scheme == QueryScheme.ALL: return base if scheme == QueryScheme.MINE: return base + [("owner_id", "=", current_user.id)] if scheme == QueryScheme.JOINED: # 团队成员表子查询,且排除自己负责的 return base + [ ("id", "in", "SELECT customer_id FROM team_member WHERE user_id = :uid"), ("owner_id", "!=", current_user.id), ] if scheme == QueryScheme.FOLLOWED: return base + [("id", "in", "SELECT customer_id FROM follow WHERE user_id = :uid")] if scheme == QueryScheme.STALE_7D: # 无活动记录的按创建时间算,避免新客户误入 return base + [( "COALESCE(last_activity_at, created_at)", "<", "NOW() - INTERVAL '7 days'" )] return base这段代码的关键在两点。一是base里永远带组织隔离,这是多租户系统的底线,任何查询方案都不能绕过。二是「七日未跟进」用了COALESCE,把空的活动时间兜底成创建时间,这就是前面说的那个坑的解法。参数上current_user至少要有id、org_id两个属性,团队成员和关注关系用子查询而不是 join,是为了避免一对多导致列表出现重复行——这个细节不注意,列表分页数量会对不上。
3.2 新增与编辑:字段校验和环检测
新增界面在 3.1.3.1.2.8 到 3.1.3.1.2.10,编辑在 3.1.3.1.2.11 到 3.1.3.1.2.12。新增和编辑共用大部分字段,差异在于新增时创建人、创建时间由系统写入,编辑时只更新最后修改人和时间。真正需要写逻辑的是「上级客户」的环检测,以及客户名称的查重。
查重这块文档在 3.1.4.2 查重设置里留了口子,说明查重规则是可配置的。我一般把查重做成「按客户名称精确匹配 + 可选的电话匹配」,命中就提示「已存在相似客户,是否继续」,而不是直接拦截,因为同名不同公司的情况太常见了。
环检测的写法:
def would_create_cycle(customer_id, new_parent_id, get_parent): """检查把 new_parent_id 设为 customer_id 的上级是否会成环""" if new_parent_id is None: return False if new_parent_id == customer_id: return True # 自己不能当自己的上级 seen = set() cur = new_parent_id while cur is not None: if cur == customer_id: return True # 沿着上级链走回了自己,成环 if cur in seen: return True # 数据本身已有环,防御性返回 seen.add(cur) cur = get_parent(cur) # 查该客户的上级 id return Falseget_parent是个回调,传进来查某个客户的上级 id,这样这段逻辑不依赖具体 ORM。seen集合是防御性的——如果历史数据里已经存在环,不加这个集合会死循环。这个函数在保存前调用一次,返回 True 就拒绝保存并提示「上级客户设置会形成循环」。别小看这十几行,客户层级关系在 3.1.3.1.2.25 里是要展示的,一旦成环,层级树渲染直接崩。
3.3 客户详情页:动态、相关、团队成员三块拼图
详情页是 3.1.3.1.2.13 到 3.1.3.1.2.26 的一大段,信息密度很高。它把客户详情拆成了几个区块:基本信息、动态、相关(联系人、商机、合同、订单)、团队成员、客户层级关系。3.1.1 业务定义里说得很清楚,客户详情是「客户数据中心」,可新建和查看跟客户有关的联系人、商机、动态、合同、订单、团队成员信息。
这意味着详情页不是一个单表查询,而是一个聚合接口。我的做法是拆成多个子接口按需加载,而不是一个接口把所有关联数据全查出来:
// 详情页数据加载:分块请求,避免首屏过重 async function loadCustomerDetail(customerId) { // 基本信息 + 层级关系,首屏必须 const base = await fetch(`/api/customers/${customerId}`).then(r => r.json()); // 动态、相关、团队成员并行拉,各自分页 const [activities, relations, members] = await Promise.all([ fetch(`/api/customers/${customerId}/activities?page=1&size=20`).then(r => r.json()), fetch(`/api/customers/${customerId}/relations?type=all`).then(r => r.json()), fetch(`/api/customers/${customerId}/members`).then(r => r.json()), ]); return { base, activities, relations, members }; }base里带客户基本信息和层级关系,因为层级关系要画树,数据量不大但首屏就要。动态、相关、团队成员各自分页,activities的size=20是经验值,动态是最容易膨胀的数据,一次拉全量迟早出事。relations的type=all表示联系人、商机、合同、订单一起拉,如果某一类数据特别多,可以再拆细。这个拆分方式的好处是详情页首屏快,坏处是接口数量多,需要前端做好 loading 态。
团队成员这块在 3.1.3.1.2.24,它和「我参与的客户」查询方案是联动的——团队成员表既是权限来源,也是查询条件来源。所以团队成员的新增删除要同步考虑:删掉一个成员,他再看「我参与的客户」就不该出现这个客户了。这个联动关系在建表时就要想清楚,别等上线了才发现权限和查询对不上。
4. 避坑与排查:客户模块上线前必须过的五道坎
4.1 现象:列表分页数量对不上,翻页出现重复客户
原因:查询方案里用了 join 关联团队成员或关注表,一个客户有多个成员时产生多行,分页计数被放大。解决:关联查询一律改成IN子查询或EXISTS,不要用 join 直接拼在主查询上;如果非要用 join,先对主表分页再关联。这个坑我在两个项目里都踩过,第二次是因为换了个人写 SQL,所以现在 code review 看到客户列表的 join 就条件反射。
4.2 现象:新客户一创建就出现在「七日未跟进」列表里
原因:未跟进天数直接用NOW() - last_activity_at,新客户该字段为空,某些数据库里空值参与运算得到 NULL,排序或过滤行为不可预期;也有实现把空值当 0 天处理,反而漏掉。解决:统一用COALESCE(last_activity_at, created_at)兜底,并且明确「创建 7 天内不算未跟进」,在 SQL 里加created_at < NOW() - INTERVAL '7 days'双重条件。
4.3 现象:更改负责人后,原负责人还能在「我负责的客户」里看到
原因:更改负责人只更新了客户表的owner_id,但缓存或搜索索引没刷新,列表走的是缓存。解决:负责人变更后主动失效该客户相关的列表缓存,如果用了搜索引擎同步,要发一条变更事件;批量更改负责人时按批提交,别一条一条刷。另外要检查「我参与的客户」逻辑,原负责人如果还在团队成员里,他仍应能看到,这是符合预期的,别误删成员。
4.4 现象:客户合并后关联数据丢失或重复
原因:3.1.3.1.2.17 客户合并要求把两个客户的联系人、商机、动态等挂到保留的客户上,实现时如果只改了主表没迁关联表,或者迁移时没去重,就会出现丢失或重复。解决:合并做成事务,先迁关联表再删被合并客户,关联表迁移时按业务主键去重;合并前给两个客户都打快照,出问题能回滚。合并是不可逆操作,我一般要求二次确认并记录操作日志。
4.5 现象:导入模板字段和页面字段对不上,导入一半报错
原因:3.1.3.1.2.27 导入按模板批量导入,模板里的列名、枚举值、参照字段格式和页面不一致,比如行业在页面是下拉选 id,模板里填的是中文名。解决:导入时做一层字段映射,枚举按名称反查 id,参照字段按名称或编码匹配,匹配不到的行单独输出错误报告而不是整批失败;导入前先做一次全量校验,校验通过再入库。导入是最容易翻车的功能,我现在的习惯是导入必带「预校验」和「错误行下载」两个能力。
5. 二次开发改造:把这份 PRD 变成可扩展的客户中台
5.1 从固定字段到自定义字段的改造思路
这份 PRD 的字段是写死的,但真实业务里客户字段几乎一定会加。3.1.3.1.2.5 高级查询里已经提到「客户表字段(包含自定义字段)」,说明设计时留了这个口子。改造的核心是把客户主表拆成「固定字段表 + 自定义字段定义表 + 自定义字段值表」三张表,固定字段走强类型列,自定义字段走 EAV 或者 JSON 列。我倾向 JSON 列,查询用数据库的 JSON 函数,比 EAV 少一堆 join,代价是索引要单独建。
改造时要注意列表显示列可自定义这个需求——它要求前端能拿到字段元数据。所以自定义字段定义表里除了字段名、类型,还要有「是否列表可见」「是否必填」「排序」这些属性,列表接口返回时带上元数据,前端动态渲染列。这一步做完,后面加字段就不用改代码了。
5.2 权限模型:组织隔离加数据范围
文档里「所属组织」不可编辑、默认当前操作员组织,这是组织隔离的锚点。二次开发时我会把权限拆成两层:功能权限(能不能看客户菜单、能不能导入导出)和数据权限(能看哪些客户)。数据权限按「本人 / 本部门 / 本组织 / 全部」四档配置,查询方案里的五个预置方案其实是数据权限的快捷方式。改造时把build_filters里的组织条件抽成一个权限策略对象,不同角色注入不同策略,比在每个查询里写死org_id灵活得多。
5.3 验证改造是否成功的三个检查点
改完之后怎么验证?我一般跑三个检查点。第一,用两个不同组织的账号登录,互相看不到对方客户,且「全部客户」方案只返回本组织数据。第二,建一个三层客户层级 A→B→C,尝试把 A 的上级设成 C,必须被环检测拦住。第三,导入一份含错误枚举值的模板,必须整批不入库并给出错误行报告。这三个点过了,客户主数据的骨架就算立住了。
| 检查点 | 验证方式 | 通过标准 |
|---|---|---|
| 组织隔离 | 双组织账号交叉查询 | 互不可见,方案过滤生效 |
| 层级环检测 | 构造 A→B→C 后设 A 上级为 C | 保存被拒并提示成环 |
| 导入容错 | 导入含非法枚举的模板 | 整批回滚,输出错误行 |
这份文档是 2017 年的,很多交互细节放到今天看不算新,但它的字段表、查询方案、状态定义这些底稿质量很高,拿来当二次开发的起点完全够用。我自己的习惯是每接一个客户模块,先把关键数据项表抄一遍,再对着查询方案写过滤逻辑,最后才动页面——顺序反了,返工是必然的。希望帮到你。
本文还有配套的精品资源,点击获取