
简介这份文档面向企业信息化建设人员、CRM系统选型与实施团队以及需要梳理销售业务流程的产品与开发人员提供一套完整的客户管理系统功能清单参考。内容以计划管理、销售合同、开票、帐款、业绩考核、市场信息、促销品广告、统计报表及公共关系管理等模块为主线逐项列出三级功能点与功能描述并配有XSP编码体系便于按模块检索与对照。压缩包内共1个doc文件约376KB适合直接查阅或作为需求文档、功能对照表的底稿使用。目前已有801人学习下载说明其在CRM功能梳理场景中具备一定参考价值。读者可据此快速了解销售计划、合同变更、调拨开票、应收结转、费用报销与广告合同等业务环节的功能边界为系统设计、选型评估或二次开发提供清晰的功能框架与字段参考。1. 从一张“功能清单”到能跑的系统CRM 客户管理到底管什么很多团队拿到 CRM 需求时第一反应是画客户表、联系人表、商机表结果上线三个月发现销售最关心的回款、开票、调拨全在另一套 Excel 里。这份 CRM 客户管理系统功能清单反着来它从销售计划、合同、开票、帐款一路铺到业绩考核和市场信息九个一级模块、上百个三级功能点本质是一张“营销业务全链路”的施工图。适合谁适合手里有这份清单、想把它落成可运行系统的开发者也适合做二次开发时要对照功能边界的产品同学。下面我按“先看清结构、再跑起来、最后能改”的顺序拆一遍。2. 功能清单背后的模块划分九个一级模块怎么落到代码结构2.1 从 XSP01 到 XSP09 看业务闭环清单里的编号不是随便编的XSP01 到 XSP09 基本对应一条从计划到执行的链路。计划管理XSP01先定销售、回款、费用、生产、要货五类计划销售合同管理XSP02承接计划转成合同开票管理XSP03处理调拨、发票、税票跟踪帐款管理XSP04做回款和月末结转业绩考核XSP05管借款、报销、费用市场信息XSP06和促销品广告XSP07是市场侧统计报表XSP08做汇总公共关系管理XSP09是短信、日程、任务这些辅助能力。落到工程上我一般按“模块即包”来切一个一级模块一个顶层包三级功能点对应到 Controller 的一个方法或 Service 的一个用例。这样清单和代码能一一对上后期对着功能清单查漏补缺不会漏。crm-server/ ├── plan/ # XSP01 计划管理 │ ├── controller/ # 销售/回款/费用/生产/要货计划 │ ├── service/ │ └── mapper/ ├── contract/ # XSP02 销售合同 ├── billing/ # XSP03 开票管理调拨、发票、税票 ├── account/ # XSP04 帐款管理回款、结转 ├── performance/ # XSP05 业绩考核 ├── market/ # XSP06 市场信息 ├── promotion/ # XSP07 促销品广告 ├── report/ # XSP08 统计报表 └── common/ # XSP09 公共关系 公共能力这个结构的好处是功能清单里任何一个 XSP 编号都能直接定位到目录。坏处是模块间耦合点集中在“单据关联”上比如合同生成调拨申请、调拨单生成发票这些跨模块调用要单独抽一个flow包别塞进某个业务模块里否则后面改开票逻辑会牵一发动全身。2.2 单据状态机是这套系统的骨架清单里反复出现“审核”“红冲”“作废”“结转”“反结”这些词背后都是状态流转。以调拨单为例从申请到出库大致是草稿 → 待审批 → 已审批 → 已调拨 → 已开票 → 已出库中间还可能走到已作废或已红冲。如果状态用字符串散落在各处判断后面加一个“部分出库”就能让代码到处翻车。常见做法是给每类单据建一张状态流转配置表或者至少在枚举里把合法迁移写死public enum TransferStatus { DRAFT, // 草稿 PENDING, // 待审批 APPROVED, // 已审批 TRANSFERRED, // 已调拨 INVOICED, // 已开票 SHIPPED, // 已出库 VOIDED, // 已作废 REVERSED; // 已红冲 // 合法迁移表新增状态时只改这里 private static final MapTransferStatus, SetTransferStatus FLOW Map.of( DRAFT, Set.of(PENDING, VOIDED), PENDING, Set.of(APPROVED, DRAFT), APPROVED, Set.of(TRANSFERRED, VOIDED), TRANSFERRED, Set.of(INVOICED, REVERSED), INVOICED, Set.of(SHIPPED, REVERSED) ); public boolean canMoveTo(TransferStatus target) { return FLOW.getOrDefault(this, Set.of()).contains(target); } }FLOW就是这张状态机的黑匣子任何业务代码改状态前先调canMoveTo非法迁移直接抛业务异常。参数说明this是当前状态target是目标状态返回布尔值。这样“已开票的单据不能直接作废、只能红冲”这种规则就落在枚举里而不是散在 Service 的 if 里。2.3 可开票量这类计算字段要单独收口清单里 XSP03.07 写得很清楚可开票量 实际库存 − 开票未提。这种公式型字段最怕每个查询各写一遍最后口径不一致。我的做法是抽一个BillingCapacityService所有涉及可开票量的地方都走它public BigDecimal calcBillableQty(Long productId, Long warehouseId) { // 实际库存从库存表汇总 BigDecimal stock stockMapper.sumAvailable(productId, warehouseId); // 开票未提已开调拨单但未出库的数量 BigDecimal invoicedNotShipped transferMapper.sumInvoicedNotShipped(productId, warehouseId); return stock.subtract(invoicedNotShipped); }两个参数分别是成品和库房返回可开票数量。注意这里sumInvoicedNotShipped的统计口径要和 XSP03.08“开票未提查询”完全一致否则页面查出来的数和可开票量对不上业务方第一反应就是系统有 bug。我一般让这两个功能共用同一个 Mapper 方法从源头保证口径统一。3. 把系统跑起来环境、依赖和第一张调拨单3.1 技术选型与本地环境这份清单描述的是典型的企业级后台系统功能点以增删改查、审核、统计报表为主没有高并发实时计算诉求。按这个场景合格开发最可能用的组合是 Spring Boot MyBatis-Plus MySQL Redis前端 Vue 或 React 配 Element/Ant Design 这类组件库。清单里 XSP09 提到短信邮件服务器设置说明还需要一个消息发送的适配层本地跑的时候用日志模拟即可。本地环境我一般这样准备JDK 17、Maven 3.9、MySQL 8.0、Redis 7。数据库字符集用utf8mb4因为客户名称、经销单位信息里难免有生僻字。建库脚本按模块拆成多个schema-xxx.sql初始化数据单独放># 建库并导入结构 mysql -uroot -p -e CREATE DATABASE crm DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p crm sql/schema-all.sql mysql -uroot -p crm sql/data-init.sql # 启动后端本地 profile 指向本机 MySQL mvn spring-boot:run -Dspring-boot.run.profileslocal启动参数里-Dspring-boot.run.profileslocal是关键application-local.yml里配本机数据库地址和 Redis别把生产配置混进来。第一次启动如果报数据源连接失败先确认 MySQL 端口和账号再看schema-all.sql是否按依赖顺序建表——合同表引用客户表、调拨表引用合同表顺序错了外键会建不上。3.2 用一条调拨链路验证环境环境起来后别急着点页面先用接口把“合同 → 调拨申请 → 审批 → 调拨单 → 发票”这条链路走一遍能跑通说明核心模块和状态机都正常。下面是一段用 curl 串链路的示例# 1. 从合同生成调拨申请XSP03.01 curl -X POST http://localhost:8080/api/transfer/apply/from-contract \ -H Content-Type: application/json \ -d {contractId:1001,warehouseId:1,items:[{productId:2001,qty:50}]} # 2. 审批调拨申请XSP03.03 curl -X POST http://localhost:8080/api/transfer/apply/10001/approve \ -H Content-Type: application/json -d {approver:tester} # 3. 生成调拨单XSP03.04 curl -X POST http://localhost:8080/api/transfer/order/from-apply/10001 # 4. 由调拨单生成发票XSP03.05 curl -X POST http://localhost:8080/api/invoice/from-transfer/20001每一步返回体里都有单据号和状态重点看状态是否按 2.2 的状态机推进。如果第 3 步报“状态不允许”大概率是第 2 步审批没成功或者审批人参数为空。这条链路跑通后再去看 XSP03.07 可开票量数值应该等于库存减去刚生成的“开票未提”数量对得上就说明计算口径收口生效了。3.3 月末结转这类批量任务怎么本地验证XSP04.02 的月末结转分正式结转、模拟结转、反结三步这是整套系统里最容易出数据问题的地方。本地验证时不要直接跑正式结转先跑模拟结转看结果-- 模拟结转只计算不落正式标记 SELECT customer_id, SUM(receivable_amount) AS period_receivable, SUM(received_amount) AS period_received FROM ar_ledger WHERE period 2024-06 AND settled 0 GROUP BY customer_id;这条 SQL 就是模拟结转的核心逻辑settled 0过滤未结帐数据按客户汇总应收和已收。正式结转时把结果写入结帐快照表并置settled 1反结则是把快照撤销、settled改回 0。注意反结要有权限控制和时间窗口限制清单里说“方便修改工作”但没说不限时间实际项目里我一般限制只能反结最近一个期间否则历史帐目会被改乱。4. 核心模块拆解合同、开票、帐款三处关键实现4.1 销售合同导入的校验链路XSP02.01 要求把客户电子文本格式的合同导入系统还要提供校验。这类导入功能翻车点不在解析而在校验规则和重复导入。我的做法是解析、校验、落库三段分离校验不通过整批回滚并返回行级错误。public ImportResult importContracts(MultipartFile file) { ListContractRow rows excelParser.parse(file); // 解析 ListRowError errors new ArrayList(); SetString seenContractNo new HashSet(); for (int i 0; i rows.size(); i) { ContractRow row rows.get(i); // 必填校验 if (StringUtils.isBlank(row.getContractNo())) { errors.add(new RowError(i 2, 合同编号为空)); continue; } // 文件内重复校验 if (!seenContractNo.add(row.getContractNo())) { errors.add(new RowError(i 2, 合同编号文件内重复)); continue; } // 库内重复校验 if (contractMapper.existsByNo(row.getContractNo())) { errors.add(new RowError(i 2, 合同编号已存在)); } } if (!errors.isEmpty()) { return ImportResult.fail(errors); // 有错整批不入库 } contractMapper.batchInsert(rows); return ImportResult.ok(rows.size()); }file是上传的电子文本rows是解析后的行对象RowError里的行号按 Excel 实际行号算表头占一行所以i 2。这里有个血泪经验文件内重复和库内重复要分开报否则用户看到“编号重复”不知道是文件里重了还是系统里已经有了。另外批量插入要控制单批条数几千行一次性 insert 容易超包大小按 500 条一批切。4.2 调拨与税票对帐的关联查询XSP03.09 要求把调拨单和发票核对XSP03.16 要求查合同、申请、调拨、出库单据之间的关联关系。这两个功能本质是同一类问题单据之间靠业务单号串起来查询时要做多表关联。设计上我建议每张单据都冗余存上游单号而不是只存上游主键因为对帐时业务方给的是单号。-- 调拨与税票对帐找出调拨单与发票金额/数量不一致的记录 SELECT t.transfer_no, t.qty AS transfer_qty, i.invoice_qty, t.amount AS transfer_amount, i.invoice_amount FROM transfer_order t LEFT JOIN invoice i ON i.source_transfer_no t.transfer_no WHERE t.status INVOICED AND (i.invoice_qty IS NULL OR i.invoice_qty t.qty OR i.invoice_amount t.amount);source_transfer_no就是调拨单号冗余到发票表上的字段LEFT JOIN保证没生成发票的调拨单也能被查出来。结果集里invoice_qty IS NULL表示调拨了但没开票数量金额不等表示开票有差异。这个查询直接对应 XSP03.09 的对帐页面业务方每天跑一遍就能发现漏开票和错开票。4.3 业绩考核的多维统计XSP05.07 要求按年月、部门、地区统计费用、销量和销售额XSP08 的报表也大量是这种多维汇总。这类查询不要在每个报表里写一遍 group by我一般建一张宽表或者用统一的事实表加维度字段报表层只做条件拼接。-- 业绩考核按部门、地区、月份汇总 SELECT dept_id, region_id, DATE_FORMAT(biz_date, %Y-%m) AS ym, SUM(sales_amount) AS sales_amount, SUM(sales_qty) AS sales_qty, SUM(expense_amount) AS expense_amount FROM sales_fact WHERE biz_date BETWEEN #{start} AND #{end} GROUP BY dept_id, region_id, DATE_FORMAT(biz_date, %Y-%m);start、end是查询起止日期sales_fact是销售事实表把销量、销售额、费用都放进去避免报表时反复 join 多张业务表。代价是事实表需要定时同步我一般用定时任务每天凌晨从各业务表汇总一次报表查询只读事实表响应能压到秒级。注意DATE_FORMAT在大数据量下会走不了索引如果数据上千万改成按ym字段预存月份。5. 避坑与排查这套 CRM 落地时最容易踩的五个坑5.1 月末结转后改数据导致帐实不符现象正式结转后业务方发现某笔回款录错直接改回款表结果应收帐款和总帐对不上。原因结转后应收数据已经进快照回款表改动不会回写快照。解决结转后禁止直接改原始单据走反结流程反结、修改、重新结转三步走并在回款维护接口上加期间校验已结帐期间的回款只允许查看不允许编辑。5.2 可开票量出现负数现象某成品可开票量查出来是负的。原因开票未提的统计把已作废调拨单也算进去了或者库存扣减和调拨单生成不在同一事务里。解决sumInvoicedNotShipped里加status NOT IN (VOIDED,REVERSED)过滤同时把调拨单生成和库存预占放在同一个事务事务提交后再允许开票。5.3 合同导入部分成功部分失败现象导入 100 条合同成功 60 条失败 40 条用户不知道哪条错了。原因逐条插入没有整批事务错误行被跳过。解决按 4.1 的做法先全量校验有错整批不入库并返回行级错误清单用户改完再导。如果业务坚持要部分成功至少返回成功和失败两个明细文件。5.4 调拨单红冲后发票没跟着处理现象调拨单红冲了但对应发票还挂着对帐时出现负数差异。原因红冲逻辑只改了调拨单状态没联动发票。解决红冲做成一个编排服务先校验发票状态未开票的直接红冲调拨单已开票的先红冲发票再红冲调拨单整个编排放在一个事务里任何一步失败全部回滚。5.5 报表查询慢拖垮业务库现象销售月报一跑业务录入页面就卡。原因报表直接查业务表还带多表 join大时间段查询扫全表。解决按 4.3 建事实表报表只读事实表同时给报表查询加时间范围上限超过一年强制走异步导出别让一个查询把连接池占满。6. 二次开发怎么改从加一个字段到加一个模块拿到这套系统做二次开发最常见的需求是“清单里没有但业务要的功能”。我的经验是先判断改动落在哪一层再决定动哪里。加一个字段最简单数据库加列、实体加属性、页面加表单项三步走完但要注意如果这个字段参与统计事实表同步任务也要跟着改否则报表里永远是新字段为空。加一个三级功能点比如在 XSP03 下加“调拨单打印次数统计”就按模块内聚原则放在billing包下Controller 加一个接口Service 复用现有调拨查询别新起一个模块。加一个一级模块比如客户关系维护才需要新建包、新建表、在菜单表里注册权限。改造时我习惯先写一个验证脚本把改动前后的关键数据对比跑一遍# 改造前后对比可开票量口径是否一致 mysql -uroot -p crm -e SELECT product_id, warehouse_id, billable_qty FROM v_billable_qty ORDER BY product_id, warehouse_id; before.txt # 改完代码重新生成视图后再跑一次 diff before.txt after.txt这个习惯救过我一次改可开票量公式时以为只是加了个过滤条件结果 diff 出来几十条差异才发现把“已红冲未重新开票”的单据漏掉了。从那以后我每次动计算口径都强制走一遍改造前后数据对比哪怕只改一行 SQL。希望这份拆解能帮你把这份功能清单真正跑起来少走几个我踩过的坑。本文还有配套的精品资源点击获取