ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CRM客户管理系统数据库设计:合同、开票、回款核心表与流程实现

CRM客户管理系统数据库设计:合同、开票、回款核心表与流程实现

简介:这份文档面向企业信息化建设者、CRM系统选型人员及软件开发与实施团队,系统梳理了客户管理系统的完整功能框架,可作为需求调研、方案设计与功能对照的参考清单。资源为1个doc文件,压缩包约376KB,内容以分级条目形式组织,便于按模块快速检索。文档覆盖计划管理、销售合同管理、开票管理、帐款管理、业绩考核、市场信息、促销品广告、统计报表及公共关系管理等子系统,每个功能点均配有编号与简要说明,如销售计划维护、合同变更历史查询、调拨与税票对帐、月末结账与反结、费用借还查询等,能帮助读者理清业务模块之间的衔接关系。已有801人学习下载,适合需要搭建或评估CRM功能体系的技术与业务人员参考。

1. 从一张“功能清单”倒推系统骨架:CRM 客户管理到底该先建哪几块

很多团队拿到 CRM 需求时,第一反应是先把客户表和联系人表建起来,结果做到一半发现合同、开票、回款三条线全缠在一起,改一个字段牵动五个模块。这份功能清单反着来——它把计划、合同、开票、账款、考核、市场、广告、报表、公共关系九大块一次性摊开,等于提前把企业销售链路的全貌摆在了桌面上。对正在做 CRM 客户管理毕设或课程设计的人来说,这份清单的价值不在于功能多,而在于它给出了一个可以直接裁剪的模块边界:哪些是主干(合同—开票—回款),哪些是旁支(市场信息、促销品广告),哪些是支撑(系统设置、群组、日程)。先看清骨架,再决定自己这版系统砍到哪一层,比闷头建表靠谱得多。

2. 需求拆解与模块划分:九大子系统怎么收敛成可交付的六层结构

2.1 从原始清单到分层模型

原始清单是九个子系统、上百个三级功能点,直接照搬进毕设会失控。我一般会按“数据流向”重新收敛:计划层(销售/回款/费用/生产/要货计划)产生目标数据,合同层承接目标并锁定履约义务,开票层把合同转成可执行的调拨与发票动作,账款层负责资金回笼与月末结转,考核层把费用和业绩挂到人,市场与广告层是外围信息采集,报表层做横向汇总,公共关系层是辅助工具。收敛后得到六层结构:

层级对应原始子系统核心职责毕设建议保留
计划层XSP01年/月/季目标录入与审核保留销售计划、回款计划
合同层XSP02合同导入、维护、变更、履约全保留
开票层XSP03调拨申请→审批→调拨→发票保留直接开票主线
账款层XSP04回款录入、月末结转/反结全保留
考核层XSP05费用、借款、报销、业绩保留费用与业绩查询
支撑层XSP06–XSP09市场信息、广告、报表、公关报表保留,其余可裁剪

这张表的作用是让你在开题报告里能说清楚“为什么只做这些”——不是做不完,是按数据闭环裁剪。计划层和合同层是输入,开票层和账款层是输出,考核层是反馈,四层闭合成一个可演示的销售管理循环,答辩时逻辑自洽。

2.2 模块边界与依赖关系

收敛之后要明确依赖方向,否则开发到中期会出现循环引用。我的做法是画一张单向依赖图(文字描述即可):计划层不依赖任何业务层,合同层依赖计划层(合同金额要对照计划),开票层依赖合同层(调拨申请可从合同生成),账款层依赖开票层(回款对应发票),考核层依赖账款层和开票层(业绩=回款+开票),报表层依赖以上所有层但只读不写。支撑层中的系统设置被所有层引用,但自身不反向依赖。

这个依赖方向决定了建表顺序和接口顺序。先建计划表和合同表,再建调拨单表和发票表,最后建回款表和费用表。如果反过来先做回款,你会发现回款要挂发票、发票要挂调拨、调拨要挂合同,一路补表补到崩溃。血泪经验是:依赖图没画清楚就动手,后面每加一个查询都要回头改三张表。

2.3 裁剪原则:哪些功能点可以合并

原始清单里有些功能点其实是同一个动作的不同视角。比如 XSP03.06 销售开票(直接开票)和 XSP03.04 调拨管理 + XSP03.05 发票管理(间接开票),本质是两种开票模式,毕设里保留直接开票一条线即可,间接开票作为扩展说明写在设计文档里。再比如 XSP03.15 税票跟踪查询出现了两次(清单里编号重复),合并为一个查询接口。XSP05.01 分类费用维护在原始清单里编号跳到了 XSP05 下面但描述独立,归入考核层的费用模块。

合并的原则是:同一张主表的不同查询视图合并为一个接口加不同参数;同一业务动作的两种模式保留主模式,另一种写进设计说明。这样功能点从上百个压到四十个左右,一个学期能做完且能演示完整闭环。

3. 数据库与核心表设计:合同、调拨、发票、回款四张主表怎么串

3.1 主表结构与关键字段

四张主表是整个系统的脊柱,字段设计要预留状态机和关联键。下面给出核心建表语句(MySQL 语法),字段按“业务必需+可扩展”原则取舍:

-- 销售合同主表:合同是后续所有单据的源头 CREATE TABLE crm_contract ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_no VARCHAR(32) NOT NULL UNIQUE COMMENT '合同编号,业务主键', customer_id BIGINT NOT NULL COMMENT '客户ID,关联客户表', sign_date DATE NOT NULL COMMENT '签订日期', total_amount DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '合同总额', status TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1已审核 2执行中 3已完成 4已变更', version INT NOT NULL DEFAULT 1 COMMENT '变更版本号,每次变更+1', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 调拨单主表:开票动作的载体,直接开票模式下与发票一一对应 CREATE TABLE crm_transfer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, transfer_no VARCHAR(32) NOT NULL UNIQUE COMMENT '调拨单号', contract_id BIGINT COMMENT '来源合同,可为空(无合同调拨)', customer_id BIGINT NOT NULL, transfer_date DATE NOT NULL, total_qty INT NOT NULL DEFAULT 0 COMMENT '调拨总数量', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1已审 2已开票 3已作废 4已红冲', invoice_id BIGINT COMMENT '关联发票ID,开票后回填', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 发票主表:由调拨单生成,记录税票信息 CREATE TABLE crm_invoice ( id BIGINT PRIMARY KEY AUTO_INCREMENT, invoice_no VARCHAR(32) NOT NULL UNIQUE COMMENT '发票号', transfer_id BIGINT NOT NULL COMMENT '来源调拨单', customer_id BIGINT NOT NULL, amount DECIMAL(14,2) NOT NULL COMMENT '开票金额', tax_amount DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '税额', invoice_date DATE NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0已开 1已登记 2已核销', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 回款主表:资金回笼记录,挂发票或挂合同 CREATE TABLE crm_receipt ( id BIGINT PRIMARY KEY AUTO_INCREMENT, receipt_no VARCHAR(32) NOT NULL UNIQUE, customer_id BIGINT NOT NULL, invoice_id BIGINT COMMENT '对应发票,可空(预收款)', contract_id BIGINT COMMENT '对应合同,可空', amount DECIMAL(14,2) NOT NULL COMMENT '回款金额', receipt_date DATE NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1已审 2已核销', created_at DATETIME DEFAULT CURRENT_TIMESTAMP );

字段设计里有几个关键决策。status用 TINYINT 而不是字符串,查询快且状态机清晰;version字段给合同变更用,每次变更生成新版本记录而不是覆盖原记录,这样 XSP02.04 合同变更历史查询才有数据可查;invoice_id在调拨表里可空,因为调拨单先于发票存在,开票后回填,这个“先有单后有票”的顺序不能反。receipt表同时挂invoice_id和contract_id且都可空,是为了支持预收款(只挂合同)和按票回款(挂发票)两种场景。

3.2 关联关系与状态流转

四张表的关系是链式的:合同 1:N 调拨单,调拨单 1:1 发票(直接开票模式下),发票 1:N 回款。状态流转要卡死,不能跳步。调拨单从“待审”到“已审”需要审批动作(对应 XSP03.03 调拨申请审批),从“已审”到“已开票”需要生成发票,作废只能从“待审”或“已审”发起,红冲只能从“已开票”发起且要生成反向单据。

状态流转的实现建议用一张状态流水表记录每次变更,而不是只改主表状态字段。流水表字段:单据类型、单据ID、原状态、新状态、操作人、操作时间、备注。这张表在答辩时是加分项——老师问“你怎么保证数据可追溯”,你把流水表一展示,比口头解释强十倍。而且 XSP03.16 单据跟踪(合同、申请、调拨、出库之间的关联关系)本质上就是查这张流水表加关联主表。

3.3 可开票量的计算逻辑

XSP03.07 可开票量查询的公式是“实际库存-开票未提”,这个公式看着简单,落地时容易翻车。实际库存要从库存表取,开票未提要从调拨单里筛“已开票但未出库”的数量。我的做法是建一个视图而不是在代码里拼 SQL:

CREATE VIEW v_available_invoice_qty AS SELECT p.product_id, p.product_name, IFNULL(s.stock_qty, 0) AS stock_qty, IFNULL(t.pending_qty, 0) AS pending_qty, IFNULL(s.stock_qty, 0) - IFNULL(t.pending_qty, 0) AS available_qty FROM crm_product p LEFT JOIN crm_stock s ON s.product_id = p.product_id LEFT JOIN ( SELECT d.product_id, SUM(d.qty) AS pending_qty FROM crm_transfer_detail d JOIN crm_transfer t ON t.id = d.transfer_id WHERE t.status = 2 -- 已开票 AND NOT EXISTS (SELECT 1 FROM crm_outbound o WHERE o.transfer_id = t.id) GROUP BY d.product_id ) t ON t.product_id = p.product_id;

视图的好处是口径统一,报表和查询都走同一个视图,不会出现“开票页面算出来 100,报表页面算出来 95”的玄学问题。NOT EXISTS子查询判断是否已出库,比 LEFT JOIN 加 IS NULL 更直观,执行计划也更稳。注意status = 2这个硬编码要跟状态字典对齐,建议在代码里用常量而不是魔法数字。

4. 开票与账款主流程实现:从调拨申请到月末结转的代码落地

4.1 调拨申请到发票生成的服务层逻辑

开票主流程是:调拨申请录入 → 审批 → 生成调拨单 → 生成发票。直接开票模式下,调拨单和发票可以一步生成。下面给出服务层核心方法(Java 风格伪代码,重点看逻辑顺序和事务边界):

@Transactional(rollbackFor = Exception.class) public InvoiceVO directInvoice(Long transferId, Long operatorId) { // 1. 锁调拨单,防止并发重复开票 Transfer transfer = transferMapper.selectForUpdate(transferId); if (transfer == null) { throw new BizException("调拨单不存在"); } // 2. 状态校验:只有已审状态才能开票 if (transfer.getStatus() != TransferStatus.APPROVED) { throw new BizException("调拨单状态不允许开票:" + transfer.getStatus()); } // 3. 校验可开票量,防止超开 for (TransferDetail d : transfer.getDetails()) { int available = stockService.getAvailableQty(d.getProductId()); if (d.getQty() > available) { throw new BizException("商品[" + d.getProductName() + "]可开票量不足"); } } // 4. 生成发票主表 Invoice invoice = new Invoice(); invoice.setInvoiceNo(invoiceNoGenerator.next()); invoice.setTransferId(transferId); invoice.setCustomerId(transfer.getCustomerId()); invoice.setAmount(transfer.getTotalAmount()); invoice.setTaxAmount(transfer.getTotalAmount().multiply(TAX_RATE)); invoice.setInvoiceDate(LocalDate.now()); invoice.setStatus(InvoiceStatus.ISSUED); invoiceMapper.insert(invoice); // 5. 回填调拨单的发票ID并改状态 transfer.setInvoiceId(invoice.getId()); transfer.setStatus(TransferStatus.INVOICED); transferMapper.updateById(transfer); // 6. 写状态流水 statusLogService.log("TRANSFER", transferId, TransferStatus.APPROVED, TransferStatus.INVOICED, operatorId); return toVO(invoice); }

这段代码有几个设计点值得说。第一,selectForUpdate行锁是必须的,两个人同时点“开票”按钮,不加锁会生成两张发票,这是开票模块最常见的翻车点。第二,状态校验放在锁之后、业务计算之前,快速失败。第三,可开票量校验逐条明细做,不能只校验总额,因为不同商品库存独立。第四,发票号和调拨单号用独立的号段生成器,不要用数据库自增 ID 拼字符串,否则并发下会重号。第五,状态流水在事务内写,保证主表和流水一致。

4.2 月末结转与反结的幂等设计

XSP04.02 月末结账分正式结转、模拟结转、月末反结三个动作。正式结转后应收帐款不可修改,反结可以把已结数据置回未结。这里最大的坑是重复结转——用户连点两次“正式结转”,如果没做幂等,应收帐款会被结转两遍。

我的做法是给结转批次建一张表,字段包括:结账期间(年月)、结转类型(正式/模拟)、状态、操作人、操作时间。正式结转前先查该期间是否已有“正式”且“成功”的批次,有则拒绝。反结时把该批次状态改为“已反结”,并记录反结时间,原结转数据打上批次号但不删除,查询时按批次过滤。这样既保证了幂等,又保留了审计痕迹。

-- 结转批次表 CREATE TABLE crm_settle_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, period VARCHAR(7) NOT NULL COMMENT '结账期间,格式2024-06', settle_type TINYINT NOT NULL COMMENT '1正式 2模拟', status TINYINT NOT NULL DEFAULT 0 COMMENT '0处理中 1成功 2已反结 3失败', operator_id BIGINT NOT NULL, settled_at DATETIME, reversed_at DATETIME, UNIQUE KEY uk_period_type (period, settle_type, status) );

唯一索引uk_period_type里带上status是个技巧:同一期间同一类型只允许一条“成功”记录,但允许存在“已反结”的历史记录。这样反结后可以再次正式结转,唯一索引不会挡住。如果只带period + settle_type,反结后就再也结不了账,这是踩过的坑。

4.3 回款核销与应收余额联动

回款录入后要核销对应发票或合同,核销动作影响应收余额。XSP04.01 回款信息维护带审核功能,审核通过才参与余额计算。实现上建议回款表加一个verified_amount字段记录已核销金额,而不是直接改发票的应收余额——发票余额是计算出来的,不是存出来的。

应收余额的计算口径:合同应收 = 合同总额;已开票金额 = 该合同下所有有效发票金额之和;已回款金额 = 该合同下所有已审核回款金额之和;应收余额 = 已开票金额 − 已回款金额。这个口径要在设计文档里写死,所有查询统一走一个ReceivableService.calcBalance(contractId)方法,不允许各页面自己拼 SQL。否则报表页和回款页的数字对不上,答辩时被问一句就露馅。

5. 避坑与常见问题:开票、结转、报表三条线上最容易翻车的五件事

5.1 并发开票导致重复生成发票

现象:两个人同时操作同一张调拨单开票,系统生成两张发票,金额翻倍。原因:查询调拨单时没加行锁,两个事务都读到“已审”状态,都通过了校验。解决:selectForUpdate行锁 + 状态校验 + 发票表对transfer_id加唯一索引兜底。唯一索引是最后一道防线,即使代码漏了锁,数据库也会拒绝第二条记录。

5.2 月末反结后数据对不上

现象:反结某月账务后,应收余额和反结前不一致。原因:反结时只改了批次状态,没有把该批次结转时生成的应收明细标记为无效,查询时新旧数据混在一起。解决:应收明细表加settle_batch_id字段,查询时只取当前有效批次的明细;反结时把批次状态改为“已反结”,查询条件自动排除。不要物理删除结转数据,审计要求留痕。

5.3 合同变更后历史履约数据被覆盖

现象:合同金额从 100 万变更为 80 万后,之前按 100 万生成的调拨单和发票对不上账。原因:合同变更直接 UPDATE 主表,没有保留版本。解决:合同表加version字段,变更时 INSERT 新版本记录,旧版本标记为历史;调拨单和发票记录生成时引用的合同版本号,履约查询按版本号关联。XSP02.04 合同变更历史查询就是查这张版本表。

5.4 可开票量查询结果与库存页不一致

现象:开票页显示可开票 50,库存页显示 60。原因:两个页面各自拼 SQL,一个减了“开票未提”,一个没减。解决:统一走v_available_invoice_qty视图,所有涉及可开票量的查询都从这个视图取数。视图定义变更时只改一处,不会漏。

5.5 报表统计口径与明细页对不上

现象:销售月报的金额和合同明细页的合计差几分钱。原因:报表用了ROUND而明细页没舍入,或者报表按开票日期统计而明细按合同日期统计。解决:报表层统一调用服务层的统计方法,不在 SQL 里做二次聚合;统计口径(按什么日期、含不含作废单据)写进设计文档并在报表页脚注明。几分钱的差异在答辩时是致命伤,老师会追着问。

6. 验收与答辩:用一条数据链路跑通全流程,比背功能清单管用

验收这套 CRM 系统,我一般不看功能列表勾了多少项,而是让演示者用一条数据从头走到尾:新建一个客户 → 录入一份年度销售计划 → 签一份合同 → 从合同生成调拨申请 → 审批 → 生成调拨单 → 开票 → 录入回款 → 核销 → 跑一次模拟结转 → 查销售月报。这条链路走通,说明计划、合同、开票、账款、报表五层的数据是打通的;走不通,功能列表再长也是散的。

答辩时老师最爱问的三个问题,提前准备好答案。第一个是“你的可开票量怎么算的”,答:实际库存减开票未提,走统一视图,并现场展示视图定义。第二个是“月末结转后数据还能改吗”,答:正式结转后应收明细锁定,反结走批次状态切换,原数据留痕不删除,现场演示反结后重新结转。第三个是“合同变更了,之前的发票怎么办”,答:合同按版本管理,发票记录生成时引用的合同版本号,变更不影响已生成单据,现场查变更历史。

验收检查表可以按下面这个结构准备,答辩前自己先跑一遍:

检查项通过标准验证方式
主流程闭环客户→计划→合同→调拨→发票→回款→报表全通现场走一遍
状态机完整调拨单五种状态流转无跳步逐状态操作
并发安全同一调拨单重复开票只成功一次两个浏览器同时点
结转幂等同期间重复正式结转被拒绝连点两次
反结可恢复反结后重新结转数据一致反结再结对比
报表口径一致月报合计等于明细合计交叉核对

最后说个习惯。我每次带项目,验收前一定自己当“刁钻用户”把每个按钮连点三遍,尤其是开票、结转、反结这三个动作——这三个地方出问题,演示时当场翻车,没有后悔药。从那以后我每次交方案前都强制走一遍这条数据链路,哪怕功能只做了一半,链路必须是通的。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表