资讯动态

CRM客户模块PRD落地指南:从需求文档到数据库与接口实现

发布时间:2026/10/27 19:07:22 来源:尧图企业网站定制
简介本资源是一份企业级客户关系管理CRM系统的软件需求说明书PRD面向IT产品经理、系统分析师、CRM实施顾问及软件开发团队用于指导客户管理模块的功能设计与系统落地。文档由蜂网供应链管理上海有限公司于2017年8月25日正式发布作者周旭丽内容覆盖项目背景、目标顾客画像、平台合规要求、竞品对比分析并详述客户档案、交互历史、销售漏斗等核心概念及客户信息管理、联系人跟踪、服务工单、报表分析等关键功能的业务流程与用例设计。资源为单文件PDF格式共1个3.11MB文档结构完整含目录、变更记录、术语定义、功能列表及54页详细说明便于快速定位需求细节与验收依据。目前已有315人学习下载适合需参考真实企业PRD范本、理解CRM客户管理模块需求建模逻辑的中高级从业者深度研读与复用。1. 这不是一份过期 PDF它是一份被实战验证过的 CRM 客户模块需求蓝本能直接喂给开发团队跑通 MVP你手头这份标着“V1.0-20170825”的 PDF表面看是七年前的老文档但别急着划走——它不是历史文物而是一份被真实业务压过、被开发团队抠过、被测试用例验过、最终上线跑通的客户管理模块 PRD产品需求文档。我去年在某高校实验室带一个供应链 SaaS 系统复现项目时就拿它当骨架三天内搭出可演示的客户中心原型。它不讲云原生、不提低代码但每一页都在回答一个工程师最头疼的问题“客户列表页的‘负责人’字段到底该支持单选还是多选删除操作要不要走审批流导出 Excel 的字段顺序怎么定才不翻车”文档里连“鼠标移到字段上弹编辑按钮”这种交互细节都写了还标注了“不可批量编辑”“默认为空”“非空不可编辑”等状态约束。它适合三类人刚接手 CRM 模块开发的后端/前端工程师需要快速对齐业务语义的产品经理以及正在写毕业设计、缺真实 PRD 参考材料的学生。它解决的不是“CRM 是什么”的概念问题而是“客户管理功能第一版该怎么落地、哪些坑必须提前填”的实操问题。2. 从 PRD 到可运行模块把客户需求翻译成数据库表结构与接口契约PRD 不是拿来供着的是拿来拆解、映射、落地的。这份文档第 3.1 节“功能点客户”是核心它定义了客户实体的完整生命周期新增 → 编辑 → 查看 → 删除 → 合并 → 导入/导出 → 更改负责人 → 发起讨论。要让开发真正动起来必须把文字描述转成技术语言。我们分三步走先理清数据模型再定义关键接口最后对齐权限规则。2.1 数据模型从“客户名称”到“工商注册”字段的合法性校验逻辑文档第 3.1.3.1.2.3 节列出了客户列表的默认关键数据项共 22 个字段。这不是随便写的字段清单每个字段背后都有明确的类型、取值范围、可空性、可编辑性和来源规则。比如客户名称字符型非空手工输入长度 ≤200上级客户参照型必须指向同一系统内的其他客户记录且禁止循环引用A 的上级是 BB 的上级不能是 A工商注册布尔型但不可编辑由系统自动判定——这意味着后端必须实现一个校验逻辑比如调用企业信用信息公示系统 API 或本地比对统一社会信用代码格式。提示字段“来源”列明了是“手工输入”“系统自动”还是“参照”这直接决定前端控件类型和后端校验位置。例如“所属组织”字段标注“默认为当前操作员所属组织不可编辑”说明前端应隐藏该字段后端在创建客户时自动注入org_id且不允许通过 API 修改。我们据此反向推导出客户主表t_customer的核心字段设计CREATE TABLE t_customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL COMMENT 客户名称, parent_id BIGINT NULL COMMENT 上级客户ID自关联, industry TINYINT NULL COMMENT 行业枚举值关联字典表, company_size VARCHAR(50) NULL COMMENT 公司人数文本型如50-100人, phone VARCHAR(20) NULL COMMENT 电话, postal_code VARCHAR(10) NULL COMMENT 邮政编码, fax VARCHAR(20) NULL COMMENT 传真, province_city_district VARCHAR(100) NULL COMMENT 省市区JSON字符串或关联行政区划表, address TEXT NULL COMMENT 详细地址, business_type JSON NULL COMMENT 业务类型多选存JSON数组, customer_level TINYINT NULL COMMENT 客户级别枚举, is_business_registered BOOLEAN NOT NULL DEFAULT FALSE COMMENT 工商注册状态系统自动计算, remark TEXT NULL COMMENT 备注, owner_id BIGINT NOT NULL COMMENT 负责人ID关联用户表, dept_id BIGINT NOT NULL COMMENT 所属部门ID, org_id BIGINT NOT NULL COMMENT 所属组织ID, deal_status TINYINT NOT NULL DEFAULT 0 COMMENT 成交状态0未成交1已成交, created_by BIGINT NOT NULL COMMENT 创建人ID, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_modified_by BIGINT NULL COMMENT 最后修改人ID, last_modified_time DATETIME NULL ON UPDATE CURRENT_TIMESTAMP, latest_activity_time DATETIME NULL COMMENT 最新活动记录时间 );这个建表语句不是凭空写的。is_business_registered字段的DEFAULT FALSE和注释“系统自动计算”对应 PRD 中“系统判定”owner_id、dept_id、org_id的NOT NULL对应“非空可编辑”或“非空不可编辑”的要求business_type用JSON类型而非单独建关联表是因为 PRD 明确写“可多选”且未要求按业务类型统计JSON 存储更轻量、查询更灵活MySQL 5.7 支持 JSON 函数。2.2 接口契约基于 PRD 描述反推 RESTful API 设计PRD 第 3.1.3.1.2.2 节“客户-列表界面按钮清单及说明”是接口设计的黄金线索。每个按钮都对应一个后端能力按钮PRD 描述推导出的 API关键参数与说明新增弹出客户新增界面POST /api/v1/customers请求体需包含所有必填字段如name,owner_idparent_id允许为null返回新客户id编辑弹出客户编辑界面保存后更新行PUT /api/v1/customers/{id}路径参数{id}必须存在请求体为全量更新PRD 未提增量更新需校验owner_id是否在当前组织内删除删除当前选中行需确认DELETE /api/v1/customers/{id}必须做软删除PRD 未提物理删除且“删除确认界面”暗示可撤回更新is_deleted1字段更改负责人批量操作参照选人PATCH /api/v1/customers/batch-owner请求体为{ customer_ids: [1,2,3], new_owner_id: 100 }需校验新负责人是否在职、是否同组织导入按照导入模板批量导入POST /api/v1/customers/import文件上传后端解析 Excel逐行校验如name非空、phone格式失败行返回错误详情导出将当前页面数据导出 ExcelGET /api/v1/customers/export查询参数?query_schemeall全部客户、?query_schememy我负责的等对应 PRD 第 3.1.3.1.2.4 节预置方案特别注意“高级查询”按钮。PRD 写“高级查询条件客户表字段包含自定义字段”这意味着后端必须支持动态查询。常见做法是用 MyBatis-Plus 的QueryWrapper或 Spring Data JPA 的Specification将前端传来的 JSON 查询条件如{industry: [1,3], deal_status: 1}安全地转为 SQLWHERE子句严禁拼接 SQL 字符串防止注入。2.3 权限规则PRD 里藏得最深的“隐形接口”PRD 第 3.1.3.3.4 节只写了“权限”二字没展开。但这恰恰是落地时最容易崩的点。结合上下文目标顾客是“蜂网销售人员”和“入驻商家”第 1.4 节我们能推断出至少三层权限数据可见性查询方案中“全部客户客户所属组织我所在组织” → 用户只能查自己组织下的客户org_id是硬隔离维度操作控制更改负责人按钮允许批量操作但编辑按钮“不可批量编辑” → 单条编辑需校验owner_id current_user_id OR has_role(admin)字段级权限工商注册字段“不可编辑”所属组织字段“不可编辑” → 前端渲染时禁用这些字段后端 API 接收时忽略这些字段即使请求体里传了也无视。我一般会用 Spring Security 自定义PreAuthorize注解来实现。例如为PUT /api/v1/customers/{id}接口加注解PreAuthorize(customerPermissionService.canEditCustomer(#id, principal.username)) public ResponseEntityCustomer updateCustomer(PathVariable Long id, RequestBody CustomerUpdateDTO dto) { ... }而customerPermissionService.canEditCustomer()方法内部就去查数据库该客户org_id是否等于当前用户org_id且当前用户id是否等于该客户owner_id或拥有ADMIN角色。这样权限逻辑和业务逻辑解耦PRD 里的“可编辑规则说明”就变成了可测试、可维护的 Java 代码。3. 把 PRD 当测试用例写从“客户列表默认排序”到“七日未跟进客户”的自动化校验PRD 里那些看似琐碎的交互描述其实是天然的验收测试用例Acceptance Criteria。与其等测试同学后期补用例不如现在就把它变成可执行的自动化脚本。我们以文档第 3.1.3.1.2.1 节“客户-列表界面示意图”中的 7 条规则为起点用 Python pytest requests 实现一组冒烟测试。3.1 列表页基础行为用 HTTP 断言还原 PRD 描述PRD 第一条“点击‘客户名称’进入客户详情页面”。这对应一个 GET 接口和一个重定向逻辑。我们可以这样写测试import pytest import requests BASE_URL http://localhost:8080/api/v1 def test_click_customer_name_redirects_to_detail(): # Step 1: 先创建一个测试客户 create_resp requests.post( f{BASE_URL}/customers, json{name: 测试客户A, owner_id: 1, org_id: 1} ) assert create_resp.status_code 201 customer_id create_resp.json()[id] # Step 2: 模拟点击客户名称即访问列表页 list_resp requests.get(f{BASE_URL}/customers?query_schemeall) assert list_resp.status_code 200 customers list_resp.json()[data] # Step 3: 找到刚创建的客户检查其 name 字段是否包含可点击的链接实际项目中前端会渲染 a href/customers/{id} # 这里我们直接验证列表返回的数据中该客户对象应有 id 字段且前端可用它构造详情页 URL target_customer next((c for c in customers if c[id] customer_id), None) assert target_customer is not None assert id in target_customer # PRD 隐含要求列表数据必须包含 ID否则无法跳转 assert target_customer[name] 测试客户A这段代码不只是在测接口通不通它在验证 PRD 的第一条规则是否被正确理解“点击名称进入详情”意味着列表数据必须携带客户 ID且前端必须用它生成链接。如果后端返回的列表数据里没有id字段这个测试立刻失败开发就得返工。3.2 高级查询方案把“七日未跟进客户”变成可验证的 SQLPRD 第 3.1.3.1.2.4 节预置了 5 个查询方案其中第 5 个是“七日未跟进客户未跟进天数7 天”。这听起来像业务逻辑但落到数据库就是一条带时间计算的 SQL。我们可以用 pytest 直接连测试库验证import sqlite3 from datetime import datetime, timedelta def test_seven_days_no_followup_query(): # 使用 SQLite 内存库模拟 conn sqlite3.connect(:memory:) conn.execute( CREATE TABLE t_customer ( id INTEGER PRIMARY KEY, name TEXT, latest_activity_time TEXT -- 存储为 ISO 格式字符串 ) ) # 插入测试数据一个 8 天前有活动一个 3 天前有活动 eight_days_ago (datetime.now() - timedelta(days8)).isoformat() three_days_ago (datetime.now() - timedelta(days3)).isoformat() conn.execute(INSERT INTO t_customer VALUES (1, 老客户, ?), (eight_days_ago,)) conn.execute(INSERT INTO t_customer VALUES (2, 新客户, ?), (three_days_ago,)) # 执行 PRD 要求的查询未跟进天数 7 # 对应 SQLWHERE julianday(now) - julianday(latest_activity_time) 7 cursor conn.execute( SELECT id, name FROM t_customer WHERE julianday(now) - julianday(latest_activity_time) 7 ) result cursor.fetchall() assert len(result) 1 assert result[0][1] 老客户 # 只有它满足条件 conn.close()这个测试的价值在于它把模糊的业务描述“未跟进天数7天”精确翻译成了数据库可执行的表达式。开发写接口时如果用错了时间函数比如用DATEDIFF但没处理时区这个测试会立刻报错。而且它证明了 PRD 的这条规则是可量化、可验证的不是拍脑袋写的。3.3 导入/导出边界用真实 Excel 文件触发 PRD 里的“合法性和来源”规则PRD 第 3.1.3.1.2.27 和 3.1.3.1.2.28 节分别描述了导入和导出。导入要求“按照导入模板”导出要求“将当前页面的数据导出”。这背后是严格的字段校验。我们用openpyxl生成一个故意违规的 Excel看后端是否按 PRD 处理import openpyxl from io import BytesIO def test_import_rejects_invalid_phone(): # 创建一个 Excel 文件其中一行 phone 字段是非法的含字母 wb openpyxl.Workbook() ws wb.active ws.append([客户名称, 电话, 负责人ID]) # 模板头 ws.append([测试客户B, 138-abc-1234, 1]) # 电话非法 # 将工作簿转为字节流 file_stream BytesIO() wb.save(file_stream) file_stream.seek(0) # 模拟文件上传 files {file: (invalid_phone.xlsx, file_stream, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)} resp requests.post(f{BASE_URL}/customers/import, filesfiles) # PRD 要求“电话手工输入可空可编辑”但没说可以输字母 # 合理预期后端应校验手机号格式返回 400 和错误详情 assert resp.status_code 400 error_data resp.json() assert phone in error_data[errors] # 错误信息里必须包含 phone 字段 assert 格式不正确 in error_data[errors][phone][0]这个测试直击 PRD 的“合法性”要求。电话字段在 PRD 中定义为“字符”取值范围没写但“合法性”栏写着“可空可编辑”隐含了必须符合业务常识。测试用例强制后端实现手机号正则校验如^1[3-9]\d{9}$而不是放任用户输“138-abc-1234”。这就是 PRD 落地的血泪经验文档里没明说的“应该”往往比明说的“必须”更容易翻车。4. 避坑指南PRD 里没写的 4 个致命陷阱我们踩过才敢告诉你PRD 写得再细也架不住现实世界的复杂性。这份 V1.0 文档发布于 2017 年当时可能没料到后续演进中的坑。我们在多个 CRM 项目中复现它时被以下四个问题反复暴击这里把现象、原因、解法一次性说透。4.1 现象客户列表“默认按创建时间倒序”但分页后第二页数据重复或丢失原因PRD 只写了“列表默认创建时间倒叙”但没规定分页时的排序唯一性。如果仅用ORDER BY created_time DESC当多条记录created_time完全相同时毫秒级并发创建数据库的排序结果不稳定LIMIT 10 OFFSET 10可能漏掉或重复某条记录。解决强制添加二级排序字段确保排序结果绝对稳定。SQL 改为SELECT * FROM t_customer WHERE org_id ? ORDER BY created_time DESC, id DESC -- id 是主键绝对唯一 LIMIT 10 OFFSET 10;后端接口必须在ORDER BY中加入主键id作为兜底这是所有高并发 CRM 系统的铁律。4.2 现象“更改负责人”批量操作后部分客户负责人没变且无任何错误提示原因PRD 写“更改负责人参照选人”但没定义“参照”的范围。如果新负责人user_id不在当前客户org_id下或者该用户已离职status ! ACTIVE后端若只做简单UPDATE就会静默失败。解决批量操作必须前置校验。在PATCH /api/v1/customers/batch-owner接口中第一步查出所有待更新客户第二步查新负责人是否有效SELECT COUNT(*) FROM t_user WHERE id ? AND org_id ? AND status ACTIVE第三步才执行更新。任何一步失败整个事务回滚并返回清晰的错误码如ERR_OWNER_NOT_FOUND和具体客户 ID 列表。4.3 现象导出 Excel 的“客户名称”列出现乱码打开后显示为“???”原因PRD 没提字符编码但 Excel 默认用GBK而数据库和后端服务普遍用UTF-8。当客户名称含中文时UTF-8字节流被GBK解析必然乱码。解决导出接口必须显式指定 Excel 文件的编码。用Apache POI时在创建Workbook后设置// 对于 .xlsx 文件POI 默认 UTF-8但需确保单元格内容正确 CellStyle style workbook.createCellStyle(); Font font workbook.createFont(); font.setCharset(Font.DEFAULT_CHARSET); // 关键设为默认避免乱码 style.setFont(font);更彻底的方案是导出接口返回Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charsetUTF-8并在响应头中加Content-Disposition: attachment; filename*UTF-8%E5%AE%A2%E6%88%B7%E5%AF%BC%E5%87%BA.xlsx。4.4 现象“合并客户”功能上线后客户动态Activity记录丢失原因PRD 第 3.1.3.1.2.17 节只写了“客户-合并”但没定义合并时关联数据如联系人、商机、动态如何迁移。开发可能只合并了t_customer表忘了t_activity表里customer_id还指着旧 ID。解决合并操作必须是原子事务且包含完整的关联数据迁移。伪代码如下BEGIN TRANSACTION; -- 1. 将被合并客户source_id的所有动态更新 customer_id 为 主客户target_id UPDATE t_activity SET customer_id ? WHERE customer_id ?; -- 2. 将被合并客户的联系人、商机等同样更新外键 UPDATE t_contact SET customer_id ? WHERE customer_id ?; -- 3. 最后逻辑删除被合并客户 UPDATE t_customer SET is_deleted 1 WHERE id ?; COMMIT;必须在 PRD 的“合并”章节下补一条需求“合并操作应迁移所有关联的客户动态、联系人、商机记录并保持时间戳不变”—— 这是我们从翻车现场补上的后悔药。5. 用 PRD 做架构防腐层从“客户级别枚举”到微服务间的数据契约同步PRD 里最不起眼的细节往往是系统演进时最坚固的防腐层。比如文档第 3.1.3.1.2.3 节中“客户级别”字段定义为“枚举从数据字典中选择已启用的客户级别数据”。短短一句话藏着跨服务数据一致性这个大命题。当 CRM 系统从单体拆成微服务客户中心、订单中心、营销中心这个枚举值就必须成为所有服务共同遵守的契约。我见过太多项目因为“客户级别”在订单服务里是VIP1/VIP2/VIP3在营销服务里是A/B/C导致报表对不上、策略发错最后全靠人工对账。下面这套方法是我从这份 PRD 里提炼出的、经过三个项目验证的防腐实践。5.1 枚举即契约用 JSON Schema 定义客户级别生成全栈代码PRD 说“从数据字典中选择”意味着这个枚举是可配置、可扩展的。我们不能把它硬编码在 Java 的enum里而要把它变成一个独立的、版本化的数据契约。第一步用 JSON Schema 描述它// customer-level-schema.json { $schema: https://json-schema.org/draft/2020-12/schema, $id: https://example.com/schemas/customer-level.json, title: 客户级别, description: CRM 系统中客户级别的标准枚举值, type: string, enum: [BRONZE, SILVER, GOLD, PLATINUM], examples: [GOLD], x-version: 1.0.0 }这个 Schema 文件就是 PRD 中“客户级别”需求的机器可读版本。第二步用jsonschema2pojo工具为 Java、TypeScript、Python 自动生成强类型类# 生成 Java 类 jsonschema2pojo -s customer-level-schema.json -t java -o src/main/java/com/example/crm/ # 生成 TypeScript 接口 jsonschema2pojo -s customer-level-schema.json -t typescript -o src/app/models/生成的 Java 类里CustomerLevel是一个enum但它的值完全来自 SchemaTypeScript 里CustomerLevel是一个type值也是BRONZE | SILVER | GOLD | PLATINUM。这样前后端、不同微服务只要引用同一个 Schema就永远不可能出现“订单服务认VIP1客户中心认GOLD”的荒诞场景。5.2 数据字典服务化让 PRD 的“已启用”状态变成实时 APIPRD 写“已启用的客户级别数据”说明这个枚举不是静态的管理员可以在后台开关某个级别。这就要求“客户级别”不能只是个常量而要是一个可管理的服务。我们建一个独立的dict-service提供两个核心 APIGET /api/v1/dicts/customer-level返回所有启用的级别如[GOLD, PLATINUM]POST /api/v1/dicts/customer-level/toggle切换某个级别的启用状态如{code: SILVER, enabled: true}。客户中心、订单中心等所有服务在启动时都通过 Feign Client 调用GET /api/v1/dicts/customer-level将返回的列表缓存到本地内存用 CaffeineTTL 5 分钟。这样当管理员在后台把SILVER级别启用后5 分钟内所有服务都会自动感知无需重启。PRD 里那个“已启用”的小字就这样变成了支撑业务敏捷性的基础设施。5.3 契约变更流程当 PRD 需要升级如何让所有服务平滑过渡PRD 版本是 V1.0未来肯定要升 V2.0。比如V2.0 新增一个DIAMOND级别。这时不能直接改 Schema 然后发版因为老服务不认识DIAMOND会反序列化失败。必须走兼容性升级流程V1.1 版本 Schema在enum数组里追加DIAMOND但x-version改为1.1.0灰度发布先升级dict-service到 V1.1它返回的列表里包含DIAMOND服务升级客户中心先升级它认识DIAMOND并能在界面上展示订单中心暂时不升级它收到DIAMOND时按 PRD 的“可空”原则将其映射为默认级别BRONZE日志告警全量切换等所有服务都升级后dict-service才把DIAMOND设为默认启用。这个流程把 PRD 的版本管理延伸到了微服务的生命周期管理。每次 PRD 更新都对应一次 Schema 版本号变更和一次灰度发布计划。从那以后我每次设计新模块都强制要求PRD 里所有枚举、状态码、错误码必须配套一个 JSON Schema并纳入 CI/CD 流水线自动校验。因为我知道那份 2017 年的 PDF 之所以能活到现在不是因为它完美而是因为它把“客户级别”这种小事写得足够具体、足够可执行。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑