news 2026/10/1 18:41:21

SpringBoot+ECharts构建CRM客户管理系统:从SSH迁移到报表可视化全复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+ECharts构建CRM客户管理系统:从SSH迁移到报表可视化全复盘

接手过老式jsp+servlet项目的人应该都有同感:一个客户关系管理系统看着不复杂,真动起手来却发现客户、商机、跟进、审批、报表、权限这些模块盘根错节,牵一发动全身。最近我刚好把一套用了好几年的SSH老系统整体迁移到SpringBoot上,顺手用ECharts把销售漏斗和业绩报表重做了一遍,技术栈就是标题里这套:java + springboot + echarts + freemarker + layui + maven + mysql。整个过程从工程搭建到核心模块落地,再到打包部署排查,踩了不少坑,今天完整复盘一下,给准备自研CRM或者接手SpringBoot项目的朋友一个可参考的蓝本。

整个项目不是简单套个增删改查框架,而是要把销售日常真正用起来。下面我按项目的实际推进顺序来拆解,从业务设计、技术选型,到模块实现、数据库优化,再到部署排查,每一块都会讲清楚为什么这么选、怎么做,以及实践里容易翻车的位置。

1. CRM项目的业务拆解与技术选型逻辑

1.1 业务需求拆解:不要把CRM做成一个客户列表

很多团队一提CRM,第一反应就是把客户信息表格化,做几个页面能增删改查,再导个Excel就算完事。这种系统上线以后大概率没人用,因为它只解决了“记录”问题,没有解决“推进”问题。

一个能给销售带来真实帮助的CRM,至少要包含三条业务主线:

  • 客户资料沉淀:客户基本信息、所属行业、客户分级、来源渠道、下次跟进时间。重点不是字段多,而是要让销售一打开客户详情就能判断“这个人值不值得继续投入精力”。
  • 商机阶段推进:从初步接触到需求确认、方案报价、商务谈判、赢单/输单。每个阶段要有状态字段、预计成交金额、预计成交日期,这样销售管理层才能从数据里看出哪些单子有风险。
  • 跟进过程留痕:谁在什么时候通过什么方式(电话、拜访、微信)跟进过哪个客户,聊了什么,下一步计划是什么。这部分最容易被忽视,但恰恰是后续复盘客户流失原因的关键。

除此之外,老板关心的报表也不能省:每个销售的量、成交率、漏斗转化率、月度回款趋势,这些数据如果全靠人工汇总Excel,系统就失去了意义。

我当时设计的时候,把业务模块拆成了五张核心主表外加日志、部门、用户表,整体上采用一个“客户-联系人-商机-跟进”的一对多树形结构。数据模型理顺之后,后面的代码才是水到渠成的事,否则一边写代码一边改表结构,返工成本非常大。

1.2 技术选型:SpringBoot+FreeMarker+Layui这套老牌组合为什么还值得用

技术选型上,我仔细考虑过要不要上前后端分离,最后放弃Vue+SpringBoot的组合,选回了FreeMarker+Layui。原因很简单:团队规模不大,没有专职前端,销售系统又强依赖服务端渲染的快速迭代能力。

SpringBoot负责整体框架这一点没有悬念,它对Spring生态的整合几乎零配置,内嵌Tomcat让开发环境不用再单独装服务器。选FreeMarker而不是JSP,核心是JSP在SpringBoot里支持得比较别扭,特别是jar包部署时JSP的编译和访问总会出现莫名其妙的404,而FreeMarker天然适合模板渲染,语法干净,对Java对象直接访问属性,前端同事配合起来阻力也小。

Layui在这个项目里是作为后台管理UI使用的。它基于jQuery,组件成熟,表格、表单、弹层、分页都有现成封装。现在前端框架五花八门,但企业后台系统的场景下,Layui这种“不需要node环境、不折腾构建工具”的方案依然很能打,一套JS文件引入即可用,特别适合服务端渲染架构。

ECharts的定位是数据可视化。CRM里面最常见的图表是漏斗图(商机阶段转化)和折线图(业绩趋势),ECharts对这类场景支持特别完善,配置项直观,图表交互细腻,而且是完全本地化的开源项目,没有外网依赖。

MySQL负责所有业务数据落地,Maven负责依赖管理和构建。这套组合最适合的是中小型企业内部管理系统:人力有限、上线周期短、不需要过度设计,又能保证代码结构清晰、后续可维护。

2. 工程初始化与SpringBoot核心配置

2.1 Maven工程结构设计与POM依赖管理

项目刚入手第一步是创建Maven工程。这里我不建议一上来就搞微服务多模块拆分,CRM这种体量,一个spring-boot-maven-plugin打出的可执行jar包就能搞定所有功能,多模块只会增加维护成本。

我在pom.xml里核心引入了这么几个依赖,这里直接贴一段简化版:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> </dependencies>

几个细节值得注意。第一是SpringBoot版本,我特意选了2.7.x而不是3.x,因为3.0开始强制JDK17,而且很多旧版三方组件兼容性踩坑严重,很多小团队生产环境还在JDK8,直接上3.x会异常痛苦。第二是mysql-connector-j这个新坐标,老版本的com.mysql.cj.jdbc.Driver驱动名依然可用,如果需要反编译排查底层驱动行为,新坐标对应jar包结构更清晰。第三是PageHelper分页插件,CRM列表页的搜索分页是刚需,自己手写limit计算工作量不大但容易疏忽total数,用插件能把精力集中在业务SQL上。

2.2 application.yml中的关键配置与连接参数

SpringBoot的配置集中在application.yml里,我用多环境配置管理,默认开发环境、生产环境通过启动参数切换。数据库连接这块配置直接决定了系统稳定性,重点看这几项:

server: port: 8080 servlet: context-path: /crm spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/crm_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 validation-query: SELECT 1 freemarker: template-loader-path: classpath:/templates suffix: .ftl cache: false charset: UTF-8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.crm.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这里有几个我踩过的大坑,单独说。

数据库URL里的useSSL=false一定要加上,否则高版本MySQL驱动默认开启SSL握手,碰到自签名证书会直接报错,明明账号密码对却连不上库。serverTimezone务必指定,我一开始漏了,启动报“The server time zone value”的错,加上Asia/Shanghai才消停。allowPublicKeyRetrieval=true这个参数很多新项目会碰到,特别是用MySQL 8.0以上配合caching_sha2_password认证方式时,不加这个参数,驱动会拒绝通过非SSL通道获取公钥,启动能过但第一次查询连接就闪断。

FreeMarker的cache:false在开发调试阶段必须关掉,否则每次改模板都要重启服务才能看到效果。生产环境记得改回true,否则性能会受很大影响。MyBatis的驼峰映射也要打开,数据库字段是create_time,Java属性是createTime,映射规则打开后就不用写一堆繁琐的resultMap了。

2.3 FreeMarker模板与Layui静态资源的整合方式

FreeMarker模板放在src/main/resources/templates下,Layui的css、js文件放在src/main/resources/static下,SpringBoot默认会把这些资源映射出去。一个容易踩的坑是,如果把Layui的layui.js放在templates目录下,页面访问会404,因为templates目录默认不对外暴露静态资源,必须走static。

我习惯把公共布局抽成独立ftl片段,比如header.ftl、sidebar.ftl、footer.ftl,再用FreeMarker的include指令引入,这样每个功能页面只需要关心自己的主体内容:

<#include "common/header.ftl"> <div class="layui-container"> <#-- 主体内容区域 --> </div> <#include "common/footer.ftl">

变量渲染时有个细节:如果后端传值为null,FreeMarker默认会直接报错,而不是输出空字符串。我用两种方式处理,一是在模板里给变量加默认值,比如${customer.phone!'-'};二是在application中配置spring.freemarker.settings.classic_compatible=true,兼容空值输出。不管哪种,都不要让一个null值把整个页面拖崩。

3. 核心业务模块的设计与实现

3.1 登录认证、用户权限与Session管理

登录认证这块我选的是比较传统的Session方案,SessionId种在Cookie里,拦截器统一校验,没有引入JWT。原因很实际:CRM的用户量不大,几十到几百人,Session方案够用且实现简单;JWT解决的是无状态和跨域问题,但会带来会话失效不好控制、密钥管理复杂的问题,对内部系统反而增加负担。

密码存储一定不能明文,我用的方案是MD5加盐,盐值固定一串随机字符串,注册时加密入库,登录时用同样算法加密比对。MD5现在不是最安全的,但对内部系统足够,如果安全要求更高,可以替换成BCrypt,做法类似。

拦截器实现思路是定义一个HandlerInterceptor,preHandle方法里检查当前请求的Session是否存在loginUser对象。没有就跳转到登录页。SpringBoot中注册拦截器要继承WebMvcConfigurer接口:

@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/doLogin", "/static/**", "/favicon.ico"); } }

这里最关键的细节是白名单:静态资源路径一定要排除,否则页面压根加载不了CSS和JS。我在一开始就漏了static放行,登录页丑得没法看,排查半天才发现是拦截器把layui文件全给拦住了。

Session对象里我建议只放用户ID、用户名、部门ID、角色标识这些轻量信息,千万别把整个用户对象塞进去,用户表字段一多,Session会膨胀,而且用户资料更新后旧的Session数据不会自动同步,容易造成权限判断陈旧。

3.2 客户列表、分页搜索与多条件查询的实现套路

客户列表是CRM系统信息密度最高的页面。我实现它时用PageHelper插件完成物理分页,配合动态SQL处理搜索条件。先看Controller层接口:

@RequestMapping("/customer/list") public String list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, CustomerQuery query, Model model) { PageHelper.startPage(page, limit); List<Customer> list = customerMapper.selectByCondition(query); PageInfo<Customer> pageInfo = new PageInfo<>(list); model.addAttribute("pageInfo", pageInfo); model.addAttribute("query", query); return "customer/list"; }

关键点在Mapper的XML里,条件是否生效要用if标签动态拼接,不能靠字符串硬拼SQL,那是SQL注入的重灾区。例如按客户名模糊搜索,姓名可能为空,就需要:

<select id="selectByCondition" resultType="com.example.crm.entity.Customer"> SELECT id, name, phone, industry, level, owner_id, next_contact_time FROM customer <where> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR phone LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="ownerId != null"> AND owner_id = #{ownerId} </if> <if test="level != null and level != ''"> AND level = #{level} </if> </where> ORDER BY create_time DESC </select>

分页插件一个容易出问题的地方是,PageHelper.startPage之后必须紧跟第一条查询语句,中间不能有其他MyBatis查询和逻辑处理,否则分页会串到别的SQL上。我刚开始写的时候在startPage和Mapper调用之间加了一个日志查询,结果列表页每次都查出来全表数据,还特别慢。

多条件查询页面上我用了Layui的form组件,搜索按钮触发table.reload,把表单数据作为参数提交。后端返回的是HTML页面就不用table.reload那套了,直接让form提交GET请求,保持URL可读可分享,更适合服务端渲染的形态。

3.3 商机阶段流转与跟进记录的落地方案

商机是销售漏斗的数据基础,设计上它必须挂在客户之下,又有独立的阶段状态。我在数据库表里设计了几个关键字段:stage(阶段)、amount(预计金额)、expect_deal_date(预计成交日期)、probability(赢单概率)。阶段用数字编码,1到6分别对应初次接触、需求确认、方案报价、商务谈判、赢单、输单,这个枚举放在代码里统一管理,避免前后端各维护一份。

页面上的商机看板按阶段分列展示,每一列是一个Kanban卡片。实现上不需要特复杂的技术,后端按阶段分组查询,前端用Layui的栅格布局排开,拖拽流转我用的是前端form select切换阶段,提交后由后端更新stage和probability。状态流转这里一定要做校验,比如从“初次接触”跳转到“赢单”就不合理,我加了一个简单的阶段顺序校验拦截器,保证数据逻辑完整。

跟进记录做成时间线样式,与客户详情页并列展示。每次跟进的类型、内容、下次跟进时间、创建人都要记录。代码里就是一张follow_up表的多表插入,关键点是插入后要更新customer表的next_contact_time字段,否则销售永远不知道自己下一次该联系谁。这个联动我一开始没做,后续跑了一段时间数据后,发现很多没有下次跟进时间的客户躺在列表里,销售根本想不起来还有这些待办,这就是典型的数据孤岛问题。

3.4 ECharts报表模块:从SQL聚合到前端可视化的完整链路

报表模块是这个系统里最出效果的部分。我做了三个核心图表:月度业绩趋势折线图、销售个人业绩排行柱状图、商机阶段漏斗图。

后端接口不做模板渲染,而是返回JSON数据。比如漏斗图接口,按商机表里的stage分组统计数量,SQL很简单:

SELECT stage, COUNT(*) AS cnt FROM business_opportunity WHERE del_flag = 0 GROUP BY stage ORDER BY stage

Controller把查询结果封装成Map,用@ResponseBody注解直接吐JSON。前端页面通过Ajax拿到数据后,组装成ECharts的series数据格式,调用setOption渲染。

这里有一个每次都会踩的坑:ECharts实例如果已经渲染过一次,第二次setOption时旧数据不会自动清掉。我在漏斗图上调了好久,发现切换时间范围后柱子高度对不上,因为新旧数据叠加了。解决方法是每次请求前调用chart.clear(),或者在setOption时设置notMerge参数为true。

$.get('/report/funnel', function (resp) { var chart = echarts.init(document.getElementById('funnelChart')); chart.clear(); chart.setOption({ series: [{ type: 'funnel', left: '10%', width: '80%', data: resp.data, label: { show: true, formatter: '{b} : {c}' } }] }); });

图表容器div的宽高一定要显式设置,很多刚接触ECharts的人把容器写成百分比高度,外层父div没有高度,最后渲染出来图表是扁的。我在报表页面里给每个图表容器都设了固定高度,实测500px左右比较合适。

4. 数据库表结构与性能优化实战

4.1 CRM核心表结构设计思路

数据库设计是整个系统稳定性的地基,我把核心表结构列出来,可以作为一个自己动手建库时的参考模板。

客户表:

CREATE TABLE customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '客户名称', phone VARCHAR(20) COMMENT '联系电话', industry VARCHAR(50) COMMENT '行业', level TINYINT COMMENT '客户等级 1-3', owner_id BIGINT COMMENT '负责人用户ID', next_contact_time DATETIME COMMENT '下次跟进时间', del_flag TINYINT DEFAULT 0 COMMENT '删除标记', create_time DATETIME, update_time DATETIME, KEY idx_owner (owner_id), KEY idx_next_contact (next_contact_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

商机表、跟进表在结构上类似,都会冗余一个customer_id外键和多一个del_flag逻辑删除标记。用逻辑删除而不是物理删除,对销售数据尤其重要,客户虽然可能被标为无效,但历史数据还需要追溯分析,真从库里删掉后续做统计就断层了。

所有表的主键用BIGINT自增,字符集用utf8mb4而不是utf8,原因是utf8在MySQL里最多3个字节,存不了emoji和生僻字。销售填客户备注的时候可能带个表情符号,如果是utf8会直接报错。基础的表结构设计不想多说,但这两个坑我见到太多人踩过了。

4.2 最容易出问题的SQL与索引优化

CRM系统的查询模式相对固定,优化重点就在几个高频列表页。我最常做的优化是给外键和查询条件字段建索引,像customer表的owner_id、next_contact_time都加了索引。

模糊搜索要特别注意。LIKE ‘%关键词%’这种写法会导致索引失效,全表扫描。客户量没上十几万的时候问题不大,但数据一旦膨胀,一个不带条件的列表查询能把数据库CPU打满。折中的办法是:如果确实需要模糊搜索,把范围限定在20万行以内强制走索引;更合理的方案是引入专门的全文索引,但小项目我一般建议保持简单,先把高频搜索条件做成下拉选择,减少模糊匹配的触发频率。

另一个大坑是深分页。销售使用系统半年后,客户表可能几十万行,翻到第1000页时limit 9000,10会导致MySQL扫描前9000行后丢弃,特别浪费I/O。我用它替换成基于最大主键的查询方式:

SELECT * FROM customer WHERE id > #{lastId} ORDER BY id ASC LIMIT 10

这种“上一页最后一条记录的ID”游标分页方式在数据量大时性能提升非常明显,页面交互改成“加载更多”而不是跳页。对CRM这种业务,用户真的需要看到很深的分页吗?我实测下来,几乎没人会翻到第100页之后。所以游标分页完全够用,还有利于前端做无限滚动。

4.3 连接池配置、事务控制与数据一致性问题

CRM业务里涉及多表联动的场景很多,比如新增跟进记录的同时要更新客户的下次跟进时间,这两条SQL必须在一个事务里,否则就会出现“记录了跟进但客户的待办时间没变”这种不一致问题。SpringBoot里最简单的方式就是给Service方法加@Transactional注解,默认遇到RuntimeException就回滚。我项目里统一定义了业务异常继承RuntimeException,这样事务在业务校验失败时也能正确回滚。

连接池这块我用的Druid,除了作为连接池,它的监控页面还能看到慢SQL统计,对排查性能问题极有帮助。在application.yml里配置好druid的stat filter和web-stat filter后,浏览器访问/druid/index.html就能看到每个SQL的执行时间。我给客户列表页做优化的时候,就是靠监控页发现慢SQL,才定位到索引缺失的问题。

事务隔离级别我用了默认的REPEATABLE_READ,MySQL InnoDB引擎下基本没问题。需要注意的一点是,长事务要避免,如果一个事务里做了大量查询或者分批插入,会持有持锁时间过长,并发一高就容易死锁。我的习惯是事务只包裹必要的写操作,查询逻辑放在事务方法外面完成。

5. 打包部署与生产环境问题排查

5.1 Maven多环境打包与可执行jar部署

开发完成之后就是构建部署。SpringBoot项目用Maven打包非常简单,核心命令是:

mvn clean package -Dmaven.test.skip=true

如果需要指定生产环境配置,使用profile参数:

mvn clean package -Dmaven.test.skip=true -Pprod

对应pom.xml里配置多套profile,每套profile指定不同的application-{env}.yml。我实际部署时没用Docker,直接在服务器上跑jar,启动命令是:

nohup java -jar crm-system.jar --spring.profiles.active=prod > /data/logs/crm.log 2>&1 &

这里有两个容易忽略的点:一是nohup一定要配合&使用,不然SSH断开后进程就没了;二是日志重定向最好落到独立文件,方便出问题时用grep定位异常。生产环境我建议在启动参数里增加-Xms和-Xmx,把堆内存上下限定成一样,避免JVM动态扩容引起性能波动,比如-Xms512m -Xmx512m,CRM这种中小系统给512M到1G完全够了。

5.2 从开发到上线的高频报错速查表

我整理了这份项目里遇到的高频问题列表,按现象、原因、解决办法三列对查:

报错现象根本原因解决办法
MySQL连接时报SSL connection error高版本驱动默认开启SSLURL后加useSSL=false
Public Key Retrieval is not allowedMySQL8新认证机制URL上加allowPublicKeyRetrieval=true
Unknown time zone / 时区错误未指定serverTimezoneURL上加serverTimezone=Asia/Shanghai
FreeMarker模板访问报错或404模板路径或文件后缀错误确认模板在templates目录且后缀是.ftl
页面CSS/JS全部加载失败拦截器拦截了静态资源拦截器排除/static/**路径
Layui表格数据正常但分页不工作PageHelper分页被其他查询污染startPage后紧跟目标查询语句
部署jar后中文乱码Linux默认编码不是UTF-8启动参数加-Dfile.encoding=UTF-8
Druid监控页面无法打开未放行druid的Servlet配置web-stat-filter并开启enabled
Maven依赖下载极慢默认中央仓库网络不稳定settings.xml配置阿里云镜像

表里这些坑有好几个都是我实际踩过的。最典型的是部署到Linux中文乱码,本地开发Windows没问题,一上服务器全变问号。排查下来发现是Linux系统locale没有设置UTF-8,Java读取文件默认用了系统编码,加上Dfile.encoding参数后解决。

5.3 生产环境数据初始化与权限配置

上线前有一件事一定要做:数据初始化。我先在测试环境把系统跑通,然后用mysqldump导出表结构和基础数据,再导入生产库。不要在生产库手工执行一遍建表脚本,漏一个索引后面补起来相当被动。

权限初始化这块,管理员账号、角色、部门数据我用一个init.sql脚本管理,脚本里先判断记录是否存在再插入,保证脚本可以重复执行。

生产环境的日志级别也要改,开发环境MyBatis打印了所有SQL,生产环境如果还开着会把磁盘写爆。我把log-impl配置成org.apache.ibatis.logging.nologging.NoLoggingImpl,SpringBoot的日志级别设为INFO,只保留业务关键日志。

6. 项目实操心得与后续扩展方向

6.1 复盘:这次CRM改造里最值得说的几个教训

整个项目做完,我复盘了一遍,有几个点值得单独提醒。

第一个是关于Session存储。一开始我把用户对象全部放进Session,里面有位图、头像字段等冗长信息,用户在个人中心上传新头像后,Session里的旧数据还留着,展示还是旧头像。后来把Session对象瘦身成ID和关键标识,需要完整信息时再查库,问题解决。

第二个是关于FreeMarker模板缓存。上线前我把cache改成了true,结果有一次改了页面上的一个错别字,重新部署后发现页面没变,排查才想起来是模板缓存的问题。生产环境如果有模板变更,要么重启服务,要么用脚本清空freemarker缓存目录,这个坑影响不大但很恼人。

第三个是SQL注入。我在写客户搜索功能时,曾经图省事直接用字符串拼接条件,结果被测试同事传入特殊字符搞崩了查询。后来全部改成MyBatis的#{}预编译参数,这个教训我在代码里贯彻得很彻底,所有动态条件都走XML的if标签配合#{},绝不拼字符串。

第四个是ECharts图表的旧数据覆盖。前面已经提过,这个问题如果不在setOption时处理,报表数据一刷新就显示混乱。我后来做了一个公共封装,每次绘制图表前都强制清空旧实例。

6.2 CRM系统可以持续演进的几个方向

核心功能落地后,我整理了目前没做但值得扩展的方向,给有同样需求的朋友一个参考:

  • 导出Excel:销售经常要把客户名单导出给领导看,我用Apache POI解决了这个问题,模板导出客户列表、商机报表,按照标题里提到的“java poi word能生成图表吗”这个思路扩展,数据还可以导出为带图表的Word文档。
  • 自动提醒:每个销售登录后能看到今天需要跟进的客户列表,靠cron定时任务扫描next_contact_time,把过期未跟进的客户推送到首页待办。
  • WebSocket消息通知:客户被分配、商机阶段变化时给相关销售实时推送消息,比刷新页面体验好很多。
  • 数据大屏:把ECharts的报表做成一整块大屏页面,投到销售办公室电视上,实时展示整体业绩和漏斗转化,这对管理层的直观冲击力很强。
  • 多租户隔离:如果以后要给不同分公司独立使用,可以在所有业务表加一个org_id字段实现数据隔离,而不用重新设计表结构。

我在实际使用这套系统的过程中,最深的感受是:技术的复杂度永远是第二位的,业务模型是不是贴合销售真实工作流,才是CRM能不能活下去的核心。SpringBoot、ECharts、MySQL这些都是随手就能上手的工具,难的是把“客户、商机、跟进”这条链路理解透,再把它们稳妥地落到代码和表结构里。希望这篇复盘能帮你在自己的项目里少走几条弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 18:40:55

ROS消息调试效率革命:从rostopic pub Tab补全到单元测试自动化

1. 这不是“命令补全”而是ROS开发者效率命脉的底层机制你有没有在终端里敲下rostopic pub /chatter std_msgs/String "data: hello"后&#xff0c;突然卡住——不确定消息类型字段名到底叫data还是msg&#xff1f;或者刚写完一个发布器节点&#xff0c;却要反复改参…

作者头像 李华
网站建设 2026/10/1 18:40:07

SpringBoot+Vue+MyBatis企业级智能物流管理系统架构与源码实战解析

企业级智能物流管理系统源码解析&#xff1a;SpringBootVueMyBatis架构从拆解到落地 做Java全栈这些年&#xff0c;接过的管理系统项目不少&#xff0c;但物流行业这套一直让我印象深刻。它不是那种简单的CRUD堆功能&#xff0c;而是真正把订单流转、仓储调度、运输跟踪、财务…

作者头像 李华
网站建设 2026/10/1 18:38:51

AI Agent静默截断:定位、防御与五层实战解决方案

1. 问题现场还原&#xff1a;当Agent“悄悄”丢掉30条数据时&#xff0c;你根本不会收到任何警告“源端有50条&#xff0c;模型只看到了20条”——这句话不是测试日志里的异常报错&#xff0c;也不是监控面板上的红色告警&#xff0c;而是一次深夜排查中&#xff0c;我在对比原…

作者头像 李华
网站建设 2026/10/1 18:38:14

微信聊天记录秒变个人知识库:解密导出与RAG应用全攻略

后台时不时有朋友跑来问我&#xff1a;“微信是不是真开源了个知识库项目&#xff1f;”一开始我也以为又是标题党&#xff0c;但顺着线索翻了一圈&#xff0c;发现大家说的其实是 GitHub 上那个热度很高的开源项目——能把电脑版微信里的聊天记录完整导出来&#xff0c;再批量…

作者头像 李华
网站建设 2026/10/1 18:37:31

Mistral本地部署实战:从模型选型、量化到推理调优的完整指南

这个系列写到第六篇&#xff0c;前面的内容基本都围绕 Mistral 的 API 应用展开&#xff1a;怎么调接口、怎么写 Prompt、怎么做 RAG、怎么接 Agent。这些内容适合快速验证想法&#xff0c;但真到产品化阶段&#xff0c;很多人会遇到同一个坎——API 调用费用、数据隐私、延迟控…

作者头像 李华
网站建设 2026/10/1 18:36:47

RBF神经网络自适应滑模控制:从原理到Matlab实现

做控制这些年&#xff0c;我最头疼的永远是“模型不准”这四个字。理论上只要给我一个精确的被控对象模型&#xff0c;往前一步是PID&#xff0c;往后一步是最优控制&#xff0c;都能画出漂亮的控制曲线。可一旦落地到真实系统&#xff0c;摩擦力、负载变化、未建模动态全冒出来…

作者头像 李华