☰
自研轻量级CRM:PostgreSQL数据建模与权限控制的实战复盘
2026/10/19 14:12:56 网站建设 项目流程

很多人问我,为什么放着现成的 SaaS CRM 不买,非要自己动手做一个 DeskcommCRM。这个问题每次聊都要解释半天,今天干脆把整个项目的来龙去脉、设计思路、技术选型和上线后踩过的坑都写出来。DeskcommCRM 是我给团队做的一个轻量级客户关系管理系统,核心解决三件事:客户资料散落、跟进节奏失控、销售管道不透明。如果你也在小团队里管客户,或者正纠结要不要自研 CRM,这篇文章应该能给你一些参考。

先交代一下背景。我们团队不到二十人,销售加客户成功大概十个,之前客户资料分散在销售个人微信、邮件客户端、Excel 表格和几张共享网盘里。一开始人少还没觉得有什么问题,直到出现了一次比较大的客户投诉:一个合作了大半年的客户,因为负责对接的同事离职,交接时漏了关键信息,后续服务断档,客户差点流失。那次之后我才下定决心,不管买还是做,客户关系管理必须有系统承接。

1. 为什么最终选择自研而不是直接买现成 CRM

1.1 小团队客户管理的三个失控时刻

我复盘了团队之前的工作方式,发现失控基本集中在三个场景。

第一个是“客户资料跟着人走”。销售把聊天记录、报价单、客户偏好都存在自己电脑里,或者干脆留在个人微信聊天记录里。一旦人离职,这部分资产基本就没了。哪怕有交接文档,写的也都是结论,中间沟通过程、客户说过的一些关键诉求,很难完整还原。

第二个是“跟进节奏靠记忆”。每天事情一多,销售很容易忘记昨天答应客户今天要发方案,或者三天后要回访。不是态度问题,是手动记录跟进计划的成本太高。客户今天不买,不代表下个月不买,但如果没有系统提醒,这个线索大概率会在三个月后彻底凉掉。

第三个是“管理层只能靠问”。每周开周会问销售:你手上几个潜在客户?金额多少?预计什么时候签?得到的答案往往很主观。销售说“这个月能签”,但他手里的商机到底是刚接触还是已经进入报价阶段,管理层根本看不到。业绩预测自然也不准。

这三个失控点,本质上都是因为客户数据没有一个唯一的、结构化的存储载体。

1.2 买 SaaS、用开源还是自研,我的对比结论

在决定自研之前,我认真对比过三条路:买现成 SaaS、用开源系统二次开发、完全自研。

先说买 SaaS。市面上成熟的 CRM 产品功能确实全面,从线索管理、商机管理到客户服务、营销自动化都有。但问题是,第一是价格,按席位收费,十个人一年下来几万块,功能用得深的还要开更高档位;第二是灵活性,我们需要很多自定义字段、自定义管道阶段,还要和自研的审批流对接,SaaS 的开放接口往往做不到那么细;第三是数据归属,客户资料放在别人服务器上,心里总是不踏实,万一哪一天服务商涨价或者产品线调整,迁移成本很高。

再看开源系统。像 SuiteCRM、EspoCRM 这些开源项目确实很成熟,直接部署就能用。但它们的问题是架构偏重,代码量大,二次开发的曲线比较陡。我们团队人少,想快速改几个字段、加一个报表,都要去理解一套别人的业务抽象,时间成本并不低。而且一些老牌开源 CRM 的前端交互方式比较传统,销售同事试用反馈不太好。

最终选了自研,不是因为自研更高端,而是因为我们有明确的、轻量化的需求边界:客户管理、跟进记录、任务提醒、销售管道看板,外加一个足够灵活的权限模型。这个规模对一个四五人的研发小组来说,工作量是可控的。自研还有一个隐性的好处,就是后续任何新需求都可以在我们自己的代码基础上快速迭代,不需要去迁就外部系统的限制。

不过我也要提醒一句,如果你的团队没有专职开发,或者需求就是“能记录、能导出”,那直接买现成产品反而更划算。自研的前提是你确定要做长期投入,而不是憋几个月上线就完事。

2. DeskcommCRM 的核心模块与数据流:先理清业务再写代码

2.1 模块到底怎么拆

自研系统最忌讳一上来就画大而全的功能清单。我做的第一件事不是建数据库,而是把团队真正需要的业务对象列出来。

DeskcommCRM 最终拆成了六个核心模块:

模块核心职责关键产出
客户管理维护客户主数据、行业、来源、状态唯一的客户档案
联系人管理记录客户企业里的决策人、关键角色沟通过程找人更准
沟通记录沉淀每一次与客户交互的上下文可追溯的往来历史
跟进任务为销售生成待办,防止跟进断档明确的下一步动作
商机管理管理潜在项目、金额、阶段、赢单率销售管道和业绩预测
报表看板统计管道金额、任务完成率、团队业绩管理层决策依据

很多人做 CRM 会把“客户”和“联系人”混在一起,觉得反正都是记名字和电话。实际上这两个对象必须分开。客户是企业,联系人是企业里的人。一家企业可能有三四个决策人,分别管技术、预算和采购,如果只用一个客户表去记录,要么字段爆掉,要么只能记录一个人,后面做权限隔离和数据分析都会很别扭。

2.2 一条客户从线索到成交在系统里怎么走

DeskcommCRM 的数据流不是靠文档规定出来的,而是靠每个模块的关联字段和数据状态机承载起来的。

整个流程大致是这样的:销售通过某次展会、朋友介绍或者官网留资获得一个线索,在系统里创建客户信息,状态设为“潜在客户”。接着在客户下添加联系人,把决策链理清楚。第一次电话沟通后,记录一条沟通记录,同时创建一条跟进任务:明天上午十点发报价单。客户有了明确预算和采购意向,销售在商机模块里建立一个项目,阶段设为“方案报价”,金额填上,预期成交时间也填上。一段时间后客户同意签约,商机阶段改成“赢单”,客户状态改成“正式客户”。合同到期前,系统再生成续约任务,提醒客户成功团队介入。

这个流程看起来简单,但落到系统设计上,有几个关键点非常重要。

第一,所有业务对象都必须挂到“客户”这个主对象上,联系人、沟通记录、任务、商机都有 customer_id 外键。这样无论从哪个模块进入,都能拼出一家客户的完整时间线。第二,商机的阶段必须用显式的枚举值,不能靠销售自定义一个文本框随便填,否则看板统计根本没法做。第三,客户状态和商机状态是两个独立的状态机,不能混在一起。客户状态描述的是“这家企业和我们的关系阶段”,商机状态描述的是“这个具体项目的销售进度”,两边各自流转、互相不影响。

数据流理清楚之后,后面的表结构设计和接口开发就有抓手了。我见过太多项目死在“先建表再补需求”上,后期发现关联关系不对,重来一遍成本极高。

3. 技术选型:PostgreSQL 才是这个项目的幕后主力

3.1 后端为什么选了 FastAPI 而不是 Django 或 Spring Boot

后端框架我选了 FastAPI,这个选择在项目初期被人质疑过,觉得它不够“企业级”。但实际用下来,对小团队来说它非常合适。

FastAPI 最大的优势是开发效率高。基于 Python 类型提示能自动生成 OpenAPI 文档,前端对接接口的时候直接看 Swagger 页面就能调试,少了很多口口相传的沟通成本。其次是它的异步能力,虽然我们这个项目数据库操作居多,但偶尔要对接企业微信、邮件网关,异步调用比同步阻塞写起来舒服很多。

不用 Django 的原因很简单:Django 自带 Admin、ORM、迁移、认证,功能确实全,但它的 Model 设计偏向大型单体应用,我们这种接口优先的前后端分离项目,经常需要绕过 Django 的约定去做一些定制,反而觉得束缚。Spring Boot 就更不用说了,Java 生态的人才成本高、启动重,对一个小团队来说性价比偏低。

3.2 前端用 React + Ant Design 的理由

前端选了 React 加 Ant Design。这个组合在中后台项目里很稳。客户管理、商机看板这种页面本质上就是大量表单、表格、抽屉、弹窗的组合,Ant Design 的组件覆盖度很高,基本不需要自己造轮子。React 的生态也成熟,后面如果要加图表,直接上 Ant Design Charts 或者 ECharts 都顺滑。

唯一要控制的是状态管理。我们项目早期用了 Redux,后来发现很多页面状态其实都是服务端数据,直接用 React Query 或 swr 管理请求缓存就够了,Redux 反而让代码变得啰嗦。后来把全局状态砍到只剩用户信息和权限点,开发体验明显提升。

3.3 PostgreSQL 为什么比 MySQL 和 MongoDB 更合适

数据库这块我几乎是毫不犹豫地选了 PostgreSQL。这里不是说 MySQL 不行,而是 Postgres 有几个能力恰好是这个项目需要的。

首先,PostgreSQL 的 JSONB 字段能让我在保留关系模型的同时,给客户、商机等对象提供可扩展字段能力。销售团队经常提出“我要加一个发票抬头字段”“我要记录客户预算区间”,如果每次加字段都做一次变更表结构,迭代成本太高。用 JSONB 存扩展字段,前端想加什么就加什么,查询时又有 GIN 索引兜底。

其次,PostgreSQL 的行级安全策略(RLS)对多用户数据隔离非常重要,这点后面权限部分细说。简单讲,MySQL 到目前对 RLS 的支持还是不如 Postgres 顺手,MongoDB 在文档级权限控制上也差点意思。

第三,PostgreSQL 自带的 pg_trgm 模糊搜索、物化视图、CTE 递归查询,在处理客户搜索、统计报表、组织层级等场景时非常省事。尤其是销售看板要统计多个维度,一条 SQL 能算清楚的事情,就不需要再起一个单独的报表服务。

对比项PostgreSQLMySQLMongoDB
JSON 扩展字段JSONB 支持索引和查询JSON 支持较弱原生文档模型,但关系查询弱
行级安全策略RLS 成熟稳定支持有限不擅长
复杂统计 SQLCTE、物化视图强大也能用但能力一般聚合管道学习成本高
事务与一致性强一致,支持外键强一致默认不要滥用事务

一句话总结,我需要的是一个既能当关系数据库用,又能灵活扩展、还能做细粒度权限控制的数据库,PostgreSQL 是当下最均衡的选择。

4. 数据建模:客户主数据与关联对象的设计细节

4.1 六张核心表的设计和关键字段

数据库设计是一个 CRM 项目的灵魂。我最终设计了一套以客户为主数据、其他业务对象通过外键关联的表结构。这里把最核心的字段列出来,方便你对照自己的场景思考。

customers 表:

字段类型说明
iduuid主键
owner_iduuid所属销售/责任人
namevarchar客户名称
industryvarchar所属行业
sourcevarchar线索来源
statusvarchar潜在/正式/流失 等
extrajsonb自定义扩展字段
created_at / updated_attimestamptz创建和更新时间

contacts 表:

字段类型说明
iduuid主键
customer_iduuid所属客户,外键
namevarchar联系人姓名
titlevarchar职位
phone / emailvarchar联系方式
is_primaryboolean是否为决策人/主要联系人

interactions 表记录每一次沟通:

字段类型说明
iduuid主键
customer_id / contact_iduuid关联客户和联系人
owner_iduuid记录人
channelvarchar电话、邮件、微信、会议
directionvarchar呼出/呼入
subjectvarchar沟通主题
bodytext沟通内容摘要
happened_attimestamptz沟通发生时间

tasks 表管理跟进任务:

字段类型说明
iduuid主键
owner_iduuid任务负责人
customer_iduuid关联客户
interaction_iduuid来源记录,可空
titlevarchar任务标题
due_attimestamptz截止时间
statusvarchar待处理/已完成/已取消
priorityvarchar高/中/低
source_ref_type / source_ref_idvarchar防止重复生成任务的来源标识

deals 表管理商机:

字段类型说明
iduuid主键
customer_id / owner_iduuid关联客户和负责人
titlevarchar商机名称
amountnumeric预计金额
stagevarchar意向/方案/报价/谈判/赢单/输单
probabilityinteger预测赢单率
expected_close_atdate预计成交日期
closed_attimestamptz实际成交/输单时间

这套表结构不复杂,但覆盖了日常客户管理需要的所有核心数据。项目中最容易犯的错误是给每张表塞入过多业务字段。我的建议是,把确定性高、需要查询过滤和统计的字段设计成独立列,把不确定的字段全部丢进 JSONB 里。

4.2 业务状态为什么要用枚举而不是自由文本

在设计商机阶段时,我特别强调要用显式枚举。sales 可以直接在下拉框里选择当前商机处在哪个阶段,但不能自己输入一个“感觉快签了”这种状态。

原因很简单,销售管道看板本质上是按 stage 分组做聚合统计。如果阶段值不统一,看板上的卡片就会乱掉。今天有人填“方案中”,明天有人填“做方案”,后天有人填“方案阶段”,数据统计基本就废了。

我用数据库 CHECK 约束加上应用层枚举双重保证。比如商机阶段只有设定好的合法值,数据库层面直接拒绝非法数据。后续如果业务新增阶段,需要同时修改代码和约束,这看起来麻烦,但能逼着业务方想清楚每一阶段真正的定义。

另外要区分两个状态机:客户状态和商机状态。客户状态记录了客户生命周期,商机状态记录单个项目的推进情况,隔离处理,避免状态之间互相污染。

4.3 JSONB 扩展字段的正确打开方式

很多团队为了追求灵活性,一上来就把整个客户表设计成了 EAV 模型或者纯文档结构,后面做统计的时候叫苦不迭。我的做法是混合模型:核心字段用强类型列,扩展字段用 JSONB。

比如客户可能关心“是否有固定账期”“客户满意度评分”“启用日期”,这些字段不同行业差异大,直接建列会导致大量空字段。放进 JSONB 后可以用类似下面的方式建表和查询:

CREATE TABLE customers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID NOT NULL, name VARCHAR(255) NOT NULL, industry VARCHAR(100), source VARCHAR(50), status VARCHAR(30) NOT NULL DEFAULT 'potential', extra JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_customers_extra_gin ON customers USING GIN (extra);

如果未来要按扩展字段过滤,比如“查询所有账期为 30 天的客户”,可以直接用 JSONB 表达式索引。需要提醒的是,JSONB 不是万能药,不要把所有核心业务字段都塞进去,否则后面写统计报表会非常痛苦。我最常说的原则是:“要用来做筛选和聚合的字段,必须提出来建独立列;只是记录一下、不太可能参与查询的字段,才放进 JSONB。”

5. 核心功能落地:沟通记录、跟进任务和销售管道

5.1 沟通记录:把零散聊天变成结构化数据

沟通记录这个模块是 DeskcommCRM 最常被使用的功能,也是销售同事从 Excel 迁移到系统后最先感到价值的模块。

日常操作是这样的:销售给客户打完电话,在系统里打开对应客户详情页,点“新增沟通记录”,选择渠道是电话、邮件、微信还是线下会议,填写沟通主题和摘要,必要时上传附件。接口设计上很简单,FastAPI 里对应这样一个端点:

@router.post("/interactions") async def create_interaction( payload: InteractionCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): interaction = Interaction( customer_id=payload.customer_id, contact_id=payload.contact_id, owner_id=current_user.id, channel=payload.channel, direction=payload.direction, subject=payload.subject, body=payload.body, happened_at=payload.happened_at or datetime.now(timezone.utc), ) db.add(interaction) await db.commit() await db.refresh(interaction) return interaction

这里有一个业务上的小细节:我让 happened_at 默认为当前时间,但允许销售回填历史时间。因为很多销售在补录前几天记录的时候,如果默认时间是现在,时间线就乱了,后面的客户跟进分析也会失真。手动指定历史时间这个能力很重要,一开始觉得无所谓,后来发现补录是常态。

另一个经验是沟通记录的正文建议用纯文本摘要,而不是整段对话原文。早年我也想过把邮件正文和微信聊天记录全部同步进来,后来发现信息过载比信息缺失更可怕。销售看到一大段原始聊天记录反而不愿意打开详情页,关键信息就被淹没了。更好的做法是让销售自己写结论和要点,原始附件留在系统里存档即可。

5.2 跟进任务:防遗忘机制怎么落地

防遗忘机制是 CRM 能不能被销售持续使用的关键。DeskcommCRM 的任务模块我设计了三层触发逻辑。

第一层是手动创建任务。销售在客户详情页可以直接建任务,比如“本周五之前给张总发送新版报价单”。第二层是在创建沟通记录时关联下一步动作。电话打完,顺便勾选“创建一个跟进任务”,这是最高频的入口。第三层是系统自动生成的任务,比如商机超过七天没有新互动,自动创建一个提醒任务。

任务生成时要特别注意去重。最初我在后台定时任务里写了“扫描超过七天无互动的商机,生成提醒任务”,上线后发现一个问题:一旦服务器上有两个 worker 同时跑定时任务,同一批商机就会被生成两遍任务。销售每天打开待办看到一堆重复项,体验极差。

后来我加了唯一约束来兜底:

CREATE UNIQUE INDEX idx_tasks_unique_pending_source ON tasks (owner_id, source_ref_type, source_ref_id) WHERE status = 'pending';

这个部分唯一索引特别有用。同一个业务来源只能存在一个待处理任务,如果定时任务重复跑,第二次插入直接报唯一约束冲突,代码里捕获异常忽略掉就行,不会产生脏数据。

任务的提醒能力也不依赖站内信,直接接企业微信机器人到个人,每天早晨把待办清单推给销售。推送文案我会附上客户名称和截止时间,方便销售在微信里直接判断优先级。这一步做下来,团队的任务完成率提高了不少。

5.3 销售管道看板背后的 SQL 聚合

销售管道看板是给管理层看的核心报表。需求很朴素:商机按阶段分组,显示每个阶段有多少单子、金额总共多少。

第一版我把后端接口写成遍历所有商机,然后在 Python 里分组累加。客户量小的时候没问题,但数据过了五千条后接口变慢,而且每次看板刷新都要把所有明文金额传到前端,既不安全也不必要。后来改成在数据库里做聚合,只返回结果,接口响应时间从 1.2 秒降到 80 毫秒。

聚合 SQL 大致长这样:

SELECT stage, COUNT(*) AS deal_count, SUM(amount) AS total_amount FROM deals WHERE deleted_at IS NULL GROUP BY stage ORDER BY CASE stage WHEN 'lead' THEN 1 WHEN 'proposal' THEN 2 WHEN 'negotiation' THEN 3 WHEN 'won' THEN 4 ELSE 99 END;

这里有个统计口径的经验。很多人会把所有阶段的商机金额加在一起当“管道总金额”,这样做其实会虚高。因为刚建立的意向单子赢单率很低,金额再大也大概率落不了地。我在看板里做了两个口径:一个是简单加总金额,另一个是加权金额,也就是amount * probability / 100。真正的业绩预测看加权口径更靠谱,简单加总只能作为参考。

管道看板做出来之后,管理层终于不用每周打电话问销售“你手上到底有哪些单了”,打开页面就知道团队状态,周会可以聊策略和卡点,而不是花半小时对数据。

6. 权限模型与数据安全:小团队也要有边界

6.1 基于角色的权限控制是最小可用方案

DeskcommCRM 的用户量不大,但角色差异很清晰:销售、销售主管、系统管理员。

我没有一上来就搞细粒度的字段级权限,而是先用角色加权限点的方式把边界立起来。权限矩阵大概是这样的:

操作普通销售销售主管管理员
查看自己的客户/商机允许允许允许
查看全团队客户/商机禁止允许允许
创建客户/商机/沟通记录允许允许允许
编辑客户信息自己负责的记录可见可编辑团队记录所有记录
删除记录禁止部分允许允许
查看报表仅自己的数据团队数据全数据
修改系统配置禁止禁止允许

这个矩阵实现起来不复杂,在后端写一个装饰器或者依赖函数,检查当前用户是否具备某个权限点即可。小团队不需要一开始就上 ABAC,那套复杂策略反而会让需求方和开发都陷入细节。

真正麻烦的是数据可见性的控制,也就是“销售只能看自己的数据,主管能看团队的”。这件事如果完全靠业务代码在每一条 SQL 后面拼where owner_id = xxx,很容易漏。尤其是后期接报表模块,开发一着急就容易忘,数据泄露风险很高。所以我最终把这道防线沉到了数据库层。

6.2 用 PostgreSQL RLS 把数据隔离沉到数据库层

RLS(Row-Level Security)是我选 PostgreSQL 的核心理由之一。简单讲,RLS 允许我在表上定义策略,让数据库在执行 SQL 时自动按当前用户过滤行,哪怕应用代码里忘了写where条件,也只会返回该用户能看的数据。

在 DeskcommCRM 里,我给 customers、deals、interactions 这些表都启用了 RLS。大致思路如下:

ALTER TABLE customers ENABLE ROW LEVEL SECURITY; CREATE POLICY customers_owner_access ON customers FOR SELECT USING ( owner_id = current_setting('app.current_user_id')::uuid OR current_setting('app.user_role') = 'admin' OR current_setting('app.user_role') = 'manager' );

每次请求进来时,后端在建立数据库连接后先执行一次:

SELECT set_config('app.current_user_id', :current_user_id, true); SELECT set_config('app.user_role', :user_role, true);

这样后续所有查询都会自动带上隔离条件。需要重点提醒的是,set_config的第三个参数要传true,意思是这个配置只在当前事务内生效,防止连接池复用连接时把上一个用户的信息串到下一个请求里。这个坑我踩过,当时排查了半天才发现是因为连接池复用时没有重置 session 变量,导致某个用户能查到别人的数据,还好是在测试环境暴露的。

对小团队来说,RLS 可能看起来“过度设计”,但我的建议是从第一天就做好。因为权限边界这件事,后期补的成本远高于早期做。数据一旦乱了、泄露了,后果不是花一晚上代码就能补救的。

6.3 审计日志和数据导出不能事后补

CRM 里面大量是客户的敏感信息,系统上线后出了争议数据,比如“谁改了这个客户的负责人”“谁删了一条商机”,如果没有审计日志,基本查不出来。

DeskcommCRM 的审计日志最初用一个简单的表记录:

字段说明
user_id操作人
actioncreate / update / delete / export
entity_type操作对象类型
entity_id操作对象 ID
before_data / after_data变更前后的 JSON 快照
created_at操作时间

在业务代码里,我会在创建、更新、删除操作后统一写审计记录。这里的关键点不是记录“张三改了客户”,而是要记录“张三把客户负责人从李四改成了王五”,所以 before 和 after 的完整 JSON 快照很重要。后续做操作回溯时,直接比较前后快照就能还原现场。

数据导出也要留痕。销售经常要导出自己的客户名单做外勤拜访,主管经常要导出团队管道做周报。我在导出接口里强制记录导出的用户、时间、筛选条件、导出条数,并且给导出文件加权限水印或者文件名带上操作人信息。这个习惯一开始没做,后来有一次客户数据被截图外传,虽然没有实锤,但因为没有导出记录可查,整个排查非常被动。现在规则定了:先有导出日志,才允许点导出按钮。

7. 上线后踩过的几个关键坑与优化实录

7.1 定时任务重复执行引发任务乱流

这个问题前面提过,值得再展开一下。上线几周后,有销售反馈说“为什么同一个客户有四个一模一样的跟进待办”。排查下来发现,后台定时任务是部署在多个节点上跑的,两个 worker 同时扫描,互相没有锁,导致重复生成。

除了加部分唯一索引兜底外,我还把重复任务生成规则改成了“幂等”逻辑:生成之前先查一下这个来源是否已经有 pending 状态的任务,有就直接跳过;即便查询和插入之间有并发空隙,唯一索引也会拦住第二次插入。线上从此没有再出现重复任务。

这件事给我的教训是:任何会触达用户行为的功能,都要假设它会跑多次。加唯一索引也好,加分布式锁也好,必须有一个数据库层面的兜底,而不是只管业务逻辑看起来没问题。

7.2 管道看板统计数字越算越大

管道看板上线后,我开始发现一个诡异的现象:同一天的deal_count比实际商机数多得多。仔细排查后发现是 SQL 里 join 了 multiple contacts。一个客户有 3 个联系人,每个联系人关联 1 个商机,最后 count 出来变成了 3,总金额也乘了 3。

问题出在统计时我从 customers 表出发 join contacts 再 join deals,导致一行商机被联系人数量放大。修复方式很简单,直接以 deals 表为主体,不再 join contacts,或者用COUNT(DISTINCT deals.id)。但这个问题背后有个更重要的设计原则:写统计 SQL 时一定要先理清主表和明细行的粒度。聚合查询尽量从最细粒度的事实表出发,不要为了临时加一个过滤条件去 join 一张会放大行数的表。

7.3 客户搜索越来越慢,pg_trgm 解决了模糊查询

客户量到三五千的时候,搜索框开始卡。原因是搜索接口写的是:

WHERE name LIKE '%关键词%'

这种写法在 PostgreSQL 里无法走 B-tree 索引,只能全表扫描。几千条数据还能忍,万一客户数据到几万、十万,页面就彻底转不动了。

我后来启用了 pg_trgm 扩展,给客户名称建了 GIN 索引:

CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX idx_customers_name_gin ON customers USING GIN (name gin_trgm_ops);

这样即使搜索条件是模糊匹配,数据库也能用三字母组索引加速。实测下来,同样一条模糊查询,在十万条数据上的响应时间从几百毫秒降到了几十毫秒。除了名称搜索,联系电话和邮箱字段我也建了合适的索引。

这个优化几乎零成本,但收益非常明显。如果你也遇到列表页搜索越来越慢的问题,先别急着上 Elasticsearch,先看看数据库本身的模糊搜索能力有没有用好。

7.4 批量导入 Excel 时的性能与脏数据问题

正式切换系统时,存在一个迁移动作:把销售手里十几份 Excel 客户名单统一导入 DeskcommCRM。第一版导入接口是循环逐条插入的,导一千条客户加三千条联系人,跑了十几分钟,中途还因为数据格式不对报错回滚,之前白等。后来又发现 Excel 里的客户名有空格的、有带后缀的,同一家公司被导入了好几遍。

后来重写了导入逻辑:

  • 用批量插入代替循环单条插入,事务粒度改为每 500 条一个批次,单批次失败不影响其他批次。
  • 导入前先排序去重,用客户名称去掉首尾空格和全角半角归一化后作为排重键。
  • 先做数据校验,把非法手机号、缺失必填字段等错误汇总成一个错误清单返回给用户,而不是遇到第一条就直接中止。

改完之后,导入一万条客户数据只需要几秒钟,而且错误清晰可读。数据迁移不是一个技术问题,它直接影响用户对系统初始数据的信任感。第一次导入如果全是脏数据,后续销售就再也不信这个系统了。

最后说一点个人体会。DeskcommCRM 这个项目从规划到上线,前后用了大概两个月。如果让我重来一次,有件事我一定会从第一天就做,就是把数据权限和审计日志当成核心功能去设计,而不是等系统跑起来后再补。权限模型决定了这套系统敢不敢让全团队真正用起来,审计日志决定了出了问题你能不能快速找到根源,这两件事都是前期省事后期加倍还的类型。

做这种内部系统的过程也让我明白,工具的价值从来不在于功能数量多不多,而在于能不能真正解决一群人的协作问题。DeskcommCRM 上线后最让我欣慰的一个画面,是销售老同事在客户详情页里找到了半年前和客户的一段对话记录,然后转头跟我说:“这个系统还挺有用的。”那一刻我觉得,这个自研的轮子没有白造。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询