ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue.js CRM系统毕设实战:从数据库设计到部署

SpringBoot+Vue.js CRM系统毕设实战:从数据库设计到部署 搞 Java Web 毕设的同学如果还在纠结选题SpringBoot Vue.js 的客户关系管理系统CRM值得认真考虑。这个组合既能体现后端接口设计能力又能展示前端交互功底而且客户管理、商机跟进、数据统计这些功能点天然适合做业务系统答辩时素材也充足。下面以我实际带过的项目为例把一个完整 CRM 的源码结构、数据库设计、接口规范、前后端联调和部署避坑讲透。1. 项目整体拆解CRM 系统到底要做什么1.1 功能模块不是拍脑袋定的CRM 的本质是管理“客户生命周期”——从线索获取、跟进触达、商机推进到最终成交所有模块都围绕这条链路展开。毕设版本的 CRM 不需要做成 Salesforce 那样庞大但要有一条完整且自洽的业务闭环。我当时设计的模块划分是这样登录与权限用户登录、JWT 鉴权、基于角色的菜单权限控制。这是所有业务的入口也是答辩时最容易讲清楚的部分。客户管理客户信息的增删改查、分页搜索、批量导入导出。注意这里要区分“客户”和“联系人”——客户是公司维度联系人是具体的人一个客户可以挂多个联系人。商机管理一个客户可以关联多个商机商机有阶段初步接触、需求确认、方案报价、谈判、成交/丢失阶段变化记录在跟进日志里。跟进记录每次电话、拜访、邮件沟通都形成一条记录跟某个客户或商机关联。这个功能最能体现系统实用性也让数据统计有素材。数据看板按销售漏斗展示商机阶段分布按月份展示新增客户量和成交额趋势。通常用 ECharts 做后端提供聚合查询接口前端渲染图表。模块之间不需要做得太花哨但关联关系必须清晰。答辩时老师一定会问“客户删除后商机怎么办”回答“采用逻辑删除保留业务数据链”就比支支吾吾强很多。1.2 技术选型为什么是 SpringBoot Vue.js很多人纠结 SSM 还是 SpringBoot我的建议是直接 SpringBoot。不必担心“用 SpringBoot 是不是太简单”评委看的是工程规范和业务完整度。SpringBoot 简化了配置但 Controller、Service、Mapper 三层架构、事务控制、异常处理、拦截器这些核心能力一个都不少反而你有更多精力把接口设计、权限控制、数据库优化做扎实。前端选 Vue.js 同理。Vue 的组件化特性让“客户列表页”、“商机详情页”、“看板页”可以拆成独立模块数据通过 Axios 请求后端接口状态管理用 Pinia。相比 JSP JQuery 的传统写法Vue 工程的前后端分离思路更贴近企业真实开发模式答辩时可以说“前端通过 Nginx 部署后端通过 Jar 包运行两岸之间通过 RESTful 接口通信”这套表述本身就是加分项。没有选择 Vue 3 TypeScript是基于一个务实的考虑毕设时间有限大多数同学对 TS 类型体操并不熟练上 TS 反而增加编译报错的焦虑。Vue 3 的 Composition API 配合 JavaScript 就足够清爽了。2. 数据库设计与 SQL 脚本地基决定了项目上限2.1 表结构设计的四个核心细节SQL 脚本是整个系统里最不能出错的环节。表结构设计得有缺陷后面所有代码都在填坑。我总结四个关键点照做基本不会踩雷。第一必有主键且用自增整数。开发阶段没有分布式需求不要用雪花算法自找麻烦。id BIGINT AUTO_INCREMENT PRIMARY KEY是最稳妥的写法。第二逻辑删除统一字段。每个业务表都带上deleted TINYINT DEFAULT 0删除操作一律走UPDATE … SET deleted 1而不是 DELETE。这样客户删除后商机数据还能从逻辑上层找回也能避免外键约束阻止删除的麻烦。第三审计字段不要漏。create_time、update_time用DATETIME类型谁创建的、最近谁修改的create_by、update_by也留着。后端的 MyBatis-Plus 提供了自动填充功能代码里配置一个MetaObjectHandler就能自动写入不需要手工维护。第四多对多关系用中间表。用户和角色之间用sys_user_role角色和菜单权限之间用sys_role_menu。不要在用户表里存角色名字符串那是在给自己埋坑。核心业务表就这样CREATE TABLE customer ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, name VARCHAR(128) NOT NULL COMMENT 客户名称, industry VARCHAR(64) DEFAULT COMMENT 所属行业, source VARCHAR(32) DEFAULT COMMENT 客户来源, level TINYINT DEFAULT 3 COMMENT 客户等级 1-高 2-中 3-低, phone VARCHAR(32) DEFAULT COMMENT 联系电话, address VARCHAR(255) DEFAULT COMMENT 地址, owner_id BIGINT DEFAULT NULL COMMENT 负责人用户ID, deleted TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_owner_id (owner_id), KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户表;owner_id上的索引一定要加。CRM 系统最高频的查询就是“当前用户负责的客户列表”没有索引数据量上去后接口会肉眼可见变慢。2.2 SQL 脚本里必须包含的初始化数据一个完整的 SQL 脚本除了建表语句还要把初始化数据准备好。很多同学把数据初始化这一步留给程序自动处理但毕设评分时老师通常是直接导入脚本看系统的登录不了就意味着第一印象崩了。至少准备这些数据一个管理员账号比如admin / admin123密码存 BCrypt 加密后的结果。一套基础菜单对应前端路由和按钮权限标识比如customer:list、customer:add、opportunity:list等。若干演示数据客户表 5~10 条、商机 5 条、跟进记录 10 条行业类型要有差异IT、制造、金融都可以来几条让看板图表不至于空荡荡。初始化数据记得写在事务里包裹确保要么全部成功要么全部回滚。数据库字符集统一用utf8mb4排序规则用utf8mb4_general_ci。用utf8会带来一个问题客户名称里如果有 emoji 字符真的有人用 emoji 做公司名数据插进去会直接报错。提示开发机装的 MySQL 是 8.0 的话5.7的 SQL 语法基本都能兼容但注意ON UPDATE CURRENT_TIMESTAMP这个特性 5.6.5 之后才支持云数据库最低版本也建议用 5.7。3. 后端 SpringBoot 核心实现接口规范的实践样板3.1 目录分层和统一返回结构一个 SpringBoot 项目的代码结构最好让评审老师一眼就能看懂。这是我常用的包结构com.example.crm ├── controller # 接口层 ├── service # 业务逻辑 │ └── impl ├── mapper # MyBatis-Plus 的 Mapper 接口 ├── entity # 数据库实体 ├── dto # 请求参数对象 ├── vo # 返回视图对象 ├── config # 配置类跨域、拦截器、MyBatis-Plus ├── common # 通用类Result、异常处理、工具类 └── security # JWT 相关接口统一返回ResultT。这个类字段固定为code、message、data配合一个Result.success(data)和Result.error(code, msg)的静态工厂方法前端拿到后无需任何特殊判断结构直接看code 200即可。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }之所以要统一返回结构是因为前后端分离项目里出错信息对用户是有价值的。比如删除客户时因为外键约束失败了后端会抛出DataIntegrityViolationException如果原样把异常堆栈返回前端既不安全也不友好。配合全局异常处理器把错误码和业务含义转换一遍前端的弹窗提示才能写得像样。3.2 JWT 鉴权与拦截器的三个注意点JWT 鉴权几乎是现在 Web 项目的标配。毕设里实现起来也不难登录成功后后端生成一个 token 返回前端存在 localStorage 里后续每次请求都在 header 里带上Authorization: Bearer token。后端写一个 HandlerInterceptor在preHandle里校验 token校验失败返回 401。这里有三个非常容易出问题的细节专门提醒一下第一拦截器要放行登录接口。注册、登录、验证码这几个接口必须配置在排除名单里否则自己把自己挡住。PathMatcher pathMatcher new AntPathMatcher(); if (/api/auth/login.equals(request.getRequestURI()) || /api/auth/register.equals(request.getRequestURI())) { return true; }第二token 过期时间不要设太短。开发时你挂在页面上 30 分钟不动再操作就弹登录过期实在烦人。默认建议 24 小时答辩演示一天都不用重新登录。第三密码必须加密存储。用BCryptPasswordEncoder不要自己写个 MD5 加盐就觉得安全了。BCrypt 密文里自带盐每次加密结果都不一样但校验算法能正确匹配是业内标准做法。3.3 接口文档不是写给老师看的是写给 Postman 看的接口文档是项目压缩包里的重要组成但对很多人来说它是最后一天抄袭赶出来的产物。我理解时间紧张但接口文档并非形式主义——它是你联调时给自己用的。前后端分离开发时前端同学哪怕就是你自己切前后端页面不可能记住每个接口的入参结构。我建议文档采用这种格式每个模块一个章节接口按 RESTful 风格列出 URL、请求方式、请求参数表格、返回示例 JSON、错误码说明。规范几个接口命名风格让评委看着舒服查询客户列表GET /api/customer/list?page1size10namexxx新增客户POST /api/customer修改客户PUT /api/customer/{id}删除客户DELETE /api/customer/{id}逻辑删除接口文档里的返回示例一定要和真实接口结构吻合不要只写一半。否则前端同学拿着文档对字段十个字段对不上八个联调时间白白浪费。我习惯在写 Controller 的时候顺手把接口注释写了后面整理文档只是复制粘贴不会产生“代码写完了文档还没写”的情况。4. 前端 Vue.js 实现要点从页面到交互的完整链路4.1 工程结构和路由设计的组织方式前端工程用 Vite 构建目录结构建议这样src ├── api # 接口请求封装一个模块一个文件 │ ├── auth.js │ ├── customer.js │ └── opportunity.js ├── assets ├── components # 公共组件 │ └── Pagination.vue ├── layout # 主框架侧边栏顶栏内容区 ├── router # 路由配置 ├── store # Pinia 状态管理 ├── utils │ └── request.js # Axios 封装 └── views ├── login ├── dashboard ├── customer │ ├── list.vue │ └── detail.vue ├── opportunity └── follow路由设计要和后端菜单权限对应起来。最简单的做法是前端路由写死全套页面但在侧边栏菜单组件里按用户角色的菜单列表动态渲染。这样代码逻辑简单清晰也能实现“不同角色看到不同菜单”的效果。动态路由的玩法用后端下发路由去router.addRoute更适合大型后台毕设不建议碰容易把菜单加载顺序的坑写出来。4.2 Axios 封装拦截器里藏了多少坑几乎所有请求都要自动携带 token所有响应要先判断业务状态码这个逻辑最好收敛到一个地方。utils/request.js是前端所有网络请求的统一出入口。import axios from axios import { ElMessage } from element-plus import { useUserStore } from /store/user import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { ElMessage.error(登录状态已过期请重新登录) const userStore useUserStore() userStore.clear() router.push(/login) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } )这里有个新手常犯的错拦截器里返回了response.data后续页面写res res.data.data时就会拿到两层 data 的嵌套。解决办法是约定好拦截器返回完整res即{code, message, data}业务代码统一写const { data } await getCustomerList(params)再用data.list取列表。前后约定要统一别一会儿用拦截器解包一会儿用业务代码解包。4.3 前端页面交互的加分项页面交互上有几个细节特别容易给评委留下“这个系统做得还挺完整”的印象列表页的搜索表单和分页联动。搜索条件变化时页码重置为 1然后再请求数据删除一条记录后如果当前页只剩这一条且页码大于 1自动让页码减一。这个小逻辑不做用户删完数据会突然看到一片空白体验很差。表单校验。新增和编辑客户时姓名必填、电话格式校验用 Element Plus 自带规则就行。不管成功message.success(保存成功)的感觉比什么都用 window.alert 强。数据看板的日期范围筛选。让用户选择起止月份后端接收 startDate、endDate 参数做聚合查询。前端 ECharts 图表的dataZoom组件可以让图标在数据点较多时自动缩放这个细节体现工程素养。5. 部署联调与排查实录从源码到能演示的完整流程5.1 本地开发环境快速跑起来拿到的项目源码要快速在本地跑起来步骤基本固定但每一步都有常见问题。第一检查本机 JDK 版本。SpringBoot 2.7 系列用 JDK 8 或 11 都可以SpringBoot 3.x 系列必须 JDK 17 以上。如果你下载的项目是 2.7 版本而本机只有 JDK 17运行大概率会碰到奇怪的反射错误。建议直接装 JDK 11对两类项目都兼容。第二初始化数据库。用 Navicat 或命令行的source导入 SQL 脚本。导入后先检查sys_user表里管理员账号是否能查到密码字段是否是 60 位左右的 BCrypt 密文。如果密码是明文那就是脚本数据没做好需要重新生成。第三修改后端配置文件application.yml里的数据库连接spring: datasource: url: jdbc:mysql://localhost:3306/crm?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password注意serverTimezoneAsia/Shanghai这个参数。不加的话MySQL 8.0 默认时区是 UTC插入时间字段会跟本地时间相差 8 小时排查起来极其痛苦。第四启动前端。进入前端目录执行npm install如果速度很慢可以临时切换镜像源。安装完npm run devVite 默认跑在 5173 端口。5.2 最容易卡壳的跨域问题和解决方案跨域是前后端分离项目绕不过去的坎。开发环境有两个办法解决后端配置 CORS 或者前端配置 Vite 代理。我推荐开发期用 Vite 代理生产期用 Nginx 反代。Vite 代理配置在vite.config.jsexport default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/customer/list时Vite 开发服务器会把它代理到http://localhost:8080/api/customer/list。环境里看不到跨域报错浏览器 DevTools Network 里只显示/api/xxx的请求非常干净。后端也可以配一个 CORS 配置类作为双保险Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }网上很多帖子说 allowedOrigin 不能和 allowCredentials 同时用*确实如此。在 SpringBoot 里用allowedOriginPatterns(*)加allowCredentials(true)则不会冲突这是 Spring 5.3 之后支持的特性可以放心用。5.3 生产环境打包部署的完整链路毕设答辩通常需要把系统跑在自己的电脑上或者部署到云服务器演示。完整链路是前端构建出静态文件后端打成 Jar 包然后用 Nginx 托管前端、用 Java 命令跑后端。前端构建npm run build构建产物在dist目录里面是 index.html 和一堆带 hash 的静态资源。这里有个关键配置Router 要用 createWebHashHistory 而不是 createWebHistory。用 History 模式的话前端路由如/customer/list在刷新时会请求 Nginx 的/customer/list路径而 Nginx 找不到这个物理路径直接 404。Hash 模式则把路由放在#后面刷新不会发到服务端对毕设部署最友好。前端构建之后的产物可以放到 Nginx 的 html 目录Nginx 配置里再配一个反向代理server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }后端构建mvn clean package -DskipTests生成的target/crm-0.0.1-SNAPSHOT.jar直接运行java -jar crm-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod生产环境的application-prod.yml里记得把数据库、日志级别调整到合理的生产配置比如logging.level.rootWARN否则控制台会被 DEBUG 日志刷屏。5.4 常见问题速查整理几个我在部署和开发过程中真实遇到的坑做成速查表排查时直接对照现象可能原因解决方案数据库连接报Access denied密码错误或权限不足检查 application.yml本地 MySQL 执行GRANT ALL PRIVILEGES前端请求全部 404代理没生效或 baseURL 配置错误检查 vite.config.js proxy、Axios baseURL 是否包含 /api登录后刷新页面路由空白前端部署 History 模式刷新丢失改用 Hash 路由或 Nginx 配 try_files时间字段差 8 小时数据库连接未指定时区url 中加 serverTimezoneAsia/Shanghai接口返回 JSON 里有 null 大段堆叠实体类关联查询未处理空值VO 里对可能为空的字段做默认值处理或前端兜底 上传文件太大被拒Spring 默认 1MB 限制spring.servlet.multipart.max-file-size50MBECharts 图表不显示容器高度未定义给图表父容器设置显式 height而非 auto跨域请求 OPTIONS 预检报错后端未放行 OPTIONS拦截器排除 OPTIONS 请求或 CORS 配置允许 OPTIONS第七个问题值得多说一句ECharts 画图时如果父容器height是 auto 或者还没渲染完成图表会算错尺寸导致空白。在mounted里等页面绘制完再调myChart.resize()或者用nextTick包裹初始化逻辑基本都能解决。6. 这个项目的延伸扩展方向毕设做完不是结束如果你还想让这个项目在简历上更有分量可以在现有基础上做几个小扩展工作量不大但亮点明显。第一个方向是数据导入导出增强。客户列表增加 Excel 导入用 EasyExcel 库几十行代码就能实现导出则在后端生成文件流前端用 Blob 接收下载。这个功能在真实业务里是刚需简历上写“精通 Excel 导入导出”比写“熟悉 CRUD”有说服力得多。第二个方向是定时任务。比如每天凌晨统计一次客户跟进活跃度把超过 7 天没有跟进记录的商机自动打上“有流失风险”的标签。SpringBoot 里加一个Scheduled注解的方法就能定时跑然后列表页根据这个标签做排序展示。第三个方向是接入日志切面。用 AOP 记录每个用户的操作日志谁在什么时间访问了什么接口、参数是什么存入数据库。这个功能可以配合“操作日志”菜单展示客户删除后管理员也能查到操作记录。扩展方向不用全做。挑一个做到完整比三个都做但都半吊子强很多。我自己带过一届学生有人做了导入导出功能后在简历上写了“使用 EasyExcel 实现万级数据导入”面试官明显对能说清底层数据流向的候选人印象更深。客户关系管理系统这个选题技术难度适中业务闭环完整扩展空间充足拿来做毕设里是很稳的选择。整个过程走一遍SpringBoot 的接口编写、MyBatis-Plus 的用法、Vue 的组件通信、Axios 的请求拦截、Nginx 的部署反代这些实际开发的核心技能基本都覆盖到了。项目做完你获得的不是一堆需要删除的代码而是对 Web 全栈开发完整流程的体感。最后提一个实操层面可能想不到的小点开发过程中如果遇到明明改了代码却不生效的情况先检查是不是后端没重启、前端 dev server 缓存、或者浏览器还挂着旧页面。这类“低级问题”在答辩演示时刻出现会让人特别紧张。所以演示前一定先按完整流程跑一遍所有核心功能把自己当用户从头点到尾该修的修完再上场。
返回列表