首页 / 资讯中心 / 文章详情

Spring Boot CRM系统实战:从数据模型到智能提醒的设计与落地

Spring Boot CRM系统实战:从数据模型到智能提醒的设计与落地 ★ FEATURED ARTICLE
1. 这个CRM项目是从一个很现实的问题开始的之前在某家做企业服务的公司带后端团队的时候接到一个特别典型的诉求销售团队手里有几千个客户线索但管理方式全靠Excel和微信聊天记录。销售离职的时候客户资料跟着聊天记录一起消失接手的同事只能重新培养信任。管理层想看销售漏斗数据得等销售手动填周报数据还经常对不上。当时市面上现成的CRM系统不是没有但要么是重型的国际产品要按坐席买license定制化成本吓人要么是轻量级的SaaS工具数据不在自己手里也没法和内部的合同系统、工单系统打通。讨论了几轮之后决定自己搭一套面向中小团队客户管理场景的Spring Boot系统。这个项目做完之后技术上的复杂度其实不算高但踩过的坑让我印象很深——尤其是数据权限、状态流转、还有智能部分应该做到什么程度才算有用而不是噱头。这篇文章把我自己的设计思路和落地过程完整拆开讲一遍包括模块划分、核心表结构、Spring Boot具体实现要点、以及哪些智能功能是真正值得做的。希望对准备做类似系统的同学有点参考价值。2. 先想清楚这个系统到底要解决谁的问题我见过不少人做内部管理系统第一步就扑到数据库建模上结果做到一半发现业务方想要的根本不是这个。所以这个项目里我做的第一件事不是写代码而是把核心用户分成了三类分别梳理他们的痛点。2.1 三类核心用户和他们的真实诉求第一类是一线销售。他们最需要的是把客户资料集中管理、随时记录跟进情况、设置提醒不遗漏跟进节点。他们要的是快半天时间必须能上手录入成本必须低。第二类是销售主管。他们需要看团队的客户储备情况、跟进活跃度、商机推进到哪一步了。他们要的是清晰能够快速看出团队的整体状态。第三类是公司管理层。他们关心的是预测按照当前的商机储备这个季度大概能签多少单。他们要的是预测能力即便是粗糙的预测也比拍脑袋强。这三种诉求其实指向了三个能力方向客户数据管理能力、流程可视化能力、基础的数据分析预测能力。这决定了后续的功能设计边界——不是一个财务级的精密系统而是一个让销售动作有迹可循、让管理者有数据可依的运营型工具。2.2 技术选型背后的几个硬性约束业务方给的工期是两个月运维团队只有一个人而且不想引入太重的基础设施。这基本注定了选型的基调Spring Boot 2.x团队Java基础扎实招聘也容易生态成熟社区资料极多出问题搜得到答案。MySQL 8.x业务量级在十万级客户以内单库单表完全扛得住不需要一上来就上分布式中间件。Redis用于缓存热点数据分布式锁以及后续做消息通知的队列缓冲。这个组件运维成本低值得引入。MyBatis-Plus团队熟悉MyBatisPlus提供了分页插件、逻辑删除、自动填充这些很实用的能力能省下不少样板代码。不引入工作流引擎很多CRM喜欢接一套流程引擎比如Flowable但真正落地起来复杂度极高。我调研之后决定用状态机加枚举的方式处理商机状态流转后面会详细说为什么。这里要特别强调一点选型不是选最流行的而是选团队能长期维护的。某公司当时正在推广微服务架构有同事建议把CRM直接拆成客户服务、跟进服务、商机服务、报表服务四个微服务。我坚决反对原因很简单团队加一起就八个人还有别的项目要维护微服务带来的部署成本、链路排查成本、分布式事务成本远超这个体量系统能带来的收益。最终选择了模块化单体Modular Monolith后期如果某个模块压力确实大再单独拆分出来。这是一个很重要的决策帮助项目在两个月内准时交付。3. 数据模型设计一张表怎么拆直接决定后面好不好改客户关系管理系统的核心数据模型绕不开这几个实体线索Lead、客户Customer、联系人Contact、商机Opportunity、跟进记录Activity。其中最容易设计错的是把客户和联系人混在一张表里。3.1 客户与联系人为什么必须分开很多销售会说客户就是那个联系人刚起步的小公司确实可以这么理解。但只要业务稍微壮大一点就会发现一个客户公司可能有多个联系人一个负责采购的、一个负责技术的、一个负责财务审批的。如果把联系人字段放在客户表里要么就得加冗余字段联系人A、联系人B、联系人C要么就完全没法记录多联系人场景。我的设计是客户表存企业维度的信息公司名称、行业、规模、来源渠道、所属销售联系人表存个人维度的信息姓名、职位、电话、微信、邮箱联系人表通过customer_id关联客户表。这样客户详情页可以展示一个联系人列表商机表再通过关联字段指向某个具体联系人操作链路就通顺了。核心表结构大致是这样只列关键字段省略create_time等通用列CREATE TABLE customer ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(128) NOT NULL COMMENT 公司名称, industry varchar(32) DEFAULT NULL COMMENT 行业, scale varchar(32) DEFAULT NULL COMMENT 规模, source varchar(32) DEFAULT NULL COMMENT 客户来源渠道, level char(1) DEFAULT C COMMENT 客户级别 A/B/C/D, owner_id bigint(20) DEFAULT NULL COMMENT 归属销售ID, status tinyint(4) DEFAULT 1 COMMENT 1正常 2已流失 3已成交, deleted tinyint(4) DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), KEY idx_owner_id (owner_id), KEY idx_level_status (level, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE contact ( id bigint(20) NOT NULL AUTO_INCREMENT, customer_id bigint(20) NOT NULL, name varchar(64) NOT NULL, position varchar(64) DEFAULT NULL, mobile varchar(20) DEFAULT NULL, wechat varchar(64) DEFAULT NULL, is_primary tinyint(1) DEFAULT 0 COMMENT 是否主要联系人, PRIMARY KEY (id), KEY idx_customer_id (customer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 数据权限同一张表不同角色能看到不同范围这是整个项目里最容易踩坑的地方。CRM系统的数据天然有隐私属性销售不应该看到其他销售的客户主管可以看到自己团队的客户老板可以看到全公司所有客户。很多团队是在每个查询SQL后面手动拼接where条件比如owner_id 当前用户id。这样做短期内没问题但只要查询条件变多一点就会被遗漏产生数据越权的bug。我的方案是基于MyBatis-Plus的拦截器实现SQL级的数据权限过滤。思路是这样的自定义一个DataScope注解标注在需要做数据权限控制的Mapper方法上。通过拦截器在SQL执行之前解析当前登录用户的角色和部门。根据角色动态改写SQL往where子句中拼入对应的数据范围条件。Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class DataScopeInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler (StatementHandler) invocation.getTarget(); MetaObject metaObject SystemMetaObject.forObject(statementHandler); MappedStatement mappedStatement (MappedStatement) metaObject.getValue(delegate.mappedStatement); String originalSql statementHandler.getBoundSql().getSql(); // 根据当前用户角色判断是否需要追加数据权限条件 if (需要数据权限控制(mappedStatement)) { String scopeSql buildDataScopeSql(); // 构造数据范围子句 String newSql injectWhereClause(originalSql, scopeSql); metaObject.setValue(delegate.boundSql.sql, newSql); } return invocation.proceed(); } }构建数据范围子句的时候要注意管理员和普通销售的处理逻辑不同。管理员不需要任何过滤部门主管需要限制在部门ID 直属下级普通销售只需要owner_id 当前用户。这个逻辑不复杂但必须统一放在拦截器里做而不是散落在各个Service方法中。3.3 状态流转用状态机而不是if-else堆逻辑线索到客户到商机再到成交这个流程涉及非常多的状态新建线索 → 已联系 → 已转化客户 → 商机阶段初步沟通 / 方案确认 / 报价 / 商务谈判→ 赢单 / 输单。最直观的做法当然是if-else但状态多了以后会变成一团乱麻。我的做法是用一个轻量的状态机框架public enum CustomerStatus { LEAD(0, 线索), CONTACTED(1, 已联系), CONVERTED(2, 已转化客户), OPPORTUNITY(3, 商机阶段), WON(4, 赢单), LOST(5, 输单); private final int code; private final String desc; private final SetCustomerStatus allowedTransitions; // 允许流转的状态集合 static { LEAD.allowedTransitions EnumSet.of(CONTACTED, LOST); CONTACTED.allowedTransitions EnumSet.of(CONVERTED, LOST); // ... } public boolean canTransitionTo(CustomerStatus target) { return allowedTransitions.contains(target); } }这样有两个好处一是所有的状态流转规则都收敛在枚举定义里新增一种状态、调整一条流转路径只需要改一处代码二是可以在Service层统一做状态校验避免非法流转比如未联系就直接变成商机。踩过的坑一开始我把状态流转写成了Service层的一个独立方法里面用switch做了很多判断。结果第三周需求变了——销售总监要求线索可以直接转为赢单但要标记为异常赢单。改起来异常痛苦后来重构成了上面的状态机枚举结构后续加需求就顺畅多了。所以我的经验是消费方流程的状态数量一旦超过五个就值得上状态机机制了。4. 功能落地技术不炫技但每一步都要能扛住生产环境4.1 登录认证与鉴权的完整链路认证方面直接用市面上验证过非常多的方案Spring Security JWT。没有引入OAuth2这种重方案因为系统是纯内部使用没有第三方授权登录的需求。关键点有几个JWT续期access token有效期设短2小时refresh token设长7天。前端拿到401之后用refresh token自动换取新token用户无感登录。密码加密BCrypt加密存储不允许明文密码入库。登录锁定同一账号连续输错5次密码锁定15分钟。用Redis记录失败次数设过期时间即可实现。登录后的用户上下文解析token后把userId、roleId放入一个ThreadLocal容器自定义的LoginUserHolder在后续的Service层直接用即可。注意ThreadLocal的清理在拦截器afterCompletion里必须remove否则线程池复用会导致用户身份串号。public class LoginUserHolder { private static final ThreadLocalLoginUser LOCAL new ThreadLocal(); public static void set(LoginUser user) { LOCAL.set(user); } public static LoginUser get() { return LOCAL.get(); } public static void clear() { LOCAL.remove(); } }4.2 客户详情页性能问题解决打开慢的体验问题客户详情页是销售每天打开最频繁的页面它要展示客户基本信息、联系人列表、跟进记录、关联商机、待办提醒。如果每次请求都全量实时查库随着数据量增长页面响应速度会直线下降。我采用了Redis缓存客户聚合信息的方案key设计crm:customer:detail:{customerId}value直接存JSON字符串。缓存更新策略客户基本信息变更时同步删除对应缓存跟进记录新增时只更新缓存中的跟进记录子集而不是删除整个缓存。缓存穿透处理如果某个客户ID在DB中根本不存在也不能让请求每次都穿透到DB缓存一个空值对象设置短过期时间5分钟。实测下来详情页接口P95响应时间从450ms下降到了90ms左右体感提升非常明显。这里有个细节要提醒Redis缓存的一致性不用追求绝对实时内部系统允许秒级延迟。所以我的做法是数据变更后直接删除缓存下一次请求再回源DB并重建缓存简单可靠。引入Canal监听Binlog、或者搞消息队列异步更新缓存这些方案在这个业务体量下都属于过度设计。4.3 跟进记录的分页查询优化避免深分页客户详情页里的跟进记录是一个典型的深分页场景。销售持续跟进一年一条客户的跟进记录可能有数百条如果用户一直往下翻就可能访问到很深的页码。MySQL深分页的问题大家都知道LIMIT 10000, 20需要先扫描前10000行成本很高。我的优化方案是使用游标分页Keyset Pagination即把LIMIT offset, size改成WHERE id ? ORDER BY id DESC LIMIT size利用主键索引直接跳过前面的数据。接口传参改成前端传上一页最后一条记录的id而不是页码。这样百页以内的列表查询性能基本稳定不会随着翻页深度变慢。类似的优化思路也被用在操作日志查询、商机明细查询上。这个改造动手成本很低但收益特别稳定。4.4 销售漏斗报表的实现思路销售主管和管理层核心看的漏斗报表数据来源是商机表的阶段分布。用MySQL直接做统计SELECT stage, COUNT(*) AS cnt, SUM(amount) AS total_amount FROM opportunity WHERE owner_id IN (SELECT id FROM user WHERE dept_id ?) AND status 1 GROUP BY stage这个SQL本身很简单但有两个问题一是实时统计在大数据量下会有性能压力二是漏斗报表不需要秒级实时只需要每天/每小时级别的新鲜度。所以我做了一个报表预聚合表定时任务每30分钟跑一次把各维度关键指标算好存起来。前端查询直接读聚合表。聚合表的数据量很小查询永远是毫秒级。这个模式虽然简单但对这个业务体量来说已经相当够用。5. 智能部分到底应该做成什么样我的取舍智能管理系统里的智能这两个字其实是最大的坑。如果做一堆看起来很炫但业务方根本用不起来的AI功能最后只会被丢在角落。我的思路是每个智能功能都必须能直接辅助一个具体的业务动作。5.1 客户价值分层不是拍脑袋而是可解释的评分算法销售手上客户很多每天时间有限先跟谁聊、后跟谁谈决定了产出效率。我实现了一套轻量的客户评分模型总分100分由几个维度加权计算维度权重评分规则客户规模25分上市公司25分 / 中小企业15分 / 小微8分需求匹配度30分有明确项目需求30分 / 有潜在需求15分 / 无明确需求5分接触活跃度20分近7天有跟进20分 / 近30天有跟进10分 / 超过30天无跟进0分决策链完整度15分已接触关键决策人15分 / 仅接触执行层8分 / 无联系人0分来源渠道权重10分官网主动咨询10分 / 展会名片6分 / 购买名单3分这套规则看着简单但它好在所有字段都是系统里现有的结构化数据不需要额外的人工录入评分逻辑可以根据权重清晰解释——销售问为什么这个客户是A级可以直接告诉他是需求匹配度和决策链完整性加分了而不是一个黑盒。业务流程中高分客户会自动进入销售主管的重点客户关注列表并且系统会给出下一个跟进时间建议。5.2 成交概率预测规则评分卡比机器学习模型更实用我在做这个项目之前也和不少人交流过发现一提到预测大家的第一反应就是上机器学习模型比如用逻辑回归、XGBoost预测商机成交率。说实话在这个数据量级下几千个商机模型的效果并不比规则评分卡好多少但是维护成本却高了一个量级——数据要打标、特征要清洗、模型要定期重训。我最终选择了基于规则的评分卡Scorecard方案商机金额大小、当前阶段、客户属于哪个等级、最近跟进频率、决策链条完整度五个因素每一项得分相加得到0-100的成交指数。对应到三个挡位成交指数≥75高概率赢单系统自动提醒主管重点介入。成交指数40-74正常推进保持跟进频率。成交指数40低概率赢单建议降低投入或作为风险预警信息展示。这个方案的好处是结果可解释、逻辑可调整。业务方如果觉得某个因素占比不合理直接在配置中心调整权重即可不需要研发重新训练模型。5.3 待办智能提醒用定时任务解决销售忘了联系客户的痛点销售最常出现的执行问题是一个高意向客户因为最近在忙另一单三周没联系结果客户被竞品签走了。智能提醒功能需要解决的就是这类问题。我的实现方式就是一个定时任务每天上午9点扫描所有客户客户的最近一次跟进时间距今超过3天生成跟进提醒。客户是A级高价值客户但超过7天未跟进追加一条重点客户超期提醒。商机状态停留在商务谈判超过7天给销售主管推送一条商机停滞预警。提醒的触达渠道包括系统内部的消息中心、企业微信通知、以及邮件。这套机制不复杂但真正业务价值很高——销售觉得系统在帮他做事而不是在给他添乱。5.4 关于大模型能力的边界思考2024年以来智能管理绕不开大模型这个方向。我实际调研过几个现成方案评估结论是在客户关系管理这个场景里目前最适合落地的是辅助生成型能力而不是自动决策型能力。我当时做过的两个试验性功能客户资料摘要生成把客户的基本信息、历史跟进记录、关联商机动态汇总后调用大模型生成一段200字的客户全景摘要。销售打开客户详情页时原本要读3分钟的内容现在扫一眼摘要就够了。这个功能确实有效。跟进行动建议根据客户当前所处的阶段给出下一步建议比如该客户处于方案确认阶段建议整理一份行业案例发送给对方。这个生成结果可以作为参考但要人工确认后再跟进。没有做的事情是让AI自动代写发给客户的邮件、自动判断客户意向等级。原因很简单AI自动代写的内容一旦有事实性错误比如把客户名称写错、报价金额搞混销售直接点发送就会造成客户投诉自动判断客户意向等级则涉及业务判断的责任归属问题。在内部工具上责任边界要非常清楚。辅助生成、人工确认这是在当前阶段比较稳妥的实践边界。6. 项目上线后部署、性能、稳定性系统上线前后的几个环节也值得展开说说。6.1 Docker部署与资源规划这套系统最终是单机Docker Compose部署的组件包括一个Spring Boot应用容器、一个MySQL容器、一个Redis容器、一个Nginx容器。初期并发量小内部不过百人同时在线单机部署完全可以胜任。Docker Compose的关键配置version: 3 services: mysql: image: mysql:8.0 container_name: crm-mysql environment: MYSQL_ROOT_PASSWORD: xxxxxx MYSQL_DATABASE: crm_db volumes: - ./mysql-data:/var/lib/mysql ports: - 3306:3306 command: --character-set-serverutf8mb4 --collation-serverutf8mb4_general_ci redis: image: redis:7.0 container_name: crm-redis ports: - 6379:6379 volumes: - ./redis-data:/data app: build: . container_name: crm-app depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql REDIS_HOST: redis ports: - 8080:8080 volumes: - ./uploads:/app/uploads部署中实际遇到的坑是应用启动时MySQL尚未就绪导致连接失败。解决方式有两个最简单的是在Spring Boot的启动逻辑里加一个depends_on的healthcheck条件更稳妥的是利用dockerize工具等待依赖服务端口通了再启动应用。这里要注意单纯的depends_on并不保证MySQL已经完成初始化需要加条件判断。6.2 慢SQL的排查与索引优化上线第一周就收到反馈客户列表页的搜索接口变慢了。查了慢查询日志发现是一个动态条件查询的SQL没有命中索引SELECT * FROM customer WHERE name LIKE CONCAT(%, xx公司, %) AND industry IT AND status 1 ORDER BY update_time DESC LIMIT 0, 20百分号前置的字段无法使用普通BTree索引导致全表扫描。我的处理策略是行业和状态字段的组合索引(industry, status, update_time)先覆盖等值查询的过滤场景。客户名称搜索场景改为全文索引ngram parser支持中文分词搜索避免全表扫描。接口层的处理对可选搜索条件做限制比如强制要求至少选择一个非「名称模糊搜索」的条件否则查询退化为普通分页逻辑。ALTER TABLE customer ADD FULLTEXT INDEX ft_search (name) WITH PARSER ngram;查询改写为SELECT * FROM customer WHERE MATCH(name) AGAINST(xx公司 IN BOOLEAN MODE) AND industry IT AND status 1 ORDER BY update_time DESC LIMIT 0, 20;上线后这条接口的耗时从1.8秒降到了80毫秒左右。6.3 日志与请求追踪没有链路追踪时怎么快速定位线上问题系统没上微服务所以没必要引入SkyWalking这类重量级APM组件。但线上排查问题的能力必须要有。我做了一个轻量级的方案网关过滤器为每个请求生成一个traceId传递到MVC层、Service层、DAO层。Logback的Pattern中输出[%X{traceId}]。通过公司现有的ELK日志平台索引traceId即可将一次请求的所有日志从入口到SQL输出串联起来。pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n/pattern这样定位问题时直接按traceId搜日志配合MySQL的慢查询日志、Redis的慢命令日志线上问题大多都能在10分钟内定位到根因。我记得有一次线上用户反映某个客户的详情页打不开排查时就用traceId把前端的出错请求串到后端日志发现了是第三方企业信息接口超时导致的异常未捕获。如果没有traceId这个问题的定位可能要花上一小时。7. 上线后维护阶段的一些细节体会7.1 定时任务的确保只有一台执行在单机部署阶段Scheduled开几个定时任务没什么问题。如果后面扩展到多节点部署就会遇到同一时刻多个节点同时执行任务的情况造成重复提醒、重复计算。我在部署时提前用Redis分布式锁做了保护Component public class ScheduleTaskGuard { Autowired private StringRedisTemplate redisTemplate; public boolean tryLock(String taskKey, long expireSeconds) { Boolean success redisTemplate.opsForValue() .setIfAbsent(crm:task:lock: taskKey, locked, Duration.ofSeconds(expireSeconds)); return Boolean.TRUE.equals(success); } }每个定时任务执行前先尝试获取锁拿不到锁的直接跳过。这个保护机制现在看着是提前量但真的到部署两节点的时候就省事多了。7.2 操作审计内部系统最容易忽略却很重要的功能CRM里的数据涉及商业机密销售误删客户资料、恶意修改商机金额这类事情虽然少但一旦发生没有追溯能力问题就大了。我的设计是基于Spring AOP统一记录操作日志自定义OpLog注解标注在Controller或Service方法上。AOP切面在方法执行成功后记录操作人、操作类型、目标对象ID、操作内容摘要、请求IP、耗时。操作日志单独存一张表不随业务数据一起逻辑删除。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OpLog { String module() default ; String action() default ; }后续如果有人问这个商机的金额是谁在什么时候改的查一下操作日志表就能完整还原当时的操作链路。这个功能平时没有存在感但真正的故障或争议场景里它价值极高。7.3 数据备份策略MySQL容器化部署之后备份的注意点很多。我用了mysqldump每天凌晨全量备份到挂载的数据盘保留最近7天的备份文件。Redis是纯缓存数据不需要备份重启后自动回源即可。备份命令写成了脚本放在宿主机crontab里0 2 * * * /usr/bin/docker exec crm-mysql mysqldump --single-transaction -uroot -p****** crm_db /data/backup/crm_$(date \%Y\%m\%d).sql这个方案看着很原始但胜在简单可靠。对内部系统来说能恢复到前一天的数据已经满足绝大多数需求了。8. 如果这个项目重新做一遍我会改哪些事这个系统做了大约两个月上线后持续迭代了半年左右。如果现在再回到起点重新做我会在这些方面做不同的决策一是领域模型的设计会更细一些。当时数据库表设计虽然能满足功能需求但字段类型和命名上有不少够用就好的痕迹。比如客户级别用了单个char类型A/B/C/D后来升级到更细分的分层模型时花了很大力气兼容已有数据。如果一开始就设计成独立的字典表扩展成本会低很多。二是接口层会从一开始就统一响应结构。早期接口的返回格式存在不统一的问题前端对接得很痛苦。后来才规范了统一响应体ResultTcode/message/data三个字段错误码也在枚举里定义清楚。后端改起来容易但对前端来说这是非常影响开发体验的事。这个统一应该在项目第一天就强制执行。三是会把文档和字段说明做得更完善。内部系统最大的坑是人员流动后接手的开发对着几百张表根本无从下手。我会在建表的时候就同步维护好数据字典文档注释写清楚每个字段的业务含义。这项工作不紧急但长期来看价值比写业务代码还要大。四是会更早考虑多租户的问题。当时是单租户内部系统后续如果想给其他公司提供SaaS服务数据隔离方案就要返工。如果你从第一天就做产品化的系统多租户的租户ID字段和数据权限设计必须提前规划不然后续改造的成本非常大。五是会更早引入统一异常处理机制。项目初期异常处理比较随意有的地方直接抛出了原始异常信息给前端有的地方又吞掉了异常。后来用了RestControllerAdvice做统一异常捕获配合自定义业务异常BusinessException这才规范化。这同样应该是项目开始时就定下的规范而不是等用户抱怨报错看不懂之后再补。9. 一些我踩过的坑提前帮你避掉9.1 时区问题本地好好的生产环境差了8小时这是Spring Boot项目几乎必踩的坑。本地开发时MySQL和JVM默认用的是本地时区一切正常。但生产服务器时区可能不同插入的时间字段会整体偏移8小时。我的配置是JDBC URL统一加上serverTimezoneAsia/Shanghai和useLegacyDatetimeCodefalse应用启动参数JVM设置-Duser.timezoneAsia/ShanghaiMySQL连接初始化时执行SET time_zone 08:00。这三处一起设了才能保证环境差异不影响时间。9.2 批量导入的Excel解析内存溢出销售团队的老客户数据都存在Excel里上线初期需要一个数据导入功能。如果直接读取整个Excel到内存再解析成对象列表十万行数据就可能导致OOM。我后来改成基于事件模式逐行读取使用EasyExcel的AnalysisEventListener每读1000行就批量入库一次然后主动GC区回收列表。这样内存峰值稳定控制在几百兆以内。9.3 消息推送的重复打扰做待办提醒的时候刚开始设置了一条规则只要客户超过3天没跟进每天早上推一条提醒。结果半个月后销售集体抗议——一个常年不怎么联系的C级客户连续被提醒了30天消息列表全是噪音。后来加了提醒次数的上限和降噪策略同一个客户每周最多提醒一次。客户等级为C/D的客户降低提醒频率甚至不提醒。已连续提醒3次仍无跟进的客户升级为任务给主管由主管决定是否分配他人跟进。这个调整上线后销售对提醒的认可度一下子提升了不少。智能提醒不是提醒得越多越好而是要在合适的时间提醒真正值得关注的事。9.4 部署升级期间的数据一致性某次发版更新了数据库表结构加了两个字段。当时直接在生产库执行了ALTER TABLE同时重启应用。结果由于新老代码并存老代码写入的数据缺了字段默认值导致部分数据不符合校验规则。后来改成严格的发版流程先备份数据库。执行增量DDL脚本保证字段有合理的默认值。发布新代码。灰度验证后再全量切流。这个流程看着简单但前提是数据库变更脚本必须提前准备并评审。在团队协作中这一点尤为重要任何alter操作都要有回滚计划。9.5 Redis连接池耗尽导致的连锁故障一次性能测试中发现高并发场景下Redis连接池被占满大量请求阻塞等待获取连接进而拖垮了整个接口层。检查后发现原因有两个一是代码里有个查询方法漏了释放连接使用的是默认的连接池配置连接池太小二是没有设置合理的超时时间。针对Redis连接池配置spring: redis: lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 max-wait: 3s timeout: 3s同时排查并修复了连接未释放的问题。从那以后每次上线前我都会检查一次连接池配置和资源释放逻辑这个好习惯帮我避免了不少线上事故。10. 从技术架构到业务价值的回顾这个Spring Boot客户关系智能管理系统做完之后同样业务链条里的合同管理模块、售后工单模块后来也都在这套架构上扩展出来了。统一的技术底座、统一的数据权限方案、统一的审计日志机制让后来的模块开发变得很快。回到最初的目的系统上线三个月后销售团队的客户信息完整度从50%提升到了95%以上主管不再需要手动汇总周报漏斗报表页面直接看管理层第一次有了相对可信的下季度业绩预测依据。这些成果完全不是靠什么高端算法达成的而是把业务动作结构化、把数据沉淀下来在正确的时间给出正确的提醒这套务实、可解释、好维护的路子在当前大多数业务场景下比堆砌花哨的技术概念要有用得多。如果这篇文章能给准备做类似系统的人一点参考我的建议浓缩成一句话先花足够的时间理解业务再用最稳妥的技术把它落地把智能用在真正能帮到用户的地方剩下的交给时间去打磨。后续如果大家遇到具体的技术细节问题欢迎在评论区交流我会基于实际项目的经验尽量解答。
阅读完成 · 觉得有帮助?
咨询建站