恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

小团队自研轻量级CRM:从数据模型到部署运维的完整实践

  • 首页
  • 资讯中心
  • /
  • 小团队自研轻量级CRM:从数据模型到部署运维的完整实践

相关资讯

皮尔逊P-Ⅲ曲线应用软件:水文频率计算与设计洪水推求避坑指南 2026/10/19 19:45:12
低剂量TEM图像去噪:非局部机制如何保住晶格细节 2026/10/19 19:45:12
open-code-review:基于规则驱动的AI协同代码评审范式 2026/10/19 19:40:12

最新资讯

Kafka全链路性能调优:吞吐量与延迟的实战权衡
ZXing.Net 0.16.8.0 条码识别实战:从解码链路到性能调优
从线程池失控到全局限流:高并发系统稳定性实战指南
STC单片机范例程序实战指南:从点灯到多外设工程搭建
AI率过高怎么办?九款降AI率工具与避坑指南
开源代码评审工作流:本地化、可审计、可定制的LLM实践方案

今日推荐

OpenCowork 实测:支持本地文件、飞书机器人的 Windows AI 助手(只需配置 Token)
GPT-6 Sol 与 Luna 怎么选?Chat、Work、Codex 版本差异与 TaoToken 接入实测
openclaw.net 集成 ElBruno.MempalaceNet 记忆系统:把 MCP 配置改到 TaoToken 的完整实践

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

小团队自研轻量级CRM:从数据模型到部署运维的完整实践

发布时间:2026/10/19 19:45:12
小团队自研轻量级CRM:从数据模型到部署运维的完整实践 如果你问一个十几人的 B2B 销售团队最需要什么工具十有八九会得到两个回答一个是要把客户、报价和跟进记录收拢到一起的地方另一个是别让自己忘了今天该给哪个客户打电话。去年我们把这两个需求做成了一套内部系统名字叫 DeskcommCRM。当时只是想解决自己公司的销售管理问题结果一年用下来它反而成了业务后台里使用频率最高的系统。DeskcommCRM 是一个面向小规模 B2B 销售团队的轻量级客户关系管理系统核心解决三件事客户信息统一沉淀、跟进记录连续可追溯、待办事项自动汇总到个人工作台。项目从原型到上线只用了一个多月后续边用边改逐步稳定中间踩了不少数据库和部署的坑也总结出一些不写代码也能让团队用起来的地推式经验。这里把从业务调研、数据模型、技术选型到部署运维的完整过程整理出来给正在考虑自研 CRM或者准备在团队里推进销售数字化的朋友一个可以参照的样本。1. 为什么一个外包服务团队最后被逼着自研了DeskcommCRM1.1 当时销售过程管理到底有多乱团队大概十个人业务以项目制外包为主一个单子从需求沟通到签约短则一周长则三个月。最混乱的不是没有表格而是表格太多。每个人手里都有一份自己维护的客户跟进表有人用 Excel有人用在线文档还有人只靠聊天记录保存关键信息。月底统计商机金额的时候销售经理得挨个问“你手头有几个单子在推进”问出来的数字还经常对不上。这种乱在刚开始还能忍直到连续发生两次客户资源冲突同一个客户被两个销售同时跟进报价各报各的客户直接质疑我们内部是不是没有沟通。这时候才意识到问题不是销售不努力而是缺少一个共同的业务事实源。客户信息、跟进历史、报价记录必须有一个系统承接而不是依赖某个人的记忆或聊天记录检索。1.2 市面上的CRM为什么没有留住我们决定上系统之后我们其实先把主流的 SaaS CRM 产品都试用了一遍。客观讲功能确实全面从线索导入、自动分配、电销一体到 BI 报表都有。但对十人左右的团队来说问题也很明显轻量版本的自定义能力通常被砍得很厉害销售要用的核心字段经常被锁死功能齐全的版本又带着一大堆我们用不上的模块每个销售都要花时间去理解那些与日常无关的菜单学习成本最后都变成团队效率的消耗。另外一个很现实的因素是数据归属。客户联系方式、历史跟进记录、商务报价都属于核心业务资产放在第三方平台上每次想到数据导出和账号权限管理心里就没底。既然团队里已经有能写后端和前端的同事不如自己动手做一个数据完全在自己手上的系统DeskcommCRM 就是这么立项的。1.3 DeskcommCRM 的定位和名字是怎么来的项目代号是 Deskcomm拆开看是 Desk Communication中文可以理解成“桌面沟通工作台”。Desk 强调团队每天打开电脑最先看到的就是它系统第一屏不是统计报表而是“今天要跟谁沟通”Comm 则强调这个系统记录的核心是人与人之间的沟通客户是活的不是躺在表格里的静态字段。这个定位直接决定了后续产品结构一个以“下一步行动”为引导的工作台而不是一个只能查资料的客户数据库。定位也收敛得很克制面向 5 到 20 人的小规模 B2B 销售团队不做销售过程自动化也不做复杂营销漏斗只把“客户、商机、跟进记录、待办提醒”这四件事管好。范围一旦收敛设计和开发的方向立刻清晰了很多。2. 数据模型设计把公司、联系人和商机用清晰的模型串起来2.1 先理清业务对象再画表开始写代码前我们花了两个下午在白板上梳理业务对象。B2B 外包业务里客户主体一定是“公司”但真正打交道的是公司里的联系人。经常出现的情况是先认识客户 A没过多久 A 跳槽了但公司还在不能因为对接人走了就把客户历史删掉反过来一家大公司里可能有多个独立业务线分别推进不同商机。因此客户模型必须有公司和联系人两个层级每个商机挂在公司下同时指定具体联系人。对应到表结构companies表存公司维度信息contacts表存联系人deals表存商机。所有表都保留owner_id用来标识当前负责人这也是后续做权限隔离的基础。实际跑了一年下来这个模型比把公司和联系人混在一张表里的方案健壮很多。CREATE TABLE companies ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, industry VARCHAR(100), source VARCHAR(50), owner_id BIGINT REFERENCES users(id), created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE contacts ( id BIGSERIAL PRIMARY KEY, company_id BIGINT NOT NULL REFERENCES companies(id), name VARCHAR(100) NOT NULL, title VARCHAR(100), phone VARCHAR(50), wechat VARCHAR(100), is_primary BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE deals ( id BIGSERIAL PRIMARY KEY, company_id BIGINT NOT NULL REFERENCES companies(id), contact_id BIGINT REFERENCES contacts(id), name VARCHAR(255) NOT NULL, amount NUMERIC(14, 2) DEFAULT 0, status VARCHAR(20) NOT NULL, owner_id BIGINT REFERENCES users(id), expected_close_date DATE, next_follow_up_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT now() );2.2 跟进记录为什么单独建表最初也有人提议把跟进记录做成deals表里的一个 JSON 字段理由是“每次沟通内容直接拼到商机备注里反正最后也是给人看的”。这个方案在第一周就被否掉了。原因一是备注越拼越长数据库读取越来越慢前端渲染也越来越卡二是跟进记录的统计价值很大比如按周看每个销售的沟通量、按商机看跟进了多少轮这些都需要结构化数据来支撑。所以我们单独建了activities表。每条跟进记录包含类型电话、微信、现场拜访、内容、操作人、创建时间。内容做成普通文本域鼓励销售用一两句话记录关键信息比如“客户对报价提出异议希望折扣再降低 5%”。这些记录按时间倒序排列在商机详情页整个谈单过程一目了然完全替代了过去翻聊天记录的工作。CREATE TABLE activities ( id BIGSERIAL PRIMARY KEY, deal_id BIGINT NOT NULL REFERENCES deals(id), user_id BIGINT NOT NULL REFERENCES users(id), type VARCHAR(20) NOT NULL, content TEXT, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_activities_deal_created ON activities(deal_id, created_at DESC);2.3 next_follow_up_at整个工作台的心脏系统上线第一个月销售反馈最强烈的一个设计是deals表上的next_follow_up_at也就是“下次跟进时间”。销售每天早上到公司不翻消息记录打开首页工作台系统自动把当天应跟进的商机排好点击进入商机详情里面就有上一次沟通的完整上下文。为了防止这个字段失效产品规则上做了一个强制动作每次新增跟进记录时必须顺手填写下次跟进时间否则不允许保存。前期确实有人觉得麻烦但两周后大家发现这个“强制填写”恰恰是在帮自己不遗漏客户反而成了系统口碑最好的功能。从数据模型看这就是一个普通时间戳但在产品逻辑上它是整个销售节奏的驱动力。3. 技术栈与工程落地FastAPI、React和PostgreSQL的组合逻辑3.1 选型时为什么没有盲目追新技术选型有一条原则团队写起来最快、最容易维护的组合就是好组合。我们后端团队熟悉 Python最终选了 FastAPI前端选了 React Ant Design数据库用了 PostgreSQL缓存和定时任务用 Redis APScheduler。没有上微服务也没有上 Kubernetes因为这个系统的规模顶多服务几十个内部用户单体应用加一台云服务器就足够了。FastAPI 的优势比较直观自带 OpenAPI 文档接口调试不用额外开工具基于 Pydantic 做参数校验定义请求和响应模型时写起来很省心异步特性对 CRM 这种 IO 密集场景也有一定帮助。更重要的是Python 生态里做 Excel 导出、数据统计、写运维脚本都非常顺手系统上后期加的一堆管理功能差不多都是短脚本实现的。前端选择 React Ant Design主要看重的是表格、表单、抽屉这类后台组件很完整对不擅长写 CSS 的后端开发者非常友好。数据库选 PostgreSQL核心原因是 JSONB 支持和比较丰富的索引类型后续如果要给客户资源增加自定义字段不需要频繁改动表结构可以直接在 JSONB 列里扩展。3.2 后端API与权限校验的核心写法CRM 系统的 API 大部分是对资源的增删改查重点不在怎么写接口而在权限控制和列表查询。我们使用 JWT 做身份认证每个受保护接口都通过get_current_user依赖拿到当前用户。数据权限只做两级普通销售只能看自己负责的数据销售经理和管理员可以看全团队数据。from fastapi import Depends, FastAPI, Header, Query from sqlalchemy import func, or_, select from sqlalchemy.orm import Session, selectinload app FastAPI() def get_current_user(authorization: str Header(...)): # 解析 JWT返回当前用户对象 return decode_token(authorization) app.get(/api/v1/deals) def list_deals( status: str | None None, assignee: str me, keyword: str , page: int Query(1, ge1), page_size: int Query(20, ge1, le100), db: Session Depends(get_db), user Depends(get_current_user), ): query select(Deal).options( selectinload(Deal.company), selectinload(Deal.contact), ) if assignee me: query query.where(Deal.owner_id user.id) if status: query query.where(Deal.status status) if keyword: like f%{keyword}% query query.where( or_( Deal.name.ilike(like), Deal.company.has(Company.name.ilike(like)), ) ) total db.scalar(select(func.count()).select_from(query.subquery())) items db.scalars( query.order_by(Deal.next_follow_up_at.asc()) .offset((page - 1) * page_size) .limit(page_size) ).all() return {total: total, items: items}列表查询最容易踩的坑是 N1。返回商机列表时如果每条商机都单独查一次公司名和联系人名接口性能会非常差。我们使用 SQLAlchemy 的selectinload一次性把关联对象加载出来配合分页上线后在几十万条记录下实测响应可以稳定在 300 毫秒内。3.3 前端工作台让销售一打开就知道今天该干什么所有页面里我们花心思最多的是首页工作台而不是客户列表。工作台一共三个模块今日需跟进商机、逾期未跟进任务、本周新增客户统计。顶部有全局搜索框支持按公司名、联系人、商机名搜索。我们还额外加了一个拼音首字母检索字段比如输入“xj”能搜到“新洁”。这个细节看起来不起眼却让很多习惯用拼音打字的销售人员接受度提升了一大截。工作台的查询逻辑很简单找出当前用户负责、next_follow_up_at在今天之前的商机按时间升序排列。逾期项用醒目的红色标识单个卡片点击后直接跳到商机详情页展示完整跟进历史。日常使用中90% 的操作可以在一到两次点击内完成这比提供复杂菜单重要得多。4. 部署运维和真实踩坑从Docker Compose上线到备份恢复4.1 用一套Compose文件跑通所有服务系统跑在一台 2 核 4G 的云主机上用 Docker Compose 编排四个服务前端 Nginx 容器、后端 API 容器、PostgreSQL、Redis。没有单独买负载均衡最外层由 Nginx 监听 80/443反向代理到前端容器前端再通过 API 路径把请求转发到后端容器。version: 3.8 services: db: image: postgres:15-alpine environment: POSTGRES_DB: deskcomm POSTGRES_USER: deskcomm POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data ports: - 127.0.0.1:5432:5432 restart: always redis: image: redis:7-alpine restart: always api: build: ./backend environment: DATABASE_URL: postgresql://deskcomm:${DB_PASSWORD}db:5432/deskcomm REDIS_URL: redis://redis:6379 depends_on: - db - redis restart: always web: build: ./frontend ports: - 8080:80 depends_on: - api restart: always volumes: pgdata:这里有个容易踩的坑前端容器构建时把 API 地址编译进去部署不同环境就会很痛苦。我们的解法是在 Nginx 配置里加一条location /api/ { proxy_pass http://api:8000; }前端所有请求都走相对路径/api这样不管部署到哪都不用重新构建前端镜像。4.2 数据库优化索引、连接池与慢查询系统上线初期接口并不慢但随着数据量累积大约一个月后商机列表接口开始出现 1 秒以上的延迟。排查下来主要集中在两个地方条件查询没有走索引以及 ORM 默认懒加载导致的额外查询。索引方面除了主键我们在deals表的owner_id status next_follow_up_at上加了联合索引在activities表的deal_id created_at上加了联合索引。连接池直接使用 SQLAlchemy 默认的pool_size5和max_overflow10内部使用量不大没有过度调优。值得一提的经验是对客户名称表开启 PostgreSQL 的pg_trgm扩展用 GIN 索引支撑模糊搜索客户名搜索的响应速度有明显提升。4.3 备份策略和时区问题要提前处理备份用的是 crontab pg_dump每天凌晨 2 点全量压缩保留最近 30 天。#!/bin/bash BACKUP_DIR/data/backup/deskcomm DATE$(date %F_%H-%M) mkdir -p $BACKUP_DIR docker compose exec -T db pg_dump -U deskcomm deskcomm | gzip $BACKUP_DIR/deskcomm_$DATE.sql.gz find $BACKUP_DIR -name *.sql.gz -mtime 30 -delete实际发生过一次误删商机数据从备份恢复后只丢了不到一天的新增记录属于可接受范围。如果对数据完整性要求更高可以考虑每天做多次增量备份但对小团队一天一次基本够用。时区是运营中很容易忽视的问题。我们踩过一个典型坑销售晚上 8 点录跟进记录系统存的是 UTC 时间导致当天“今日新增跟进数”统计变成 0。最终方案是数据库统一用timestamptz存 UTC所有展示都通过接口层根据用户时区转成北京时间转换逻辑集中在一个工具函数里不散落在页面各处。5. 权限、报表和“团队真正愿意用”的落地细节5.1 权限模型用归属过滤代替复杂RBAC小团队内部权限不需要做成动态 RBAC我们只定义了三种角色销售、Manager、Admin。销售只能看自己负责的数据Manager 除了看全部数据还能调整商机的负责人Admin 负责账号管理和系统配置。在代码里这种权限用一个简单函数就能实现查询时判断用户角色如果不是 Admin 或 Manager就自动在查询条件里追加owner_id current_user.id。好处是逻辑直观不容易出越权漏洞坏处是未来如果要做跨团队协作需要引入角色矩阵但对当前场景来说完全够用。字段级权限也做了一层普通销售的商机详情不显示成本价字段这已经覆盖了我们所有的权限边界。5.2 销售报表实现中的几个关键口径报表模块是上线两周后才加的一开始只需要一个视图每月每个销售的新增商机数、赢单金额和赢单率。用 SQL 聚合写起来很快但统计时要注意“金额归属时间”的定义。是按商机创建时间算还是按赢单时间算不同口径结果完全不同。我们统一按赢单时间计入当月业绩创建时间只作为趋势分析参考。SELECT u.name AS sales, COUNT(DISTINCT d.id) AS deal_count, SUM(CASE WHEN d.status won THEN d.amount ELSE 0 END) AS won_amount, ROUND( COUNT(CASE WHEN d.status won THEN 1 END)::numeric / NULLIF(COUNT(*), 0) * 100, 1 ) AS win_rate FROM deals d JOIN users u ON d.owner_id u.id WHERE d.created_at date_trunc(month, now()) GROUP BY u.id, u.name ORDER BY won_amount DESC;这个 SQL 在几千条数据量下执行非常快。真正需要优化的是按月同比、环比这类跨整个历史范围的计算。因为内部报表只给两个管理者看我们用 Redis 缓存每日结果每天早上 8 点刷新一次足够使用。5.3 比功能更重要的怎么让销售愿意打开这个系统系统做得再完善销售不打开就是白搭。我们总结出三条很土但很有效的经验。第一减少录入成本。客户和商机的关键字段尽量做成下拉选择或自动识别能选的不让手输。联系人电话录入后自动格式化公司名支持首字母检索。我们给自己定的目标是销售录入一个新客户的时间控制在 30 秒以内。第二让系统替代“人盯人”。以前销售经理每周一早上都要在例会上挨个问“这周计划跟哪些客户”系统上线后经理直接在后台看工作台的跟进计划例会只讨论异常项时间缩短了一半。第三及时反馈。系统发出待办提醒销售点击处理后界面要立刻变成“已完成”这种即时反馈会鼓励人继续点击。若延迟超过一秒钟用户就会觉得卡顿慢慢开始排斥操作。这些细节不涉及高深技术但正是它们让 DeskcommCRM 真正融入了日常业务。最后再分享一点个人经验。DeskcommCRM 这个项目给我最大的收获不是学会了 FastAPI 或 Docker Compose而是意识到工具的价值永远取决于使用它的那些人。代码里最复杂的部分不是商机状态机也不是权限过滤而是让业务负责人愿意把团队的真实规则讲清楚。如果你也想自研一套 CRM别急着写代码先花一周时间坐到销售旁边看看他们每天是怎么工作的。很多时候一个简单的“下次跟进时间”提醒比十个华丽的报表模板更有用。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号