ARTICLE DETAIL

资讯详情

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

Spring Boot客户管理平台实战:从数据库设计到部署全流程

Spring Boot客户管理平台实战:从数据库设计到部署全流程 带论文的毕业设计/课程设计项目里客户管理平台算是出现频率最高的一类题。但说实话真正能把 Spring Boot 客户管理平台从数据库设计讲清楚、从前端到后端联调跑通、再部署到服务器上的完整资料并不多。最近正好完整做完了一个这样的系统从需求梳理到上线部署踩了不少坑把整个设计和实现过程整理成这篇实战记录希望能给正在做类似项目的同学或者刚接触 Spring Boot 的开发者一些能直接抄作业的参考。这个项目本身是一个典型的单系统客户管理平台包含客户信息管理、客户分类与标签、跟进记录、统计分析、系统用户管理等核心模块采用 Spring Boot MyBatis MySQL 这套经典组合。文章会从技术选型讲起依次拆解数据库表结构设计、后端接口实现、界面设计、调试部署最后把实际开发中遇到的问题和排查思路一并列出。无论你是要用它做毕业设计还是只是想学习一个完整的 Spring Boot 项目如何从零落地这篇文章应该都能帮到你。1. 为什么选 Spring Boot 做客户管理平台——选型与整体设计1.1 先搞清楚这个系统到底要管什么客户管理平台CRM 的简化版核心就三件事管客户资料、管跟进过程、管结果分析。听起来简单但实际设计时很容易陷入两个极端要么把表设计得特别复杂动辄十几张表开发周期被无限拉长要么只做一张客户表加一个增删改查交上去之后被导师或面试官一问就露怯。我最终确定的功能范围是这样的客户信息管理维护客户的基本资料公司名称、联系人、电话、邮箱、所属行业、客户等级、来源渠道支持新增、修改、删除、查看详情。客户分类与标签用客户等级和客户状态做分类比如潜客、意向客户、成交客户、流失客户再用标签表做更细粒度的标记。跟进记录每次电话沟通、拜访、邮件往来的内容都记录在案形成客户的完整跟进时间线。数据看板统计不同状态客户的数量、本月新增客户数、跟进次数排行等用图表展示。系统管理用户登录、修改密码、用户列表管理。这个范围的好处在于功能覆盖了管数据—管流程—管统计三个层次既不会单薄也没有超出单人开发的能力边界。毕业设计答辩时有东西可讲企业级项目里也有参考价值。1.2 技术栈组合Spring Boot MyBatis MySQL 是怎么定下来的技术选型不需要追新关键是稳和熟。这个项目用的组合是后端框架Spring Boot 2.7.x。为什么不追 3.x因为很多第三方依赖和资料还停留在 2.x 时代对新手来说 2.7 生态最成熟遇到问题搜得到答案。Spring Boot 的核心价值在于自动配置和内置 Tomcat不用再像 SSM 时代那样写一堆 XML 配置启动一个 Web 项目的成本被压得非常低。ORM 层MyBatis。相比 JPA/HibernateMyBatis 的 SQL 是自己控制的对学生项目来说更容易展示我确实会写 SQL。配合 MyBatis-Plus 使用的话单表 CRUD 基本不用写 SQL效率能提升一截。数据库MySQL 8.0。主流、免费、资料多用 Navicat 或者 MySQL Workbench 管理都行。前端这里做了一个比较务实的选择采用 Thymeleaf 模板引擎 Bootstrap 的轻量方案。有人会觉得 Vue 前后端分离更时髦但单系统单体应用场景下模板渲染能减少大量联调成本一个文件同时搞定页面和部分逻辑对学生项目来说是最稳妥的。如果确实想用 Vue后面我会单独说怎么改造。认证方案登录用 Session 拦截器做也可以换成 JWT。这个项目用了 JWT因为接口将来如果扩展成前后端分离认证逻辑不用重写。这个组合的好处是每一层都有明确的替代方案出了问题也好定位。SQL 报错查 MyBatis 映射文件页面渲染问题查 Thymeleaf 模板启动问题查依赖冲突边界非常清晰。2. 数据库设计客户核心模型怎么建才合理2.1 客户主体表别把字段一股脑塞进去客户表是整个系统的主体设计的时候最需要注意的就是字段职责分离。很多初学者会把客户等级客户来源跟进状态这些枚举值直接设计成字符串字段然后在前端写死几个选项。这样做的后果是后面前端选择项要改时必须改 JS要统计某个等级有多少人时SQL 里全是散落的字符串常量。我的做法是CREATE TABLE customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, customer_name VARCHAR(100) NOT NULL COMMENT 客户名称/公司名, contact_person VARCHAR(50) COMMENT 联系人, contact_phone VARCHAR(20) COMMENT 联系电话, contact_email VARCHAR(100) COMMENT 邮箱, industry VARCHAR(50) COMMENT 所属行业, source_channel VARCHAR(50) COMMENT 来源渠道自拓/转介绍/线上, customer_level TINYINT DEFAULT 1 COMMENT 客户等级1潜客 2意向 3成交 4流失, customer_status TINYINT DEFAULT 1 COMMENT 客户状态1正常 2暂停 3已删除, remark VARCHAR(500) COMMENT 备注, created_by BIGINT COMMENT 创建人ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户信息表;几个关键设计点客户等级和状态用 TINYINT 整数存储含义在代码常量类里统一管理不散落在 SQL 和前端模板里。用逻辑删除customer_status3而不是物理 DELETE。客户数据是业务资产误删了没法恢复逻辑删除是最保险的做法。create_time 和 update_time 用数据库默认值维护插入和更新时不用手动 set 时间减少代码量也避免时间格式不一致。表名和字段名都用小写加下划线避免大小写问题在不同操作系统之间导致坑。2.2 跟进记录与标签一对多关系的经典处理跟进记录表单独建一张外键指向客户表CREATE TABLE follow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL COMMENT 客户ID, follow_type TINYINT DEFAULT 1 COMMENT 跟进方式1电话 2微信 3拜访 4邮件, follow_content TEXT COMMENT 跟进内容, next_plan VARCHAR(200) COMMENT 下次跟进计划, follow_person VARCHAR(50) COMMENT 跟进人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户跟进记录表;标签体系我用了一个比较灵活的方式不建客户-标签多对多关联表而是在客户表里加一个tags VARCHAR(255)字段用逗号分隔标签名。原因很简单这个项目的标签只是辅助筛选没有复杂的标签查询需求用关联表反而增加三张表的维护成本。如果将来要做标签云或者任意标签组合筛选再拆成tag表和customer_tag_rel表也不迟。2.3 用户表和登录状态设计用户表不搞花活CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50), role TINYINT DEFAULT 1 COMMENT 1管理员 2普通用户, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码必须加密存储这个没有商量余地。Spring Security 自带的 BCryptPasswordEncoder 拿来就用别自己写 MD5 加盐没必要也容易被质疑安全性。用户登录成功后我用 JWT 生成 token 存在前端 cookie 里拦截器里校验 token 是否有效、用户是否存在。这里有一个需要注意的细节JWT 的过期时间不要设太长我设的是 24 小时。太长了安全问题大太短了用户频繁被踢下线体验差。24 小时对内部管理平台来说是比较合适的平衡点。3. 后端核心模块接口设计与业务逻辑实现3.1 Service 层的套路模板方法 通用返回体后台接口开发最忌讳的就是 Controller 里堆业务逻辑。我习惯的做法是三层结构Controller 只做参数接收和结果返回Service 承担业务规则Mapper 负责 SQL。客户管理的 Service 里有一个非常典型的场景——新增客户时要做重复校验Service public class CustomerServiceImpl implements CustomerService { Autowired private CustomerMapper customerMapper; Override public Result? addCustomer(Customer customer) { // 参数校验客户名称必填 if (StringUtils.isBlank(customer.getCustomerName())) { return Result.error(客户名称不能为空); } // 重复校验同一公司名不能重复录入 LambdaQueryWrapperCustomer wrapper new LambdaQueryWrapper(); wrapper.eq(Customer::getCustomerName, customer.getCustomerName()) .ne(Customer::getCustomerStatus, 3); if (customerMapper.selectCount(wrapper) 0) { return Result.error(客户已存在请勿重复添加); } customer.setCreateTime(new Date()); customerMapper.insert(customer); return Result.success(添加成功); } }所有接口统一返回一个 Result 对象里面包含 code、message、data 三个字段。前端只需要判断 code 是否为 200 就能确定操作是否成功非常干净。这个习惯看起来不起眼但真的能避免大量前后端扯皮。3.2 分页查询条件筛选 分页参数的正确写法客户列表页是核心页面需要支持按客户名称模糊搜索、按等级筛选、按状态筛选还要分页。MyBatis-Plus 的分页插件是现成的只需要在配置类里注册一个分页拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后 Service 里就是构造条件并调用分页查询Override public PageCustomer pageCustomers(CustomerQuery query) { PageCustomer page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperCustomer wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(query.getCustomerName()), Customer::getCustomerName, query.getCustomerName()) .eq(query.getCustomerLevel() ! null, Customer::getCustomerLevel, query.getCustomerLevel()) .ne(Customer::getCustomerStatus, 3) .orderByDesc(Customer::getCreateTime); return customerMapper.selectPage(page, wrapper); }这里有个经验之谈条件构造时一定要判断参数是否为 null 或空再决定是否拼条件。否则用户不输入关键词时SQL 会变成WHERE customer_name LIKE %%看着没错实际上会扫全表数据量大时性能明显变差。3.3 登录认证JWT 拦截器的完整闭环登录接口拿到用户名和密码校验通过后生成 tokenOverride public Result? login(String username, String password) { SysUser user userMapper.selectOne( new LambdaQueryWrapperSysUser().eq(SysUser::getUsername, username)); if (user null || !bCryptPasswordEncoder.matches(password, user.getPassword())) { return Result.error(用户名或密码错误); } String token JwtUtil.generateToken(user.getId(), user.getUsername()); return Result.success(token); }拦截器里校验 token这里贴一个关键逻辑Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/login)) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token) || !JwtUtil.verify(token)) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } return true; }拦截器注册时要注意放行静态资源否则 CSS、JS、图片全被拦截页面打开就是裸的 HTML。这个坑我踩过当时排查了半天才发现是静态资源没放行。4. 界面设计与前后端交互4.1 布局后台管理系统的标准件客户管理平台的界面不需要花哨关键是信息和操作路径清晰。我用的 Thymeleaf 模板整体布局是左侧导航栏 顶部用户信息 右侧内容区的经典后台结构。左侧菜单固定右侧通过th:replace切换不同页面片段。Bootstrap 提供的栅格系统让表单和表格的排列非常省心。客户列表页的设计思路是顶部放筛选条件搜索框、等级下拉框、查询按钮中间是工具栏新增客户、批量删除按钮下方是数据表格最底部是分页条。这是管理后台最常见的搜索区 操作区 数据区 分页区结构用户上手几乎零成本。4.2 前端与后端的交互方式用 Thymeleaf 的时候页面交互有两种方式一种是整个页面提交表单刷新另一种是用 AJAX 调接口局部刷新。我强烈建议数据操作走 AJAX这样体验好也符合实际项目的习惯。比如删除客户function deleteCustomer(id) { if (!confirm(确定要删除该客户吗)) return; $.ajax({ url: /customer/delete/ id, type: POST, success: function (res) { if (res.code 200) { alert(删除成功); location.reload(); } else { alert(res.message); } } }); }这里有个小细节删除我用的是 POST 而不是 DELETE 方法。原因是很多浏览器对 DELETE 请求类型支持不友好而且拦截器和过滤器处理起来也有差异。POST 加语义化 URL/customer/delete/{id}在实际项目中完全够用还省了跨域预检的麻烦。4.3 数据交互里的 JSON 格式问题后端接口返回 Result 对象时默认通过 Jackson 序列化为 JSON。需要留意的是日期格式默认序列化出来的日期是yyyy-MM-dd HH:mm:ss还是时间戳取决于配置。我在 application.yml 里做了统一处理spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8不配置这个前端表格里显示的时间可能会少 8 个小时或者变成一串难看的数字。这种小配置项直接决定了系统留给人的第一印象是否专业属于那种小小的细节很大的作用的坑。5. 开发环境搭建与调试部署全流程5.1 本地开发环境从 JDK 到 IDEA 的一次性配置开发这个项目的环境清单如下JDK 1.8Spring Boot 2.7 完美兼容不需要上 17Maven 3.8IDEA 2022 或更新版本MySQL 8.0本地端口 3306Navicat 或 DBeaver 作为数据库客户端Postman 或 Apifox 调试接口先把项目导入 IDEA等 Maven 把依赖下载完。这一步最容易出问题的是 Maven 镜像源默认中央仓库在国内下载依赖慢到怀疑人生。在settings.xml里配置阿里云镜像mirror idaliyun/id mirrorOfcentral/mirrorOf nameAliyun Maven Mirror/name urlhttps://maven.aliyun.com/repository/central/url /mirror配置完记得重启 IDEA 并刷新 Maven不然不会生效。数据库方面在 Navicat 里新建一个customer_db数据库字符集选 utf8mb4然后把项目的sql目录下的初始化脚本导入。注意脚本里如果有外键约束导入顺序很重要先建主表再建子表否则会报无法创建表的错误。5.2 application.yml 的核心配置Spring Boot 项目里配置文件是第一个要核对的东西。我当时用的配置如下server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://localhost:3306/customer_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.example.crm.entity configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplconnection URL 里的serverTimezoneAsia/Shanghai是必须的否则 MySQL 8.0 和 JDBC 之间会因为时区差异直接报错。useSSLfalse是本地开发需要避免连接时检查证书生产环境再考虑是否开 SSL。log-impl配置成 StdOutImpl 可以把 SQL 打印到控制台调试时能直接看到 MyBatis 执行的 SQL 和参数这对排查问题非常有帮助。项目跑通了之后再把它关掉不然日志会刷得飞快。5.3 打包部署mvn package 与服务器运行打成 jar 包部署是最省事的方式。在 IDEA 右侧 Maven 面板里执行package然后会在target目录下生成一个可执行 jar。命令行里也能打mvn clean package -DskipTests然后直接上传到服务器Linux 环境运行nohup java -jar customer-platform-1.0.0.jar --server.port8080 app.log 21 nohup加保证进程在后台运行日志输出到app.log。访问http://服务器IP:8080就能看到登录页面。部署时最容易忽略的是数据库权限问题。如果 MySQL 装在服务器上注意要用userlocalhost还是user%授权很多同学本地能连部署到服务器就报Access denied for user十有八九是授权范围的问题。5.4 调试现场实录一个请求从发起到返回的全过程调试接口时我推荐一套完整的验证步骤先用浏览器或 Postman 直接请求接口确认 URL、请求方式、参数名没问题。看后端控制台有没有打印 SQL如果有检查 SQL 参数是否替换成功。如果 SQL 没问题但数据不对单独在 Navicat 里执行这条 SQL确认是 SQL 本身的问题还是数据问题。如果后端没报错但响应数据是 null检查实体类字段名和数据库列名是否一致。MyBatis-Plus 默认驼峰转下划线只要实体字段是customerName、数据库是customer_name就没事但如果你某个字段命名不规范就查不出来。这套流程能覆盖 90% 的接口问题。核心思想很简单把问题逐步缩小到某一层而不是对着整段代码瞎猜。6. 常见问题与排查技巧实录6.1 启动直接失败端口被占用或依赖冲突Spring Boot 项目最常见的启动失败原因就是端口被占用。启动时如果报Port 8080 was already in use用下面命令找到占用端口的进程然后结束掉# 查看8080端口被谁占用 lsof -i:8080 # 结束对应进程 kill -9 进程号如果是依赖冲突Maven 会报jar:jar或者ClassNotFoundException。优先看是否是 spring-boot-starter-web 和 spring-boot-starter-thymeleaf 等包版本冲突通常用 IDEA 的 Maven 窗口里的Show Dependencies依赖树功能排查最快。6.2 启动成功但是页面 404 或 500404 大部分是访问路径不对。检查项目里 Controller 的RequestMapping路径和 Thymeleaf 模板 return 的逻辑视图名是否对应模板文件要放在src/main/resources/templates目录下否则 Thymeleaf 找不到页面。500 多半是模板渲染报错。Thymeleaf 的报错信息经常很长但关键信息在Caused by那几行往下翻找到根因。常见错误有属性拼写错误、模板变量不存在、日期格式化出错。还有一种很隐蔽的情况模板里用了${customer.name}但实体类里没有 getName 方法Thymeleaf 不会直接告诉你而是报一个复杂的 EL 表达式错误。6.3 数据库中文乱码乱码问题说白了就是创建数据库的字符集、连接字符串的编码、页面传输的编码三者不一致。创建数据库时明确指定CREATE DATABASE customer_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;连接 URL 里带characterEncodingutf8。页面响应加上% page contentTypetext/html;charsetUTF-8 %或者设置response.setCharacterEncoding(UTF-8)。三层保持一致乱码基本绝迹。6.4 JWT 登录失效但前端没跳转这个问题的表现是token 过期后用户继续操作接口返回 401但前端页面没有任何反应。解决方案是在全局 AJAX 配置里统一处理 401$.ajaxSetup({ statusCode: { 401: function () { alert(登录已过期请重新登录); window.location.href /login; } } });这样任何接口返回 401前端都会自动跳回登录页体验比每个接口单独处理要好得多。6.5 数据统计页面数据为 0 的排查统计看板用了 GROUP BY 语句如果查询结果为空常见原因是数据确实没有、条件不对、或者时间范围筛选把数据过滤掉了。排查时先把统计 SQL 复制到 Navicat 手动执行看能不能查出数据。能查出数据但接口返回空多半是返回类型不匹配比如 SELECT 语句查出来的是 Long 但你用了 Integer 接收。别问我怎么知道的改个类型就好了。7. 我的几点体会和补充建议整个项目做完之后回过头看给我最深的感觉是这种客户管理平台型的系统技术上没有太高深的东西真正花时间的往往是那些看起来很简单的细节。比如分页参数的边界处理、用户输入合法性的校验、逻辑删除和物理删除的选择、前端提示文案是否友好。这些细节堆在一起决定了系统是能用还是好用。给正在做类似项目的人三个具体建议第一个先把数据库表和接口清单列出来再动手写代码。我当时是先画了表格把每个模块的接口、请求参数、返回结果都列清楚后面写代码基本是流水线操作没怎么返工。第二个保留完整的演示数据。系统答辩或者演示的时候数据库里要有十几条有代表性的客户数据、几条跟进记录不然页面上空荡荡的功能再完整也显得没做完。我自己还额外造了一条已成交客户 近 30 天数据的演示记录看板效果立刻不一样。第三个不要把时间浪费在校能炫技的技术上。客户管理平台这类系统用户关心的是能不能快速录入、能不能方便查询、数据统计准不准、界面顺不顺眼。把一个 CRUD 做得干净利落、后台页面看着舒服比堆一堆微服务框架更有意义。真遇到了解这些技术深度的问题可以放到论文的研究背景和展望里去提但不要把它作为系统的核心。最后分享一个小技巧把系统里的关键常量比如客户等级、跟进类型集中放在一个常量类里前端用 Thymeleaf 渲染时通过xxxConst方式直接访问常量写死在中英文之间的差异就彻底没了。这个小习惯让我在后期改需求时省了非常多力气推荐你也试试。如果你准备用这篇文章里的思路去开发自己的客户管理平台从选一个干净的 Spring Boot 骨架开始先把数据库跑通再逐步往里面加模块整个过程会顺畅很多。遇到具体报错也欢迎对照前面的排查思路一步步来大部分坑其实都没有想象中那么难踩。
返回列表