如果你接手过保险行业的信息化项目肯定知道真正麻烦的从来不是一张保单的增删改查而是合同从录入、核验、审批到续期、批改、归档这一整条链路的闭环。今天聊的这套“可盈”保险合同管理系统就是按这条链路设计出来的技术栈是典型的SpringBoot Vue MyBatis MySQL前后端分离组合。它把保险业务里最常用的客户管理、保单录入、审核流、附件归档和统计查询全部串了起来是一个可以直接拿去二次开发的项目底座。这篇总结不打算列一条条API清单我更想站在刚把项目完整搭起来的开发者视角把从架构选型到后端实现、从Vue前端联调到打包部署的关键环节以及那些文档里不会写的坑一次性掰开讲清楚。如果你是正在学SpringBoot Vue全栈的初学者或者准备做毕业设计、公司内部测试系统的同学这篇内容可以当作一条完整的实战路线图来参考。里面涉及的分页插件、XSS过滤器、跨域、打包部署每一块都是实操中一定会碰到的硬骨头。1. 项目整体架构与设计思路1.1 为什么选前后端分离从保险合同的业务场景说起保险合同管理系统有一个很典型的特点页面交互复杂度远高于普通CRUD。一张合同表单往往带着几十个字段从投保人信息到险种明细从缴费方式到特别约定前端要做的联动计算、动态校验、附件上传非常重而后端要管的则是业务规则和状态流转。如果还按老一辈的JSP Thymeleaf方式做前端改一个表单布局都要碰到后端页面两边耦合在一起谁都不敢动。前后端分离之后分工就非常清楚了Vue那边只负责渲染、交互、表单校验SpringBoot只提供JSON接口MySQL负责数据落库MyBatis管SQL。前端要改后端接口不动就行后端要加权限校验前端也没有感知。对保险业务这种经常调整表单、增加审批节点、补充统计口径的系统来说这个弹性太重要了。实际开发的时候还可以前后端并行后端先定好接口文档前端直接拿Mock数据开发等到联调阶段再对接真实接口整体工期能压缩不少。另外一个现实原因是招人和学习的角度。现在SpringBoot Vue的前后端分离模式是整个互联网行业的中坚形态不管是面试还是接手现有项目这套组合都属于必修课。用保险合同这种业务逻辑完整的系统来练手比单纯写一堆Demo更能体会到架构设计在真实业务里的重量。1.2 技术栈选型与版本搭配的心得这套系统的技术栈本身不新鲜但版本搭配是有讲究的。我用的是SpringBoot 2.7.x兼容性最稳的一个版本线JDK8和JDK11都能跑网上资料也最多。Vue 3 Vue Router 4 Pinia新项目直接用Vue3没毛病Composition API写业务逻辑更舒服。如果你手里的代码习惯是Vue2写法换成Vue2也完全能对应上核心逻辑不变。MyBatis 3.5.x手写SQL可控性最强尤其在复杂统计报表、多表关联查询时MyBatis的XML里写SQL比JPA自动生成的SQL直观得多。MySQL 5.7 / 8.0开发环境用5.7最常见生产环境建议上8.0但要注意8.0的驱动类名是com.mysql.cj.jdbc.Driver连接URL里还要加上serverTimezoneAsia/Shanghai否则时区报错能卡你半小时。这里多说一句标题写的是MyBatis但如果你追求开发效率完全可以在这个架构上换MyBatis-PlusCRUD不用写XML内置分页插件。我在这套系统里坚持用原生MyBatis一是为了把SQL控制权握在手里二是给阅读源码的人展示最本质的配置方式。实际上手时如果表单CRUD太多引入Plus也不丢人这是我前后端分离项目里最常干的取舍。1.3 业务模块划分与数据库表设计复盘这个系统的业务模块我拆成了六个核心块客户管理投保人、被保人基础信息维护。合同录入选择险种、填写保额保费、生成合同编号。合同审核工单式的审批流支持通过、退回、驳回。合同管理对已生效合同进行批改、续期、终止操作。附件归档电子保单、签名文件、身份证照片上传下载。统计报表按险种、按业务员、按月份统计生效合同数量与保费规模。数据库设计上核心表是contract合同主表字段包括contract_no合同编号、customer_id客户ID、policy_status合同状态、total_premium总保费、start_date起保日期、end_date终保日期等。这里有两个容易翻车的点我特意说一下第一合同金额字段千万不要用double或float。保险的保额、保费精确到分浮点运算会出精度误差MySQL里直接用decimal(18,2)Java实体里用BigDecimal这是金融类系统的红线。第二审批记录不要简单地在主表上堆状态字段建议单独建一张approval_log表记录contract_id、action操作类型、operator_id操作人、comment审批意见、create_time操作时间。这样合同在哪个环节被谁退回过都有完整审计链路这是保险行业合规检查的刚需。2. 后端核心实现SpringBoot MyBatis 的打磨过程2.1 SpringBoot骨架搭建与关键配置项目骨架直接用Spring Initializr生成依赖选web、mybatis、mysql driver、lombok、validation这几个就够了。需要注意的在于很多人建完项目后直接把默认配置丢着不管等上线才暴露问题。application.yml里我通常会把数据源配置写成这样spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ke_ying_contract?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 30000 connection-timeout: 30000关于HikariCP连接池参数很多人直接复制默认值其实maximum-pool-size的取值有个简单经验公式((核心线程数 * 2) 磁盘并发数)。比如一台4核8线程的服务器业务以IO为主连接池上限设在10到20之间比较合理。不是越大越好连接数太多反而增加数据库负担尤其MySQL默认连接数就那么多池子开太大会先耗尽数据库资源。后端还有一个非常重要的习惯是多环境配置。我习惯把公共配置写在application.yml然后拆出application-dev.yml和application-prod.yml启动时用spring.profiles.activedev来切换。这样本地连测试库、生产连正式库不用每次上线都改代码。这套系统里我走了不少弯路早期把数据库密码直接写在公共配置里后来一次误连测试库把业务数据搞乱才老老实实拆环境。2.2 MyBatis映射、动态SQL与分页插件用法MyBatis的使用里mapper接口和XML是核心。我习惯把SQL都写在XML里因为合同列表这种查询条件非常多按客户名、按险种、按时间范围、按合同状态条件的组合情况可能有十几种用动态SQL最合适。下面这个片段是合同条件查询的精简版我用where标签自动处理多余ANDselect idselectContractList resultTypecom.keying.entity.Contract SELECT * FROM contract where if testcustomerName ! null and customerName ! AND customer_name LIKE CONCAT(%, #{customerName}, %) /if if testinsuranceType ! null and insuranceType ! AND insurance_type #{insuranceType} /if if teststatus ! null and status ! AND policy_status #{status} /if /where ORDER BY create_time DESC /select分页部分是这个项目的重头戏。MyBatis本身不提供分页能力最常用的方案是PageHelper分页插件。用法看起来简单但坑不少PageHelper.startPage(pageNum, pageSize); ListContractVO list contractMapper.selectContractList(queryDTO); PageInfoContractVO pageInfo new PageInfo(list);注意PageHelper.startPage()必须写在要分页的查询语句之前而且是紧挨着的那一行。它底层是用拦截器改写SQL给原语句包一层LIMIT ? OFFSET ?。如果你在startPage()和selectList()之间插入了其他查询分页条件就被其他SQL截胡了查出来的数据就是全量还特别隐蔽。另外一定要根据MyBatis版本选对依赖。SpringBoot整合场景下直接用pagehelper-spring-boot-starter不是mybatis-pagehelper那种老包不然拦截器不会自动注册。我早期就踩过引错包的坑分页一直不生效翻源码才发现是注册机制对不上。2.3 全局过滤器处理XSS攻击保险合同系统里有很多富文本字段比如特别约定、批改说明前端是直接渲染HTML的。这就带来一个常见安全问题XSS攻击。用户在批改说明里塞一段恶意脚本如果后端不过滤直接存库前端v-html再渲染出来等于把攻击执行在了每个管理员的浏览器里。这种攻击在金融系统里特别致命因为后台权限高一旦中招可能被拖走大量保单数据。处理方案是在SpringBoot里加一个全局过滤器继承OncePerRequestFilter对请求参数做转义。核心代码思路是这样的public class XssFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { chain.doFilter(new XssHttpServletRequestWrapper(request), response); } }XssHttpServletRequestWrapper重写getParameter、getParameterValues、getHeader把script、javascript:这类危险字符转义成HTML实体。然后通过FilterRegistrationBean注册并且要设置过滤的URL匹配规则不是所有接口都需要过这一层比如登录接口过了也没意义。这里要说一个容易过度的地方别把富文本字段里的合法HTML也全过滤掉。我一开始图省事把所有参数都做HtmlUtils.htmlEscape结果合同特别约定里加粗、表格这些样式全变成了乱码前端渲染出来像一堆源码。正确做法是做个白名单机制只对危险标签做处理允许b、p、table这种业务需要的标签通过。2.4 MyBatis缓存与慢SQL排查记录MyBatis自带一级缓存和二级缓存一级缓存是SqlSession级别的默认开启但Spring整合后SqlSession每次操作完就关闭了所以跨方法基本用不上。二级缓存是mapper级别的需要手动开启但要注意缓存了合同这种敏感数据后一旦数据修改缓存刷新不及时会出大问题。我在合同系统里默认关闭二级缓存因为保险数据的一致性要求非常高不是那种读多写少的论坛场景没必要为了一点性能提升冒数据不一致的风险。搜索热词里有一条“mybatis update 执行慢”我在这套系统里确实碰到过。有一次在审批通过的逻辑里更新合同状态单条update语句执行了快两秒查了一圈发现是contract表的policy_status字段没有索引而这个字段被大量where条件引用。后来在状态字段上加了普通索引又检查了事务隔离级别更新就回到了毫秒级。排查这类问题先把MyBatis的SQL日志打开logging: level: com.keying.mapper: debug一旦日志打印出SQL语句可以直接把语句拿到MySQL客户端里EXPLAIN执行计划看有没有走全表扫描。这是定位慢SQL最标准的一条路比瞎猜where条件快得多。3. 前端Vue实现与前后端联调3.1 Vue环境配置与项目初始化前端环境这块第一步就劝退不少人。Vue的构建工具链依赖Node.js建议不要直接装最新版用nvm管理多个Node版本更靠谱。我在这套系统里用的是Node 16对应npm 8搭配vue-cli或者Vite都能跑。这里提醒一句npm install如果慢得想砸电脑先检查镜像源是不是默认的国外源换成国内镜像后速度立竿见影。初始化项目我用vue create命令选择自定义配置包含Router、Pinia或者Vuex看你习惯、ESLint。目录结构我会主动调整一下把接口请求统一放src/api页面放src/views公共组件放src/components工具函数放src/utils。别看这个整理动作很小等合同录入页、审批页、客户管理页全部铺开后乱目录的项目改起来想哭。3.2 路由设计与页面间参数传递前端路由的核心场景是合同列表页跳转合同详情页。我用的路由配置大概长这样const routes [ { path: /contract/list, component: () import(/views/contract/ContractList.vue) }, { path: /contract/detail/:id, component: () import(/views/contract/ContractDetail.vue) }, { path: /approval/list, component: () import(/views/approval/ApprovalList.vue) } ];从列表跳详情用params传合同IDthis.$router.push({ name: ContractDetail, params: { id: row.id } });这里要区分params和query的适用场景。params传参后刷新页面参数可能丢失适合详情页这种不依赖URL回显的场景query参数会拼接在URL上适合过滤条件、分页页码这样用户复制链接或者刷新后条件还在。我在合同列表页的筛选功能里就用query跳详情页用params两种方式各司其职。很多新人在详情页取不到参数通常是因为路由表里没有配置:id占位符或者跳转时用query传参却用params接收。花五分钟检查路由定义比在页面里打印$route半天效率高得多。3.3 Axios封装、跨域配置与统一错误处理前端和后端联调时如果每个页面都直接写axios.get那重复代码能让你怀疑人生。我习惯在src/utils/request.js里封装一个统一实例把baseURL、请求头、响应拦截器都集中管理import axios from axios; const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL || /api, timeout: 30000 }); service.interceptors.request.use(config { const token sessionStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { // 跳转登录页逻辑 } return Promise.reject(error); } ); export default service;这样封装的好处是后端接口返回结构统一后页面里直接const res await getContractList(params)就能拿到数据不用每个页面都判断一遍HTTP状态码。开发环境的跨域问题通过vue.config.js配置代理就能解决设一个proxy把/api开头的请求转发到后端服务。但到了生产环境前端静态文件和后端接口如果不在同一端口要么用Nginx做反向代理要么后端开启CORS。我的习惯是生产环境优先用Nginx让前端请求/api时由Nginx转发到Java后端这样浏览器看到的是同源请求最省心。3.4 典型业务页面的实现细节合同编辑页是前端交互最复杂的部分涉及险种下拉联动、保额保费自动计算、日期范围校验。这里有两个容易忽视的细节第一表单数据初始化时日期字段从后端拿到的可能是2024-05-01T00:00:00这种带T的格式直接放进el-date-picker会显示异常。我一般统一封装一个formatDate方法对数据做一次格式化再塞回表单。第二保额保费的计算前端可以使用vue的计算属性做联动展示但最终提交给后端时必须规范成字符串避免精度问题。前端的浮点运算会出0.10.20.30000000000000004这种结果合同金额绝不允许这样提交。我的做法是前端只做展示用精确计算全部交给后端前端拿到后端返回值再回显。页面里合同状态我习惯做成标签组件不同状态显示不同颜色待审批黄色、已生效绿色、已退保灰色、审批驳回红色。这样审批人员在列表页一眼就能看到哪些合同卡住了比纯文字直观得多。这个组件抽出来之后列表页、详情页、审批页都能复用是前端项目里投入产出比很高的一个小模块。4. 本地到生产Maven打包与多方式部署实战4.1 MySQL安装与数据库初始化数据库这块先说环境。Windows下安装MySQL通常用免安装版或者安装包Linux下用apt或yum装安装完成后第一件事就是改root密码和设置字符集。我的建议是数据库统一用utf8mb4字符集千万别只用utf8因为utf8mb4才完整支持emoji和生僻字。保险合同里的客户姓名可能就带生僻字用utf8存进去会变问号这是保险行业里实际发生过的事故。初始化数据库时把建表语句按模块拆成几个SQL文件contract.sql、user.sql、approval.sql分文件执行比一大坨脚本混在一起好维护。导入完成后务必先跑几条查询验证表结构再改application.yml里的连接配置。4.2 后端打包与jar启动后端打包命令很简单mvn clean package -DskipTests打包完成后在target目录下会生成一个可执行的jar。启动就用java -jar ke-ying-contract.jar --spring.profiles.activeprod这里有个经典问题SpringBoot的spring-boot-maven-plugin没配置时打出来的jar启动会报“没有主清单属性”。解决方式是在pom.xml里加上这个插件的配置让jar能直接找到main方法。如果是用Spring Initializr生成的项目一般不会有这个问题就怕某些人从网上复制pom把插件整丢了。另外要说一下jar包和静态资源的关系。虽然是前后端分离但如果你想偷懒可以把Vue打包后的dist目录直接复制到SpringBoot的src/main/resources/static/下这样后端jar启动后就能访问前端页面。这种“合并部署”模式适合小项目、测试环境但真正的前后端分离生产环境我更推荐下面这种方式毕竟独立部署才是分离架构的初衷。4.3 前端打包与Tomcat部署前后端同机方案前端打包命令npm run build执行完会生成dist目录里面是静态HTML、JS、CSS。把dist目录里的内容复制到Tomcat的webapps/ROOT目录下先清空原来的ROOT然后启动Tomcat就能通过http://ip:8080访问前端页面。后端jar还在8081端口跑前端通过/api请求同源的Tomcat端口再由Tomcat或Nginx转发。这种前后端共用一台云服务器的方式最省钱也最常见。需要注意两点第一Tomcat默认端口是8080如果后端jar占了8080要改Tomcat的server.xml端口或者让后端换端口两人都抢8080谁都起不来。第二前端路由用的是history模式时直接访问http://ip:8080/contract/detail/1会返回404因为Tomcat找不到这个真实文件路径。解决办法是配置Tomcat的rewrite规则让它把不存在的路径统统指向index.html。如果你不信邪非要在前端路由里用hash模式那URL里会多一个#虽然省配置但被产品经理看到会被嫌弃。4.4 升级玩法Nginx静态托管 Jenkins自动化部署如果用Nginx托管前端配置更灵活。典型的配置片段server { listen 80; server_name your-domain.com; location / { root /opt/ke-ying/dist; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里try_files $uri $uri/ /index.html;就是为了解决history路由刷新404的问题比Tomcat rewrite简单太多了。Jenkins自动化部署是团队协作场景下的加分项。流程不复杂在Jenkins里建两个任务一个负责后端执行mvn clean package后把jar传到服务器通过脚本停机、替换、重启另一个负责前端执行npm run build后把dist同步到Nginx目录。最怕的是用Jenkins部署前后端分离项目时构建产物路径搞错发布半天页面没变化一看是Nginx还指向旧目录。所以部署脚本里我用绝对路径禁止写相对路径这是被坑过后的血泪教训。5. 常见问题与排查技巧实录5.1 分页不生效、查出来全量数据这个坑前面提了一嘴这里把现象说全。症状是调接口返回的数据不分页或者分页参数没作用。排查顺序是这样的第一确认是否在startPage()后紧跟着执行了查询第二检查是不是引错PageHelper依赖包第三看MyBatis的拦截器是否注册成功可以在日志里看有没有PageInterceptor的初始化记录最后还是不行的话在SQL日志里直接看有没有LIMIT语句没有就说明插件没拦截到语句。有一个很隐蔽的问题PageHelper.startPage()之后如果调用了mapper的查询但这条SQL属于嵌套查询或者有子查询分页插件可能会把count语句也执行一次导致总记录数不对。这种情况下我会先简化SQL让子查询改成JOIN再测试分页。5.2 前后端跨域与401认证拦截开发环境跨域按前面说的配proxy就行。如果出现了“Access-Control-Allow-Origin”报错先确认代理是否生效再看baseURL是不是写成了绝对地址导致请求直接从前端端口发出而没有走代理。生产环境跨域则优先用Nginx代理。如果非要在后端开CORS加上这样一段配置允许指定来源registry.addMapping(/api/**) .allowedOriginPatterns(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true);这段配置要特别注意allowedOriginPatterns和allowCredentials必须配对如果写了allowedOrigins(*)又开allowCredentials(true)浏览器照样报错。我见过很多人在这里卡一小时。401拦截的问题通常是指定了token请求头但后端没接住。前后端要约定同一个header名称我习惯统一用Authorization后端用一个JwtInterceptor或者HandlerInterceptor处理前端封装axios时把这个header带上。前后端字段名对不上时登录接口成功但业务接口全是401排查起来真的耽误时间。5.3 部署后路由刷新404、静态资源不显示这个属于部署环节的高频问题。路由刷新404的原因前面说了history模式下需要在后端配置路径重写。Nginx用try_filesTomcat用rewrite或者FallbackServlet。如果你用的是Nginx我建议把这个配置当成标准配置固定下来每个项目都套用这一套能少踩太多坑。静态资源不显示的表现是页面能打开但样式全乱、图片不加载。大部分情况是publicPath配置成了绝对路径/而部署的站点不是根路径而是带了一个子路径如http://ip:8080/ke-ying/。解决办法是在vue.config.js里把publicPath改成相对路径./重新打包部署即可。还有一个隐藏点前端打包后浏览器缓存了旧版本。我经常碰到用户反馈“我改了界面怎么线上没变”清缓存看一次十有八九是新版本没生效。前端可以在资源文件名上带hash或者把Nginx的Cache-Control配置得短一些至少我在内部系统里是直接把no-cache写上的。5.4 MyBatis参数绑定与SQL字段映射问题MyBatis里最常见的报错是“Parameter xxx not found”。解决手法就一句话多参数方法上一定要加Param注解。比如ListContract selectByCustomerAndStatus(Param(customerId) Long customerId, Param(status) String status);XML里的#{customerId}和#{status}就会和注解绑定。如果不加注解MyBatis会按arg0、param1这种顺序位置来匹配写起来又乱又容易错。我对团队的要求是只要接口方法参数超过一个必须加Param这是能省一晚上调试时间的好习惯。字段映射问题则是数据库字段用下划线policy_statusJava属性用驼峰policyStatus结果查出来全是null。解决方法是开启map-underscore-to-camel-case配置mybatis: configuration: map-underscore-to-camel-case: true这个配置开启后默认就能把policy_status映射到policyStatus。前提是SQL查询的列名要规范别自己起一些奇怪的别名把映射规则破坏了。写在最后的一个实操提醒系统完整跑起来之后我最大的体会是这种前后端分离项目真正花时间的从来不是能不能跑通而是各种“资源都配好了但就是不对”的隐形问题。比如分页插件的拦截顺序、跨域的凭证字段、数据库时区、前端路由刷新404这些问题单看都不难但叠加在一起足以让人心态爆炸。最后再分享一个小技巧整套系统的开发调试阶段我习惯把后端的SQL日志打开同时在前端控制台打开Vue DevTools和Network面板。后端打开MyBatis日志能看到SQL是否正确DevTools能看到Vue组件状态和数据流Network面板能看到请求状态码和响应时间。这三者配合百分之八十的联调问题都能当场定位不用来回猜。后续如果要把这套系统往生产级演进还可以在这个底子上加工作流引擎处理更复杂的保险审批流把Redis缓存加进来提升查询性能以及对接报表引擎做保费统计的套打导出。每一步都是在现有架构上做增量不会推翻重来这也是当初选SpringBoot Vue这套成熟组合的最大价值。