资讯动态

自研轻量级CRM:PostgreSQL数据建模与权限控制的实战复盘

发布时间:2026/10/19 14:12:59 来源:尧图企业网站定制
很多人问我为什么放着现成的 SaaS CRM 不买非要自己动手做一个 DeskcommCRM。这个问题每次聊都要解释半天今天干脆把整个项目的来龙去脉、设计思路、技术选型和上线后踩过的坑都写出来。DeskcommCRM 是我给团队做的一个轻量级客户关系管理系统核心解决三件事客户资料散落、跟进节奏失控、销售管道不透明。如果你也在小团队里管客户或者正纠结要不要自研 CRM这篇文章应该能给你一些参考。先交代一下背景。我们团队不到二十人销售加客户成功大概十个之前客户资料分散在销售个人微信、邮件客户端、Excel 表格和几张共享网盘里。一开始人少还没觉得有什么问题直到出现了一次比较大的客户投诉一个合作了大半年的客户因为负责对接的同事离职交接时漏了关键信息后续服务断档客户差点流失。那次之后我才下定决心不管买还是做客户关系管理必须有系统承接。1. 为什么最终选择自研而不是直接买现成 CRM1.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 能算清楚的事情就不需要再起一个单独的报表服务。对比项PostgreSQLMySQLMongoDBJSON 扩展字段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_idpayload.customer_id, contact_idpayload.contact_id, owner_idcurrent_user.id, channelpayload.channel, directionpayload.direction, subjectpayload.subject, bodypayload.body, happened_atpayload.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 把数据隔离沉到数据库层RLSRow-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 / exportentity_type操作对象类型entity_id操作对象 IDbefore_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 上线后最让我欣慰的一个画面是销售老同事在客户详情页里找到了半年前和客户的一段对话记录然后转头跟我说“这个系统还挺有用的。”那一刻我觉得这个自研的轮子没有白造。

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

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

免费获取报价 →
↑