拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

前后端分离应急物资管理系统实战:SpringBoot+Vue部署全攻略

前后端分离应急物资管理系统实战:SpringBoot+Vue部署全攻略 2. 前后端分离的应急物资管理系统项目实战复盘做完整源码项目这么多年前后端分离 应急物资管理系统这个组合我前前后后带过不少团队落地。选这个项目类型很讲究核心业务清晰物资入库、出库、库存盘点、预警统计技术栈又是国内招聘需求最旺盛的 SpringBoot Vue MyBatis MySQL 全家桶加上完整源码和部署教程的定位正好卡在刚入行和正在准备面试的开发者最需要的区间。这套系统能解决的核心问题是应急场景下物资从采购入库、日常库存维护到紧急调拨出库的全流程数字化管理同时对低库存、临期物资做自动化预警取代传统Excel台账的人工统计方式。无论你是刚学完SpringBoot和Vue基础理论、想拿一个完整项目练手的新人还是已经在写业务代码、想补全系统设计和部署经验的开发者这套系统的完整实现过程都值得完整过一遍。我会把源码设计思路、每个核心功能背后的考量、部署时的操作细节和一坑一坑踩出来的排查经验全部拆开讲。1.1 项目业务需求梳理开始设计之前先把业务场景摸清楚。应急物资管理系统本质上是一个多角色协作的库存管理平台不是简单的增删改查。我在前期需求调研阶段就把角色分成三类系统管理员负责基础数据维护和用户管理、仓库管理员执行物资出入库操作、盘点库存、普通用户/领导层查看库存情况和预警报表。核心业务流有三条主线入库流物资到达 → 验收 → 生成入库单 → 库存数量增加 → 更新库存记录出库流领用申请 → 审批 → 出库登记 → 库存数量减少 → 生成出库记录预警流库存低于安全阈值触发低库存预警物资超过保质期触发临期预警考虑应急场景的特殊性还要支持应急调拨这种快速出库方式——不需要走完整的审批链由管理员确认后直接出库并留痕。这个设计源于一个实际痛点突发情况时物资需求紧急如果流程太长反而会耽误事。1.2 系统模块划分与功能清单整体模块划分围绕上述业务流展开我最终落地为六大功能模块模块核心功能说明用户管理登录认证、权限控制、用户CRUD基于JWT实现无状态认证物资管理物资分类、物资信息维护、库存管理物资编码唯一关联分类和供应商出入库管理入库单、出库单、调拨单、操作记录每笔操作都生成明细流水库存预警低库存预警、临期预警、预警处理定时任务扫描手动触发刷新统计报表库存总览、出入库趋势、分类占比基于ECharts展示可视化图表系统管理操作日志、数据备份、个人信息修改记录关键操作支持审计追溯这套模块划分的核心逻辑是业务闭环从数据维护→业务操作→状态监控→统计分析每一个环节的数据都有上游来源和下游去向不会出现数据孤岛。我在设计时特别要求自己养成一个习惯先画业务数据流图。做数据流设计的时候会发现需求里很多我以为没问题的边界情况比如物资被领用出库后还能不能修改基础信息、库存不足时入库单如何冲正等问题。1.3 数据库表设计与关系建模数据库是这类系统的地基我见过太多项目后期改表结构改到崩溃的情况。这个系统的核心表有九张用户表、角色表、物资分类表、物资信息表、供应商表、入库单表、入库明细表、出库单表、出库明细表加上系统日志表和预警记录表共11张表。关键设计点在于主表和明细表分离。比如入库单主表只存单号、入库时间、操作人、供应商ID、备注等汇总信息入库明细表才存具体每项物资的数量、单价。这样的好处是统计时可以直接按单汇总明细查询也不会因为数据量大而拖慢主流程。物资信息表我单独强调一下字段设计有讲究CREATE TABLE t_material ( id INT PRIMARY KEY AUTO_INCREMENT, material_code VARCHAR(32) UNIQUE COMMENT 物资编码, material_name VARCHAR(64) NOT NULL COMMENT 物资名称, category_id INT COMMENT 分类ID, specification VARCHAR(128) COMMENT 规格型号, unit VARCHAR(16) COMMENT 计量单位, stock_quantity INT DEFAULT 0 COMMENT 当前库存数量, safe_quantity INT DEFAULT 0 COMMENT 安全库存阈值, expiry_date DATE COMMENT 保质期截止日期, supplier_id INT COMMENT 供应商ID, created_time DATETIME, updated_time DATETIME );这几个字段的取舍有实际业务考量stock_quantity作为冗余字段直接存在物资表而不是每次通过明细表SUM计算。虽然违反了部分范式理论但实际查询库存列表和做预警判断时性能优势明显因为这类系统读操作远多于写操作用空间换时间是值得的。safe_quantity是预警逻辑的核心依赖这个数值由管理员根据物资重要程度和使用频率动态设置。1.4 为什么选SpringBootVue这套技术组合技术选型方面我在项目启动前对比过好几套方案。RuoYi若依这类框架虽然功能全、生态成熟但代码量庞大新手往往分不清核心逻辑和框架自带功能的边界。纯Servlet JSP的老方案则是开发效率太低前后端耦合严重维护成本高。最终确定的组合是SpringBoot 2.7 MyBatis MySQL 8.0 Vue 2.6 Element UI Axios。对这套方案的考量SpringBoot简化了Spring的配置地狱内嵌Tomcat打Jar包就能跑部署极其方便是目前企业级项目的事实标准MyBatis相比JPA/HibernateSQL完全手写可控复杂查询和调优有底气国内团队使用率极高出现问题网上资料也最全Vue 2 Element UI如果用Vue 3则对应Element Plus在中后台管理系统领域生态最成熟表格、表单、弹窗、分页这些组件开箱即用能节省大量开发时间这套组合的开发效率我实测下来从零到功能完整可部署一个熟练的开发者大约需要7-10天。如果换成其他组合时间成本至少翻一倍。涉及数据库连接池我用的是阿里巴巴的Druid不纯粹是因为功能强而是它自带的监控页面对于排查慢SQL和连接泄露问题太方便了生产环境排查问题时会感激当初这个选择。1.5 项目工程结构与开发环境配置项目采用前后端彻底分离的物理结构我习惯把代码分成两个独立目录互不干扰emergency-material-system/ ├── frontend/ # Vue前端工程 │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── assets/ # 静态资源 │ │ ├── components/ # 公共组件 │ │ ├── router/ # 路由配置 │ │ ├── store/ # Vuex状态管理 │ │ ├── views/ # 页面视图 │ │ ├── App.vue │ │ └── main.js │ ├── package.json │ └── vue.config.js └── backend/ # SpringBoot后端工程 ├── src/main/java/com/emergency/ │ ├── config/ # 配置类CORS、拦截器、Druid │ ├── controller/ # 控制层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis数据访问接口 │ ├── entity/ # 实体类 │ ├── common/ # 通用工具、统一返回结果 │ └── EmergencyApplication.java ├── src/main/resources/ │ ├── mapper/ # MyBatis XML映射文件 │ └── application.yml └── pom.xml后端按controller - service - mapper三层分包每一层只做自己的事。我特别习惯在common包里放一个Result统一返回结果的类所有接口都返回{ code, message, data }这种结构前端处理起来非常统一不用每个接口单独判断返回格式。开发环境的JDK我用的是1.8MySQL 8.0Node.js 14以上。这里要强调一个兼容性细节SpringBoot 2.7搭配MySQL 8.0的驱动是com.mysql.cj.jdbc.Driver而不是老版本的com.mysql.jdbc.Driver如果照抄旧项目配置直接会报驱动类找不到。1.6 核心依赖版本选择策略Maven依赖的版本号是我踩过不少坑之后总结出来的策略优先选稳定版本不盲目追新。SpringBoot的父依赖直接用官方管理的版本号不用额外指定MyBatis的SpringBoot整合包用mybatis-spring-boot-starter当前稳定版是2.3.xDruid用druid-spring-boot-starter1.2.x。数据库驱动版本让Maven自动管理即可不需要手动指定。选版本有个很实用的经验去Maven中央仓库看下载量和发布时间下载量高、发布时间超过半年的版本通常比较稳。那些刚发布一两个月的所谓最新版除非有必须要用的新特性否则不要在生产项目里当小白鼠。2.1 登录认证与权限控制实现方案登录认证是这类系统的第一道门我采用的是JWTJSON Web Token 拦截器方案这也是目前前后端分离项目的主流做法。相比传统的Session方案JWT无状态的特点让后端服务天然支持水平扩展多个实例之间不需要共享Session。实现逻辑是用户提交用户名密码 → 后端校验通过后生成一个有效期为2小时的Token → 返回给前端存储 → 前端每次请求在Header中携带Authorization: token值→ 后端拦截器校验Token合法性并解析用户信息。JWT生成的核心代码基于jjwt 0.9.1public String generateToken(User user) { // JWT生成设置主题用户ID、签发时间、过期时间用HS256算法签名 return Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(username, user.getUsername()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }这里有几个容易掉进的坑密钥硬编码问题很多示例代码把SECRET_KEY直接写在代码里生产环境一定要放到配置文件或环境变量中拦截器放行路径登录接口、静态资源、Swagger文档等必须放行否则会出现登录页能打开但接口全被拦截的诡异问题Token过期处理前端要统一处理401状态码过期后自动跳转到登录页并清除本地存储的Token信息2.2 统一返回结果与全局异常处理写接口时如果每个方法都手动拼JSON返回代码会非常啰嗦且风格不统一。我设计了一个Result类作为所有接口的统一出口同时配合RestControllerAdvice做全局异常处理。这部分的投入会在后期开发和对接时省下大量时间。统一返回结构public class ResultT { private Integer code; // 200成功500业务错误401未认证 private String message; // 提示信息 private T data; // 业务数据 // 静态方法success(data)、error(message)... }全局异常处理的核心逻辑是用ExceptionHandler捕获所有未处理的异常转成标准的Result错误响应。我在这里专门处理了一个业务场景当库存不足时Service层抛出自定义的BusinessException(库存不足)由全局处理器统一转换前端就能在响应中直接拿到错误信息并提示用户。这样Service层就不需要到处写try-catch代码干净很多。2.3 物资出入库业务逻辑与事务控制出入库是系统的核心业务也是事务控制的重灾区。入库操作的逻辑看似简单但牵扯到三个表的更新插入入库单主表记录、批量插入入库明细表、更新物资表库存数量。任何一个步骤失败都会导致数据不一致所以必须用Transactional注解来保证原子性。入库的核心流程代码Transactional(rollbackFor Exception.class) public void createInboundOrder(InboundDTO dto) { // 1. 生成入库单号RK 年月日 随机数如 RK20250115120345 String orderNo generateOrderNo(RK); // 2. 插入入库单主表记录 InboundOrder order new InboundOrder(); order.setOrderNo(orderNo); order.setSupplierId(dto.getSupplierId()); order.setInboundTime(new Date()); // 3. 批量插入入库明细 for (InboundItemDTO item : dto.getItems()) { inboundOrderMapper.insertItem(order.getId(), item); // 4. 更新对应物资的库存数量递增 materialMapper.increaseStock(item.getMaterialId(), item.getQuantity()); } }这里有几个事务控制的细节值得注意rollbackFor Exception.class必须写因为Spring默认只对运行时异常回滚受检异常不触发回滚不写这个参数会导致部分异常场景下数据错乱明细批量插入时我用的是MyBatis的foreach标签拼接批量Insert一次SQL执行插入全部明细比循环单条插入性能高一个数量级increaseStock使用SQL层面的自增操作UPDATE t_material SET stock_quantity stock_quantity #{quantity}而不是先在Java代码里查询当前值再加避免并发场景下的超卖问题2.4 MyBatis分页插件与多表联查技巧列表查询是这个系统最常用的功能。MyBatis使用分页插件PageHelper是最常见的方案用法很简单// 分页查询第一页查10条PageHelper会自动拦截下一条查询并生成COUNT和LIMIT语句 PageHelper.startPage(pageNum, pageSize); ListMaterialVO list materialMapper.selectMaterialList(materialName, categoryId); PageInfoMaterialVO pageInfo new PageInfo(list);使用PageHelper有一个很重要的注意点PageHelper.startPage()必须紧跟第一条查询语句中间不能插入其他查询。因为我见过不少人把startPage放在查询前的好几行比如先查了字典表结果分页信息全乱了。另外如果查询结果不需要返回总记录数可以用PageHelper.startPage(pageNum, pageSize, false)跳过COUNT查询列表性能会有明显提升。多表联查方面物资列表需要关联物资分类表和供应商表展示分类名称和供应商名称我在MyBatis的XML中直接写LEFT JOIN返回一个VO对象select idselectMaterialList resultTypecom.emergency.entity.vo.MaterialVO SELECT m.*, c.category_name, s.supplier_name FROM t_material m LEFT JOIN t_category c ON m.category_id c.id LEFT JOIN t_supplier s ON m.supplier_id s.id where if testmaterialName ! null and materialName ! AND m.material_name LIKE CONCAT(%, #{materialName}, %) /if if testcategoryId ! null AND m.category_id #{categoryId} /if /where ORDER BY m.updated_time DESC /select动态SQL用where标签加上if条件判断比直接用字符串拼接SQL要安全得多既防止SQL注入风险也不需要花精力处理条件多余的AND问题。这个写法属于MyBatis的基础操作但也是面试常考的mybatis面试题类型建议把where、set、foreach、choose这几个动态SQL标签的用途彻底掌握。3.1 Vue项目初始化与环境配置前端开发的第一步是搭建Vue工程环境。在开始项目前必须把Node.js环境安装并配置好——去Node.js官网下载LTS版本安装包按提示安装即可。很多人在这个环节遇到npm不是内部或外部命令的报错原因是环境变量没有配置到位需要把Node.js的安装路径加到系统的Path环境变量中。安装完Node之后用Vue官方脚手架创建项目选用Vue 2.6版本确实在Element UI生态兼容上更省心# 安装vue cli脚手架全局安装一次即可 npm install -g vue/cli # 创建项目选择Vue 2预设Babel和Router必选Vuex看需求 vue create emergency-material-frontend # 进入项目目录安装Element UI和Axios cd emergency-material-frontend npm install element-ui npm install axios这里我要专门提一个新手很容易犯的错误修改npm源为国内镜像。默认npm源在国外安装依赖速度极慢甚至超时失败。通过npm config set registry https://registry.npmmirror.com切换到镜像源后安装速度从十几分钟能降到几十秒。还有npm install的时候尽量不要中途强制中断容易导致依赖安装不完整后续启动直接报各种模块找不到的错误。3.2 路由配置与页面结构设计前端路由是系统页面的骨架我按功能模块划分路由结构通过嵌套路由实现布局管理// 路由配置示例 const routes [ { path: /login, component: Login, }, { path: /, component: Layout, // 主布局包含侧边栏和顶栏 redirect: /dashboard, children: [ { path: dashboard, component: Dashboard, meta: { title: 首页概览 } }, { path: material, component: MaterialList, meta: { title: 物资管理 } }, { path: inbound, component: InboundOrder, meta: { title: 入库管理 } }, { path: outbound, component: OutboundOrder, meta: { title: 出库管理 } }, { path: warning, component: WarningList, meta: { title: 预警管理 } }, ] } ]路由设计上有两个细节一是菜单动态渲染根据登录用户的角色权限过滤可显示的菜单项而不是直接硬编码所有菜单二是路由守卫在router.beforeEach里检查本地是否有Token没有Token却访问非登录页就直接跳转到登录页。这个机制保证了用户无法绕过登录直接访问系统内部页面。3.3 Axios请求封装与API调用规范前端和后端交互的核心是Axios。我习惯在src/api目录下创建一个request.js统一封装利器核心原因是项目里有几十个接口如果每个地方都单独写axios.get(...)一旦需要加公共参数比如统一处理Token或统一错误提示就得改所有代码。封装之后所有请求自动走统一逻辑// request.js 核心封装 const service axios.create({ baseURL: /api, // 请求前缀通过代理转发到后端 timeout: 15000 // 15秒超时 }) // 请求拦截器每次请求自动携带Token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) // 响应拦截器统一处理返回数据和错误 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { // 业务错误统一提示 Message.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { // Token过期清空本地存储并跳转登录页 localStorage.removeItem(token) router.push(/login) } Message.error(网络请求异常) return Promise.reject(error) } )API层的组织结构是按业务模块分文件每个模块导出对应的接口函数。比如src/api/material.js里放物资相关的所有请求方法。这样页面组件里使用起来特别清晰import { getMaterialList, addMaterial } from /api/material可读性和维护性都很好。3.4 核心页面组件开发实战以物资管理列表页为例这是系统里最典型、也最考验细节的页面。主要包含搜索栏物资名称、分类下拉、表格列表物资编码、名称、规格、单位、库存量、状态、操作列编辑、入库、出库、分页组件。Element UI的el-table提供了强大的表格能力配合el-pagination做分页交互和PageHelper的数据格式天然契合。!-- 分页区核心代码 -- el-pagination size-changehandleSizeChange current-changehandleCurrentChange :current-pagequeryParams.pageNum :page-sizes[10, 20, 50, 100] :page-sizequeryParams.pageSize layouttotal, sizes, prev, pager, next, jumper :totaltotal /el-pagination分页参数命名在这里有个隐藏讲究前后端的参数名必须完全一致。前端传pageNum和pageSize后端Controller接收的参数名也必须是pageNum和pageSize否则会出现能查出数据但总条数不对的坑。我调试时经常看到参数名不一致的问题导致分页失效所以建议前端统一用pageNum和pageSize与PageHelper的天然约定保持一致。库存量展示这块我加了一个状态Tag库存低于安全阈值时显示红色库存不足标签高于阈值显示绿色充足标签。这个功能后端其实已经返回了stock_quantity和safe_quantity两个字段前端做一次比较渲染即可但用户体验提升明显仓库管理员一眼就能看到重点。4.1 初始数据库初始化与MySQL环境准备部署的第一步是准备数据库。我在项目中提供了一个完整的init.sql脚本包含建库、建表以及默认数据管理员账号admin/admin123、基础物资分类、若干测试物资。生产环境部署时直接执行这个脚本即可。MySQL的安装部署我踩过不少坑最典型的是Windows环境。安装过程本身跟着安装向导走就行但有两个问题容易卡住MySQL 8.0安装后无法连接报ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock这个错误常见于服务没有启动需要去服务管理里手动启动MySQL服务或者执行net start mysql命令密码认证插件问题MySQL 8.0默认使用caching_sha2_password认证插件一些老版本的客户端工具特别是旧版Navicat连接时会报认证失败需要切换到mysql_native_password插件或者使用支持MySQL 8.0的新版本工具Linux环境部署MySQL相对顺畅一些CentOS系统用yum install mysql-serverUbuntu用apt install mysql-server安装后需要执行systemctl start mysqld启动服务同时用systemctl enable mysqld设置开机自启。4.2 SpringBoot后端打Jar包部署后端部署的核心是打成可执行Jar包运行SpringBoot内嵌Tomcat让这个过程变得非常简洁。在项目根目录执行mvn clean package -DskipTests执行完成后target目录下会生成一个emergency-backend-1.0.0.jar文件。部署时直接上传到服务器运行命令java -jar emergency-backend-1.0.0.jar --spring.profiles.activeprod这里有几个生产环境运行的关键优化参数我整理成了一套完整启动脚本nohup java -Xms512m -Xmx1024m \ -Djava.security.egdfile:/dev/./urandom \ -Dspring.config.location/opt/emergency/config/application-prod.yml \ -jar /opt/emergency/emergency-backend-1.0.0.jar \ /opt/emergency/logs/app.log 21 参数背后的考量-Xms512m -Xmx1024m设置堆内存初始值和最大值防止JVM默认分配过大或过小。整体上给1G内存对这个业务规模完全够用-Djava.security.egdfile:/dev/./urandom解决Linux环境下Java生成随机数阻塞导致启动慢的问题。不加这个参数进程可能卡在启动阶段几十秒甚至更久nohup加让进程在后台持续运行即使关闭SSH终端也不受影响生产配置文件和Jar包分离这样上传新版本时不需要重新修改配置4.3 Vue前端构建与Nginx部署前端部署的核心是构建产物加Web服务器托管。执行构建命令npm run build构建完成后dist目录下就是纯静态文件index.html加一堆带hash的js/css文件。把这些文件上传到服务器的Nginx站点目录配置如下server { listen 80; server_name your.domain.com; # 前端静态资源 root /opt/emergency/dist; index index.html; # 前端路由配置所有路径都指向index.html交给Vue Router处理 location / { try_files $uri $uri/ /index.html; } # 接口反向代理前端请求/api/xxx转发到后端8080端口 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存策略 location ~* \.(js|css|png|jpg|gif|svg)$ { expires 7d; } }这个Nginx配置是整个部署过程的核心它的关键价值在于解决了两个大问题一是解决了前端路由刷新404问题。前后端分离项目直接访问/material这个路径时如果Nginx没有正确配置会尝试寻找服务器上的/material目录导致404。try_files $uri $uri/ /index.html;的做法是如果找不到对应文件就统一返回index.html交给Vue Router来处理前端路由。二是用反向代理规避跨域问题。开发环境前端走Vue的devServer.proxy代理解决跨域生产环境直接由Nginx把/api开头的请求转发到后端8080端口。这样一来前端请求的URL是/api/material/list浏览器认为同源根本不会有跨域请求。这个方案比后端开启CORS配置要优雅得多也不会暴露后端真实端口。/api路径在转发时有一个细节proxy_pass http://127.0.0.1:8080;后面没有带斜杠这意味着Nginx会把完整的/api/xxx路径直接转给后端后端接口的RequestMapping(/api)前缀就能匹配上。如果加了斜杠变成proxy_pass http://127.0.0.1:8080/;/api前缀会被剥掉后端就必须去掉/api前缀才能对上这一点极其容易搞混。4.4 Tomcat部署方式的适用场景SpringBoot项目默认推荐Jar包直接运行但有些场景确实需要传统WAR包部署到独立Tomcat。比如公司已有的运维规范要求所有Java应用统一部署在Tomcat中或者服务器上已经有Tomcat实例想直接复用。改造步骤并不复杂修改打包方式在pom.xml中将packagingjar/packaging改为packagingwar/packaging改造启动类继承SpringBootServletInitializer并重写configure方法SpringBootApplication public class EmergencyApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(EmergencyApplication.class); } public static void main(String[] args) { SpringApplication.run(EmergencyApplication.class, args); } }构建mvn clean package生成的emergency.war放入tomcat/webapps目录启动Tomcat后自动解压部署这里要注意内嵌Tomcat和外部Tomcat的Servlet版本兼容性SpringBoot 2.x对应Tomcat 9。如果用Tomcat 8.5以下版本可能会出现JSP解析或Servlet API冲突的报错。5.1 跨域问题的完整排查与解决方案前后端分离项目最经典的Bug就是跨域。我在开发联调阶段遇到过好几种表现最典型的是浏览器控制台报错Access to XMLHttpRequest has been blocked by CORS policy.解决方案按场景可以分三类开发环境在vue.config.js中配置代理让前端开发服务器把接口请求转发到后端浏览器看到的是同源请求这也是我推荐的首选方案// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }生产环境用Nginx反向代理按前面讲的方式配置不产生跨域后端开启CORS如果前后端分别部署在不同的独立域名下比如前端app.front.com后端api.back.com就必须由后端支持CORSConfiguration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); // 允许所有来源 config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }addAllowedOrigin传*和setAllowCredentials(true)不能同时使用浏览器会直接拒绝这种配置。用addAllowedOriginPattern(*)可以绕开这个限制。5.2 数据库连接池与慢SQL排查这个系统上线运行一段时间后最常见的性能问题是数据库连接池配置不合理导致连接耗尽。Druid连接池的核心配置在application.yml中spring: datasource: druid: initial-size: 5 # 初始连接数 min-idle: 5 # 最小空闲连接 max-active: 20 # 最大活跃连接 max-wait: 60000 # 获取连接最大等待时间60秒 test-while-idle: true test-on-borrow: false validation-query: SELECT 1 # 开启Druid监控页面 stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin123连接池参数不是越大越好。单机部署、业务并发量平时不超过50人的系统max-active设为20完全够用。设太大反而会耗费数据库连接资源一旦请求量上来MySQL默认的max_connections上限很容易被打满。慢SQL排查方面我在MySQL中开启慢查询日志-- 开启慢查询日志阈值2秒 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 2;排到一个典型的慢SQL物资列表查询没有给category_id和material_name建索引当数据量到了几万条时模糊查询的LIKE %关键字%全表扫描速度很慢。解决方案是给常用条件字段加组合索引同时把大字段查询拆出去。数据量大是以后的事但索引设计越早做越好这是我从这个项目经验得到的结论。5.3 XSS攻击防护与系统安全加固安全方面的经验教训也值得记录一下。系统有一个上传操作日志附件的功能测试阶段发现上传的PDF文件里如果包含恶意脚本存在存储型XSS风险。我在后端设计了一个全局过滤器来处理这类问题。Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // 包装请求对用户输入进行转义清理 chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response); } }核心思路是在请求进入Controller之前把所有用户传入的参数包括JSON格式的请求体做HTML转义把script、onerror等危险标签转成普通字符串。这个过滤器还顺便处理了上传文件的ContentType校验——只允许PDF、图片等指定格式防止上传可执行脚本文件。安全加固的完整清单包括密码加密存储使用BCrypt加密而不是明文或MD5SQL注入防护全程使用MyBatis的#{}参数占位禁止使用${}拼接登录防爆破连续5次密码错误后锁定账号30分钟操作日志记录所有的登录、增删改操作都写入系统日志表便于审计追溯5.4 部署后的前端路由与静态资源问题部署上线后最常遇到的问题是前端路由刷新404和后端接口404两类场景前端刷新404表现为访问http://ip:8080/material后点击浏览器刷新按钮页面变白或报404。前面已经提到这是Nginx没有配置try_files导致的按下面的配置就能解决location / { try_files $uri $uri/ /index.html; }后端接口404问题需要区分场景。如果所有接口都404检查Nginx的proxy_pass配置和后端启动是否成功如果只有特定接口404通常是路径匹配问题。我在开发中遇到过一次某个接口在Controller中写的是RequestMapping(/api/material)但前端请求的是/api/material/多了末尾斜杠导致匹配失败。解决办法是不要在URL末尾加斜杠或者在后端配置spring.mvc.servlet.path时统一处理。静态资源加载失败的问题通常是路径写死了。项目里有个同事把图片路径写成了/static/logo.png构建后部署到子路径时图片全部404。最稳妥的做法是将vue.config.js的publicPath设为./并使用相对路径引用资源。5.5 基于Jenkins的自动化部署实践项目稳定后我把它接入了Jenkins自动化部署流水线前端资源更新频繁、后端版本迭代快手动上传部署很容易出错效率也低。流水线按两个阶段设计前端流水线# 安装依赖并构建 npm install --registryhttps://registry.npmmirror.com npm run build # 备份当前dist目录拷贝新构建产物 cp -r dist /opt/emergency/dist_bak rm -rf /opt/emergency/dist cp -r dist /opt/emergency/dist后端流水线mvn clean package -DskipTests # 停掉旧进程启动新进程 kill -9 $(pgrep -f emergency-backend) nohup java -jar target/emergency-backend-1.0.0.jar app.log 21 自动化部署的核心价值是规避人工操作的不确定性。手动部署时经常会漏掉备份环节一旦新包有问题没有回滚方案。Jenkins流水线里加上备份动作之后出问题随时可以恢复上一版本。聊到这里应急物资管理系统从需求设计、前后端开发到部署上线的整个生命周期都过完了。我最后想分享的体会是这类完整源码项目最有价值的不是代码文件本身而是代码背后那一整套设计决策和部署经验。如果你现在能把这个项目从数据库设计开始亲手把每个模块敲一遍再走一遍前后端联调和服务器部署的全流程收获会远超只看不练的效果。还有一个小技巧分享给你做这类项目时给自己做一个部署Checklist把数据库初始化、后端Jar包启动、Nginx配置、防火墙端口这些步骤全部列出来逐一勾选。我在第一次部署时因为忘记放行服务器的8080端口导致后端一直连不上排查了整整两个小时才发现是防火墙的问题。有了Checklist这些低级错误基本不会再犯。祝你顺利把系统跑起来也希望能看到你在这个项目基础上做出更多有价值的扩展。
返回列表