ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot CRM系统实战:数据模型、权限设计与小程序落地

Spring Boot CRM系统实战:数据模型、权限设计与小程序落地 做CRM这类业务系统最难的从来不是某个接口调不通、某个页面渲染不出来而是业务模型怎么设计才不会被销售、运营、管理层轮番推翻。我最近把一套Java写的CRM系统完整梳理了一遍后端是Spring Boot那套经典技术栈管理后台走Web另外还配了一套小程序CRM端算是在传统PC办公之外把移动办公这条腿也补齐了。这套源码适合谁呢如果你正在做企业级管理系统、毕业设计选题或者公司内需要一套私有化部署的客户管理系统又不想被商业产品的年费和定制成本套牢那它很值得拿来当基线工程。这篇博客就围绕这套源码讲讲它背后的模块划分、核心表设计、权限模型、小程序端落地方式以及我把项目跑起来过程中踩过的那些真实坑。1. 为什么自己搭CRM会比买商业套件更划算1.1 商业CRM的痛点和自研真正要解决的事最早我接触的是商业CRM功能倒是全客户管理、商机漏斗、合同回款、工单售后一个不落。但真正用到业务里就发现两个问题一是销售场景变化特别快今天要加一个“客户来源渠道”字段明天要调整回款提成计算规则每次定制都走商务流程周期长、报价还不低二是数据都放在SaaS厂商的库里核心客户资产始终不在自己手里做报表、做二次开发、接内部系统都得看对方脸色。所以后来我倾向于自己用源码搭一套。自研不等于从零造轮子真正要解决的核心问题是把客户、跟进、商机、合同、回款这条销售主线完整串起来。围绕这条主线才能真正把系统的复杂度控制住。客户管理不是单纯建一张表、写几个增删改查接口它背后牵扯的是销售的日常跟进习惯、管理层的漏斗分析需求、财务对回款节点的把控以及最容易被忽略的数据权限边界。1.2 这套源码的技术选型逻辑这套源码后端用的是Spring Boot MyBatis-Plus Redis MySQL 8这套组合。Redis用来做登录会话、权限缓存和热点数据缓存MySQL存业务数据。前端管理后台是Vue生态小程序端用原生微信小程序或者uni-app封装的那套方式实现。这里说一个很多新手容易犯的误区项目标题里带了“大型”两个字就想着上微服务、上Spring Cloud全家桶。其实对大部分CRM业务场景“大型”更多指业务模型复杂、数据量大、权限层级深而不是用户并发量真的到了需要十几个微服务拆分的程度。这套源码在架构上采用了模块化单体设计代码层面按业务域拆成独立的包结构customer、sales、contract、workflow、system、report后期如果某个模块真的承担了极高并发再按模块边界单独把服务拆出去。才是兼顾开发效率和后续演进的做法。我的建议是拿到源码先不要急着改代码先花半天时间搞清楚四个问题数据如何初始化、权限如何判定、文件存储到哪里、登录会话如何保持。这四个问题搞明白了整个系统的骨架也就掌握了。2. 后端设计里最值钱的部分数据模型与权限体系2.1 以“客户主档”为中心的数据血缘关系客户是CRM系统的绝对中心几乎所有业务动作都最终落到客户身上。这套源码的表设计没有搞出几十张表那种吓人的规模核心业务表就那么几张但每张表之间的关系很清晰crm_customer客户主档保存公司名、行业、来源渠道、负责人等基础信息。crm_contact联系人一个客户下挂多个联系人联系人是实际对接的人。crm_business商机一个客户下可以有多个销售机会每个商机有自己的金额、阶段、预计成交时间。crm_contract合同商机推进到签约阶段就生成合同合同关联客户和产品明细。crm_receivable回款计划合同回款拆成多期每期有应收日期、金额、状态。crm_follow_record跟进记录销售每次电话、拜访、微信沟通都按时间线记录下来。这里最值得说的设计是客户主档和联系人分表而不是把联系人字段直接堆在客户表里。因为一个客户往往有多个角色联系人老板关心整体预算技术负责人关心产品能不能满足需求采购负责人关心合同流程他们说话的分量完全不一样。把联系人拆出来再给联系人打“决策人”“对接人”“使用人”这种标签销售后续跟进时才不会只认一个窗口。商机表里一定有一个stage字段就是销售阶段从初步接触、需求确认、方案报价、商务谈判到赢单这个字段是管理层做漏斗报表的数据基础。阶段变了要在代码里写阶段变更历史方便复盘是哪个环节卡住了。这套源码里我用了一个简单的stage_log表记录每次变化这个表看着不起眼但管理层复盘时非常依赖它。回款计划也建议拆出来不要光在合同表里写一个“合同总额”就完事。很多合同是分期回款的如果只存总额财务根本没法知道哪笔钱到账了、哪笔钱还没到期。回款计划表按合同ID下钻每期有金额、计划回款日期、实际回款日期、回款状态才能支撑应收报表。DDL里比较关键的是索引设计随便举个例子CREATE TABLE crm_follow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, contact_person VARCHAR(50), follow_type TINYINT COMMENT 1-电话 2-拜访 3-微信/邮件, content TEXT, next_follow_date DATETIME, create_by BIGINT, create_time DATETIME, KEY idx_customer_time (customer_id, create_time) ) COMMENT客户跟进记录;关注idx_customer_time这个联合索引。CRM系统最频繁的查询就是进入某个客户详情页按时间倒序拉这个客户的所有跟进记录。如果只给customer_id建索引数据量上来之后排序还是要回表联合索引能让这个查询非常快。这类索引设计反过来也提醒你建索引不要只盯着查询条件要盯着查询条件加排序条件。2.2 RBAC权限模型和数据权限的落地思路权限体系是CRM区别于普通增删改查项目最重要的部分。这套源码权限分两层功能权限和数据权限。功能权限就是传统RBAC那套用户关联角色角色关联菜单和按钮权限后端接口通过注解校验当前用户有没有访问按钮的权限。登录时把用户的权限标识列表加载到Redis里每个请求进来后从Redis取不用每次都查数据库性能压力小很多。数据权限是更容易被忽略的。销售只能看自己和下属的客户部门主管能看整个部门的客户老板能看全部客户。这个数据权限如果靠业务代码里到处写if判断基本就会写成一锅粥。这套源码的做法是做一个自定义注解DataScope标注在查询接口上注解里写明当前操作的数据级别然后用MyBatis拦截器在SQL执行前自动拼上权限条件。核心逻辑大概是DataScope(type DataScopeType.DEPT_AND_CHILD) public PageResultCustomerVO pageCustomer(CustomerQuery query) { // 正常写查询方法不用手动拼权限SQL }拦截器在解析到DataScope注解后从当前登录用户的上下文里拿到用户ID和部门ID再通过“查询本部门及下级部门所有用户”得到可见的负责人ID集合然后往SQL上自动追加WHERE create_by IN (...)或者WHERE owner_user_id IN (...)。好处是业务代码保持干净新同学加入项目不会因为忘记拼权限条件把数据泄露出去。这类权限设计最大的坑出现在跨部门协作。比如销售A和销售B共同跟进一个客户客户负责人是A但B也有机会看到这个客户如果只用“负责人IN”过滤B就看不到他实际参与跟进客户客户交接和协同就全乱套了。源码里我用了一张crm_customer_share表来承接共享逻辑处理团队协作数据可见性这个细节在二次开发时一定要保留住。3. 小程序CRM端怎么复刻核心流程3.1 移动端保留哪些能力砍掉哪些能力小程序端的定位不是把PC后台整个搬上去而是一个轻量移动工作台。销售在外面跑客户最刚需的场景是快速查客户输入名称直接搜客户看到联系人、电话、地址。新建/编辑客户现场拿到老板名片立刻建档。记跟进记录拜访完之后随手把交流内容录入。看今天的待办哪些客户该跟进了哪些商机快到期了。处理审批申请报价折扣、申请合同用印手机上过一遍流程。PC端那些复杂的东西比如自定义字段配置、角色权限管理、报表大屏设计器这些就不要放在小程序里了。硬塞进去会让小程序包体变大操作路径变长销售用起来反而烦。所以小程序端代码虽然看起来不如后台那么庞大但它把“高频轻操作”这个原则贯彻得很彻底。3.2 登录鉴权链路微信手机号和业务账号的绑定小程序登录是第一个要处理的问题。和普通Web登录不一样小程序可以先通过wx.login拿到一个临时code后端拿这个code去微信接口换openid。但如果业务账号体系是手机号就要在此基础上再走一步绑定逻辑用户首次登录输手机号加短信验证码把openid和手机号绑定下次再进来只要微信静默登录就能换到业务token。小程序源码里的登录流程是这样设计的启动时调wx.login拿到code。把code发给后端后端调微信接口换openid。查询openid有没有绑定过手机号如果没有小程序端弹出手机号授权登录界面。绑定成功后后端签发token返回给小程序后续所有请求带token。这个流程唯一的坑是微信官方对getPhoneNumber的调整新版本拿手机号需要用户点击授权按钮触发不能脚本静默获取。所以源码里做了兼容处理授权拿不到手机号就降级为手动输入手机号加短信验证码的方式。小程序请求后端接口时token放在自定义Header里后端统一拦截器校验。记住一个原则凡是需要token才能访问的接口后端必须做用户状态校验和权限校验不要指望小程序端做了菜单隐藏就够了接口安全永远只信后端。3.3 列表加载、文件上传和缓存处理小程序端的客户列表用的是触底加载分页也就是上拉加载更多。分页参数是pageNum和pageSize响应返回total和records前端拿到下一页数据后拼接进数组同时记录当前已加载页码避免重复请求。这里要处理一个并发问题用户快速多次触发上拉到底时会同时发好几个重复请求。处理办法是加一个isLoading布尔标志请求未返回时忽略新的加载事件细节不复杂但必须有。跟进记录里经常要传图片比如拍一张现场照片、拍一张合同盖章页。小程序源码里文件上传走的是wx.uploadFile这个接口有个老坑它不支持自定义Header直接用application/json只能通过formData传参token也要放在formData里。后端拿到上传文件后保存到配置好的存储目录。如果是本地部署存储路径一定不要写相对路径写绝对路径否则重启后容易找不到文件。小程序端缓存用得好的地方是数据字典。客户来源、商机阶段、跟进方式这些东西不会频繁变化启动时加载一次存到wx.setStorageSync之后页面直接读本地缓存减少网络请求。只有用户手动下拉刷新时才强制重新拉取这是小程序开发里很划算的优化手段。4. 启动源码前必须解决的几个坑4.1 本地环境准备清单拿到源码第一步不是我急着改代码而是把环境弄干净。这套源码后端要求的最低环境是这样的JDK1.8或11都行但建议统一用11避免后期某些语法特性在1.8上编译不过。MySQL5.7或8.0强烈推荐8.0字符集统一utf8mb4排序规则建议utf8mb4_general_ci。会涉及中文和emoji存储utf8mb4是底线。Redis5.0以上版本主要存登录token和数据字典缓存。Maven3.6以上拉依赖用。Node环境看小程序端使用的方式uni-app需要HBuilderX或CLI原生小程序则打开微信开发者工具就行。MySQL导入SQL脚本时最容易出现的坑是timezone问题。连接串必须加serverTimezoneAsia/Shanghai不加的话如果MySQL驱动版本比较新而数据库用的UTC时区存进去的时间会和北京时间差8个小时。这个坑我踩过不止一次现在所有项目的JDBC连接串都会显式指定时区。4.2 配置文件里的三个重灾区配置文件基本集中在application-dev.yml和application-prod.yml不管源码里写的是什么结构这三个配置项必须仔细核对。第一是数据源配置。数据库地址、账号、密码改成自己的连接池参数initial-size和max-active不要照抄取决于你本地机器内存。内存只有8G的时候max-active设成100启动时反而有风险。第二是Redis配置。如果你本地Redis没设密码就把password那行注释掉。序列化这块建议用StringRedisSerializer序列化key用Jackson序列化value不要直接拿默认的JdkSerializationRedisSerializer否则缓存数据肉眼看不出来用RedisDesktopManager看的时候全是乱码。第三是文件存储配置。源码默认支持本地磁盘存储和OSS对象存储两种方式通过一个配置项切换。本地存储时要把upload.path配置成绝对路径比如/opt/crm/upload或D:/crm/upload并且提前创建好目录很多报错是“文件上传失败”其实是目录不存在而不是代码有问题。还有一类比较隐蔽的启动异常是端口冲突。后端默认端口如果配置成8080本地又有其它服务占用了启动日志会提示端口被占用不要慌改spring.application.port就行。4.3 初始化数据与演示账号SQL脚本里一般会包含库表结构和初始数据。初始化数据里至少要有管理员账号登录后能看到所有菜单和所有数据。系统数据字典客户来源、商机阶段、合同类型等基础字典。示例部门和示例用户方便你马上测数据权限。建议第一次启动后先拿管理员账号登录后台进“系统管理”看一眼菜单是不是都加载出来了。如果登录后看到菜单正常但点开客户管理列表页报错多半是数据权限拦截器在解析当前用户部门ID时空指针。这种情况通常是因为初始化数据里admin账号没有设置部门ID处理方式是给admin关联一个顶级部门或者调整拦截器逻辑超级管理员直接就放行不用拼数据权限条件。这也是这套源码里我强烈建议所有权限拦截器先判断“当前用户是不是超级管理员”的原因。5. 从“能跑”到“扛得住”性能与稳定性的处理5.1 查询层优化深分页和慢SQLCRM系统运行半年以上客户表百万行很常见。如果前端分页一直用limit offset方式页码越大查询越慢因为数据库要扫描并丢弃前面所有行。这套源码里我给列表查询做了深分页优化前端传入上一页最后一条记录的ID或时间戳SQL改成WHERE id ? ORDER BY id DESC LIMIT ?也就是“键集分页”。虽然前端代码要稍作调整但百万表上查询时间能降一个数量级。慢SQL排查不要靠猜打开MySQL慢查询日志最直接SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 2;跑两个小时后看slow_log表按rows_examined排序基本就能定位到哪条SQL没走索引。CRM系统里最常见的问题就是SQL里对create_time用了函数处理比如DATE_FORMAT(create_time, %Y-%m-%d) 2024-01-01这样索引会失效。正确写法是范围查询create_time 2024-01-01 00:00:00 AND create_time 2024-01-02 00:00:00。5.2 缓存层优化别什么都往Redis里塞登录用户的权限标识集合适合缓存到Redis数据字典适合缓存但业务数据不要随随便便都缓存。客户列表如果缓存了销售改一条跟进记录其他同事看到的列表数据就可能不一致缓存失效又是一个复杂的工程。这套源码的取舍是字典和权限缓存到Redis客户和商机数据只在单条查询时用Caffeine做本地缓存缓存时间控制在30秒最大条目数配置成10000左右更重要的是一旦有人修改了客户资料主动删除该客户相关的缓存key。这里有一个非常容易踩坑的细节缓存key一定要拼上租户或部门信息不然两个部门的人查同一个客户缓存的查询条件可能互相污染。建议key格式固定为crm:customer:detail:{customerId}:{ownerUserId}防止串数据。5.3 并发和数据一致性跟进记录别丢、审批别重复多个销售同时跟进一个客户会发生在真实场景里比如售前和售后同时维护关系。跟进记录如果直接往表里insert通常没什么冲突但商机的状态变更就得小心。两个用户同时把一个商机从“谈判中”改成“赢单”和“输单”最后谁后提交谁覆盖。解决办法是商机表加version版本号更新SQL带上AND version #{oldVersion}受影响行数为0就提示“数据已变更请刷新后重试”这就是乐观锁。合同审批流程涉及状态机建议用一张workflow_process表把当前审批节点、审批人、审批状态存下来每次审批动作都通过Redis分布式锁包一层。锁的key用业务单号比如approve:contract:{contractId}这样能避免用户在审批页快速点击两次造成重复提交。分布式锁实现可以用Redis的SETNX加过期时间也可以直接引入Redisson源码里我更倾向于用简单轻量的方式毕竟过度设计也要成本。6. 我踩过的坑和二次开发最该预留的口子6.1 权限设计远比功能开发难如果只做增删改查一个周末就能把客户模块跑起来。但真正让CRM难做的是权限设计。统计口径不同的销售团队会有完全不同的权限诉求有人希望“每个销售只看见自己的客户”有人坚持“部门内客户全透明”还有跨部门共享和离职交接。这套源码里的数据权限注解覆盖了从“仅本人数据”到“全部数据”五个级别但具体业务落地时还是建议先把公司组织架构和审批链绘画清楚再动报表。6.2 报表和大屏是最容易被低估的模块管理者看CRM核心就是看几件事每个人有多少客户、商机总金额多少、赢单率怎么样、这个月的回款指标完成了多少。这三张报表的价值甚至超过业务模块本身。源码里报表模块单独抽了出来底层用聚合查询而不是循环查清单算求和前端用ECharts画漏斗和折线图。如果你要二次开发我建议一定把报表和大屏放在需求列表的前几项它是系统上线后能不能让管理层持续买单的关键。6.3 二次开发最该预留的三个扩展点第一是自定义字段。不同行业对客户的描述维度完全不同可以在crm_customer表里预留ext_json字段存扩展属性的JSON代码里封装好get和set方法查询过滤时再用JSON_CONTAINS或者后续升级到ES去处理。第二是操作日志留痕。每一次客户查看、导出、删除最好都存操作日志表这个功能不出彩但出事时能救命。第三是模块间解耦。客户删除不直接物理删除而是打失效标识保留历史数据后续做数据分析时才不会出现合同、回款找不到关联客户的尴尬场面。6.4 一个关于源码学习路径的建议我大概花了两个星期才把这套源码的核心链路完整吃透走了一条弯路一开始从工具类、常量类看起看了三天毫无头绪。后来聪明了一点点不再顺着文件目录看代码而是顺着业务场景看从登录接口开始走一遍“登录获取token - 进入客户列表 - 打开客户详情 - 发起跟进 - 新建商机 - 创建合同 - 生成回款计划”这条主链路每个接口都从Controller入口往下追到Service和Mapper整个系统怎么运转就全清楚了。这种“跟着业务流走代码”的方式比按包名目录看代码高效得多。这套源码现在已经是我做企业管理系统时反复参考的基线了每次二次开发新的业务系统都会先把它加载起来把数据权限、操作日志、缓存处理这些老底子逻辑先迁移过去再谈新需求。最后再分享一个小技巧如果你在改权限模块时被部门树递归搞到头大先别急着写递归直接在内存里用两层循环组装Map解决问题。递归优雅是优雅但团队里不是每个人都看得懂简单能跑比优雅更重要。
返回列表