做了这么多年制造业信息化,我一直觉得质量管理系统是中小企业最容易忽略、又最值得投资的一块。前几个月正好把一个“前后端分离的中小型制造企业质量管理系统”从零整理到了可部署状态,技术栈就是最经典的 SpringBoot + Vue + MyBatis + MySQL,整套源码加部署步骤都沉淀下来了。今天就把这套系统的设计思路、核心模块、关键代码、部署实操以及踩过的坑一次性讲清楚。
这篇内容不是那种“演示用的 CRUD 玩具”,而是按真实工厂质量管理流程去设计的:来料检验、过程巡检、成品检验、不合格品处置、质量追溯、统计报表都有落地。适合正在学前后端分离项目的同学参考,也适合小工厂的IT人员直接拿去改改就用。
1. 为什么是质量管理系统,为什么是这套技术栈
1.1 中小制造企业的质量管理到底要管什么
先说业务。中小型制造企业最常见的质量痛点就三个:批次出了问题找不到源头、检验记录靠纸质表格最后堆成山、月底统计合格率全靠Excel手工拉数据。一套质量管理系统要解决的,就是把“检验—记录—判定—处置—追溯—统计”这条链跑通。
拆开来看,核心业务大概这六个板块:
- 来料检验(IQC):原材料到货后,质检员按检验标准抽检,记录供应商、到货批次、抽检数量、不合格数量、检验结论。
- 过程检验(IPQC):生产过程中的首件检验、巡检记录,出现异常能及时拦截,不让不良品流到下一道工序。
- 成品检验(FQC/OQC):产线完工后、入库或发货前的最终检验,判定合格、不合格或者让步接收。
- 不合格品管理:对判退、返工、返修、报废、让步接收的产品做登记,记录处置方式和原因分析。
- 质量追溯:通过生产批号、订单号反向查原料批次、检验记录、操作人员,一旦客诉能在最短时间内定位问题范围。
- 统计报表:按产品、按供应商、按日期维度统计来料合格率、过程合格率、成品合格率、缺陷类型分布,给管理者做改进决策。
如果你接触过工厂的质检流程就会明白,这些模块互相之间是有依赖的:来料检验不录好,后面的过程追溯就断链;不合格品不记录原因,统计报表做了也是空的。所以数据模型的关联设计比页面数量重要得多。
1.2 技术选型:SpringBoot + Vue + MyBatis + MySQL 的理由
先说后端。SpringBoot 在 Java 生态里几乎是中小型管理系统的事实标准,它的一大好处是“约定大于配置”,你不需要像以前 SSM 时代那样写一堆 XML 配置,起步快,社区资料也最全。MyBatis 作为持久层框架,相比 JPA/Hibernate 更轻、更可控,尤其是质量管理这类报表查询多、多表关联多、SQL 经常要针对特殊统计逻辑定制的场景,手写 SQL 反而比 ORM 自动生成更放心。
前端选择 Vue 而不是 React,对于很多从后台管理系统入手的团队来说更平滑。Vue 的单文件组件、指令系统、双向绑定,学习曲线相对平缓,而且 Element Plus 这类组件库对表格、表单、弹窗这些后台高频场景覆盖得很到位,两三天就能把一套带增删改查的页面搭出来。
MySQL 更不用多说,中小制造企业的数据量级也就百万条以内,单机 MySQL 8 完全扛得住,稳定性和运维成本都合适。这套组合在“开发效率、运行性能、招人难度”三者之间找到了一个最适合中小团队的平衡点。
有人会问:为什么不用若依框架直接改?若依确实是很好的脚手架,但它是通用后台管理系统,带了大量和工作流相关的功能,对只想专注质量业务、想彻底搞懂每一行代码的开发者来说,反而有点重。这个项目选择自己搭基础权限和登录,核心模块全部围绕质量业务定制,学习成本更低,二次开发也更灵活。
1.3 前后端分离的取舍
前后端分离是这个项目最明显的一个架构特征。前端 Vue 项目独立开发、独立构建,通过 HTTP 接口和 SpringBoot 后端通信,两边不共享会话。
分离带来的最大好处有两个:一是前端开发和后端开发可以并行推进,只要提前把接口文档约定好;二是部署弹性更好,前端静态文件交给 Nginx 托管,后端作为一个 Java 进程独立跑,任何一边要升级都不需要停整体服务。
当然代价也清楚:跨域问题躲不掉、Token 鉴权得自己搞、前端打包后刷新 404 这些“分离病”都要处理。这些坑我在后面部署章节会逐个给解决方案。对于想系统学习前后端分离项目套路的人来说,这套系统倒是绝佳的练手样本。
2. 从业务需求到表结构:质量系统的骨架设计
2.1 六大核心模块与业务闭环
系统模块划分不建议按传统软件开发那样单纯按功能拆,而是按“一条质量业务流”来拆:基础数据维护(物料、供应商、客户、检验标准)→ 检验记录录入(来料/过程/成品)→ 不合格品处理 → 质量追溯 → 统计报表 → 用户权限管理贯穿全局。
这样设计后,哪怕是一个只做过几个页面的新手,也能一眼看出数据是怎么流动的:检验员录完一张来料检验单,数据落到检验记录表;这张单子如果判不合格,系统自动生成一条不合格品待处理记录,质量主管在用户界面进行处置登记;处置完成后,追溯模块就能以供应商、物料、批次为线索把整条链串起来;月底统计报表从这些表里聚合数据生成合格率趋势图和缺陷分布图。
必须注意的一个设计原则是:核心业务表必须保留“业务状态”字段,比如检验单的状态可以设计为草稿、已提交、已审核。中小企业最容易出现的情况是质检员录错了想要改,如果只有一条业务记录没有状态流转,改数据就没有依据。加上状态字段后,草稿状态随便改、已提交状态要主管审核才能改,这个约束在质量管理体系审核里会非常有用。
2.2 数据库表结构与关键字段设计
数据库命名用的前缀区分模块:sys_(系统管理)、base_(基础资料)、qc_(质量检验)、trace_(追溯)。下面这张表是核心表清单,都是我实测后整理出来的设计,字段做了精简展示:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 用户表 | id, username, password, real_name, dept_id, status |
| sys_role | 角色表 | id, role_code, role_name |
| sys_user_role | 用户角色关联 | user_id, role_id |
| base_supplier | 供应商表 | id, supplier_code, supplier_name, contact_person, phone, status |
| base_material | 物料表 | id, material_code, material_name, spec, unit, default_check_plan |
| qc_check_item | 检验项目表 | id, item_code, item_name, item_type, upper_limit, lower_limit, unit |
| qc_incoming_inspection | 来料检验主表 | id, inspection_no, supplier_id, material_id, batch_no, arrival_date, inspector, sample_qty, qualified_qty, unqualified_qty, conclusion, status |
| qc_incoming_inspection_detail | 来料检验明细表 | id, inspection_id, check_item_id, check_value, check_result, remark |
| qc_process_inspection | 过程检验表 | id, inspection_no, work_order_no, process_name, product_id, inspection_type, inspector, result, status |
| qc_product_inspection | 成品检验表 | id, inspection_no, product_id, batch_no, order_no, inspector, qualified_qty, unqualified_qty, conclusion, status |
| qc_nonconforming | 不合格品记录表 | id, source_type, source_no, product_id, batch_no, defect_code, defect_desc, disposition, responsible_dept, handle_result, handle_user |
| trace_batch | 批号追溯表 | id, batch_no, material_id, product_id, supplier_id, work_order_no, related_order_no, create_time |
有几个字段我特别强调一下。
batch_no(批号)是整个追溯链的核心,所有检验记录和流转记录都要保留这个字段。批号规则最好在上线前就定死,比如“日期+产线+班次+流水号”,例如20250610-A-01-001,不要等录了几千条数据再改规则,那时候改数据会改到怀疑人生。
inspection_no(检验单号)建议使用独立序列,不要依赖自增主键。自增主键在并发高的时候拿不到“下一个号”,而且单号会暴露数据量。我用的是“前缀 + 年月日 + 随机三位/序列”,比如IQC20250610001,在代码里加一个synchronized方法生成,足够中小企业的并发量。
conclusion字段采用字典值(PASS/FAIL/RETURN),不要直接存中文。
qc_incoming_inspection_detail这种明细表不追求“录入快”,但检测值和判定结果一定要分开存,因为后面统计“哪个检验项最容易不合格”完全依赖明细表的数据。
2.3 前后端接口约定与统一返回体
前后端分离项目最容易乱的就是接口。我的做法是规定所有接口走/api前缀,Controller 里全部返回一个统一的Result<T>对象,结构是code(200成功、401未登录、500服务异常)、msg、data三件套。
后端这样定义:
public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.setCode(500); r.setMsg(msg); return r; } }前端所有请求都统一经过封装好的request工具,只处理Result结构。这样前后端对接的时候,接口列表只需要看 Controller 的方法名和请求方式,不用各写一套文档。
分页接口也统一返回PageResult<T>,包含records、total、current、size四个固定字段。分页逻辑我使用了 PageHelper 插件,PageHelper.startPage(page, size)后面紧跟查询方法即可,插件自动拦截 SQL 生成 limit 语句和 count 查询,对中小项目来说省事又可靠。
3. 后端实现:SpringBoot + MyBatis 的核心代码笔记
3.1 项目结构搭建与基础配置
后端工程我用 IDEA 创建,Spring Boot 版本选择 2.7.18,JDK 用的 1.8。这里有一个经验:不要一上来就选最新的 Spring Boot 3.x。Spring Boot 3 把javax.servlet换成jakarta.servlet,很多网上的旧教程代码直接拿来用会报ClassNotFoundException,MyBatis 相关依赖也要跟着升版本,排查起来很费时间。除非你没有历史包袱,否则用 2.7.x 是最稳的组合。
核心依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>注意mysql-connector-j这个是新版坐标,旧教程里的mysql-connector-java坐标虽然还能用,但官方已经把它归并进来了。对应驱动类也要用com.mysql.cj.jdbc.Driver。
application.yml的关键配置:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/qms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.qms.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case必须开,数据表的batch_no才能自动映射到 Java 实体类的batchNo,不然每个查询都要手写resultMap,工作量翻倍。serverTimezone=Asia/Shanghai和useSSL=false是 MySQL 8 时代最常见的两个坑,不写时区在 JDBC 连接时报错,不关 SSL 虽然能连但控制台会刷一大段警告。
3.2 登录鉴权与JWT Token处理
前后端分离项目不能依赖 Session,我用 JWT 做登录鉴权。流程很简单:用户提交用户名密码,后端校验通过后生成一个 token 返回给前端;前端把 token 存到 localStorage,每次请求在 Header 里带Authorization: Bearer <token>;后端用一个拦截器统一校验。
JWT 工具类核心逻辑:
public class JwtUtil { private static final String SECRET = "qms-secret-key-please-change-in-production"; private static final long EXPIRE = 1000 * 60 * 60 * 12; public static String createToken(Integer userId, String username) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器里需要把/api/login放行,其余接口都校验 token。校验失败统一返回 401,前端收到 401 就自动跳到登录页。这部分值得细说,因为很多初学者把 token 校验写在每个 Controller 里,代码重复又容易漏。拦截器一注册,所有接口自动生效:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/captcha"); } }3.3 检验单模块的完整实现思路
拿来料检验单举例,这个模块包含主表加明细表两张表。保存时要保证事务:主表插入后拿到自增主键,再把明细列表逐条插入,任何一个明细失败都要回滚。
Service 层核心代码:
@Transactional(rollbackFor = Exception.class) public void createInspection(IncomingInspectionDTO dto) { QcIncomingInspection master = new QcIncomingInspection(); BeanUtils.copyProperties(dto, master); master.setInspectionNo(generateInspectionNo()); master.setStatus("SUBMITTED"); incomingInspectionMapper.insert(master); for (InspectionDetailDTO item : dto.getItems()) { InspectionDetail detail = new InspectionDetail(); detail.setInspectionId(master.getId()); detail.setCheckItemId(item.getCheckItemId()); detail.setCheckValue(item.getCheckValue()); detail.setCheckResult(judge(item.getCheckItemId(), item.getCheckValue())); detailMapper.insert(detail); } }判定逻辑judge()是调检验项目表里配置的上限下限去比的,这样换产品、换标准时不用改代码,改数据就行。真实工厂里检验项标准经常变,把标准配置化是这套系统能用下去的一个前提。
查询列表时用联表查询,把检验单主表关联供应商表和物料表,PageHelper 分页,代码如下:
<select id="pageQuery" resultType="com.example.qms.entity.vo.IncomingInspectionVO"> SELECT t.*, s.supplier_name, m.material_name FROM qc_incoming_inspection t LEFT JOIN base_supplier s ON t.supplier_id = s.id LEFT JOIN base_material m ON t.material_id = m.id <where> <if test="batchNo != null and batchNo != ''"> AND t.batch_no LIKE CONCAT('%', #{batchNo}, '%') </if> <if test="supplierId != null"> AND t.supplier_id = #{supplierId} </if> </where> ORDER BY t.create_time DESC </select><if>标签里的动态 SQL 是 MyBatis 最有价值的地方,条件判断自动拼接,不需要为“有筛选条件/无筛选条件”写两套 SQL。但注意文本比较要写and batchNo != '',不能只写and batchNo != null。
3.4 MyBatis 使用中的几个经验点
第一个经验是批量插入。质量检验明细通常一次几十上百条,循环逐条 insert 性能差也麻烦。用 MyBatis 的<foreach>一次拼一条批量 INSERT 最方便:
<insert id="batchInsert" parameterType="list"> INSERT INTO qc_incoming_inspection_detail (inspection_id, check_item_id, check_value, check_result, remark) VALUES <foreach collection="list" item="item" separator=","> (#{item.inspectionId}, #{item.checkItemId}, #{item.checkValue}, #{item.checkResult}, #{item.remark}) </foreach> </insert>第二个经验是只要遇到多表联查或者报表统计,一定要先写 SQL 到数据库客户端里跑通,再往 Mapper XML 里搬。直接在项目里调试 SQL 效率太低,报错信息也不直观。我在 IDEA 里装了 MyBatis Log Plugin 插件,把 MyBatis 生成的完整 SQL 打印出来看,控制台输出这个方便:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl第三个经验是关于缓存的。MyBatis 一级缓存默认开启,范围是一个 SqlSession,在同一个 SqlSession 内连续查两次相同 SQL 会命中缓存。一级缓存我觉得不用管它,反而是二级缓存要小心:它默认关闭,如果在多表联查的 Mapper 上开启,一旦关联的其他表数据变了,缓存不会自动失效,很容易查出脏数据。质量管理系统这种查询条件复杂、数据实时性要求又高的项目,二级缓存建议直接保持默认关闭,性能不够再加 Redis。
4. 前端实现:Vue 项目与接口联调
4.1 项目初始化与目录规划
前端用的是 Vue 3 + Vite + Element Plus + Pinia + Vue Router。创建项目不要太老套地拿 Vue CLI,现在初始化项目直接用:
npm create vite@latest qms-web -- --template vue然后安装依赖:
cd qms-web npm install npm install vue-router@4 pinia axios element-plusNode 版本建议 16.13 以上,实测 Vite 5 在 Node 14 下会直接报错。npm 安装依赖如果慢,可以配置镜像源npm config set registry https://registry.npmmirror.com,这属于常规操作,能省下很多等待时间。
目录规划上,我的习惯是按“模块”划分而不是按“文件类型”堆:
src/ ├── api/ # 接口请求统一放这里 │ ├── login.js │ ├── incoming.js │ └── report.js ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── layout/ # 主框架布局 ├── router/ # 路由配置 ├── store/ # Pinia 状态 ├── views/ # 页面 │ ├── login/ │ ├── quality/incoming/ │ ├── quality/process/ │ ├── quality/product/ │ ├── quality/nonconforming/ │ └── report/ └── utils/ # 工具函数、axios封装api目录下每个文件导出对应模块的接口函数,页面里只调用函数、不直接写 axios,这样后端接口地址调整时只改一个文件。
4.2 axios 封装、Token 处理与路由守卫
请求封装是整个前端联调的核心。直接上代码:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) request.interceptors.response.use( res => { const data = res.data if (data.code === 200) return data if (data.code === 401) { localStorage.removeItem('token') router.push('/login') ElMessage.warning('登录已过期,请重新登录') return Promise.reject(new Error('unauthorized')) } ElMessage.error(data.msg || '请求失败') return Promise.reject(new Error(data.msg)) }, err => { ElMessage.error('网络异常,请检查后端服务') return Promise.reject(err) } ) export default request这里强烈建议在baseURL写/api,而不是写死http://localhost:8080/api。开发时靠 Vite 代理把/api转发到后端,生产时靠 Nginx 反向代理同样转发,代码仓库里不需要因为环境不同而去改接口地址。很多新手项目写死 localhost,换一台机器部署就一脸懵,就因为这里没有处理好。
路由守卫逻辑要兜底未登录的页面访问:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() return } if (!token) { next('/login') return } next() })这部分配合后端 JWT 拦截器,就构成了一套完整的鉴权闭环:前端没 token 进不去页面,后端没 token 或 token 过期接口返回 401,前端收到 401 自动清 token 回登录页。
4.3 检验录入、分页查询与报表展示
检验录入页面是质检员每天用最多的页面,设计上最重要的不是炫酷,而是“少点几下鼠标”。我的做法是主表字段放顶部表单,明细项用可动态增删的表格,填写完一行自动追加一行空行,最后一次性提交。
分页查询用 Element Plus 的 el-table 加 el-pagination,请求page参数变化时重新加载数据:
<el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.size" :total="total" layout="total, sizes, prev, pager, next" @change="loadData" />报表模块用 ECharts 画折线图和柱状图展示合格率趋势、缺陷分布。图表数据接口由后端 SQL 聚合,比如按月份统计合格率,SQL 大概长这样:
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS total_count, SUM(CASE WHEN conclusion = 'PASS' THEN 1 ELSE 0 END) AS pass_count FROM qc_product_inspection WHERE create_time >= #{startDate} GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month前端图表只负责展示,复杂的统计尽量下沉到 SQL,不要让前端去把原始数据再加工一遍,不然数据量大了页面会卡。
5. 部署实操:从源码到可访问的系统
5.1 环境准备:JDK、MySQL、Node、Nginx
后端运行环境需要 JDK 1.8 + Maven 3.6+,前端构建需要 Node.js 16+,数据库用 MySQL 8。
MySQL 的安装我建议用免安装版 zip 包,最适合服务器环境:解压后修改my.ini,然后执行mysqld --initialize-insecure初始化(root 默认空密码),再net start mysql启动服务。比起安装版,免安装版没有图形化向导干扰,路径自己可控,出了问题也好排查。安装版向导有时候装完找不到配置文件,反而不好处理。
Java 后端部署直接打 jar 包运行,这是 Spring Boot 最舒服的部署方式,不需要装 Tomcat:
mvn clean package -DskipTests java -jar target/qms-system.jar --spring.profiles.active=prodNginx 用来托管前端静态文件和做反向代理,安装好后目录结构大概是/usr/share/nginx/html放前端 dist 内容。
5.2 数据库初始化与配置文件调整
源码包里的sql/qms_init.sql是完整初始化脚本,包含建库、建表、初始账号数据。首次部署先执行:
mysql -u root -p < qms_init.sql默认初始账号是admin / admin123,首次登录后建议立刻改密码。生产环境配置建议把账号密码、数据库地址放到application-prod.yml里,用启动参数指定,避免把生产密码写进源码仓库。
5.3 后端打包与启动
打包前重点关注三件事:一是依赖能不能正常拉到私服,二是测试类会不会因为连不上数据库而报错,三是application-prod.yml里的数据库密码是否已改。
启动命令建议用 nohup:
nohup java -jar qms-system.jar --spring.profiles.active=prod > logs/qms.log 2>&1 &日志文件独立存放,后面出问题有据可查。启动后用curl http://localhost:8080/api/login验证后端是否正常。
常见启动失败原因无非端口被占用(netstat -ano | findstr 8080查一下,或者改 server.port)、数据库连接不上(检查地址、账号、密码、MySQL 服务状态)、驱动类找不到(检查依赖坐标)。这三个排查掉,后端一定能起来。
5.4 前端构建与 Nginx 配置
前端构建:
npm install npm run build构建产物在dist目录。把dist里的文件上传到服务器 Nginx 的 html 目录。Nginx 配置是整个前后端分离部署最关键的地方,提供一份可用的配置:
server { listen 80; server_name localhost; root /usr/share/nginx/html; index 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }location /api/把所有后端接口反向代理到 SpringBoot 进程,浏览器里所有请求都走同一个域名,彻底消除跨域问题。try_files $uri $uri/ /index.html是 Vue Router 的 history 模式刷新 404 的解决方案,如果不需要保留 url 语义,也可以在代码里改成 hash 模式,连这行都不用配。
5.5 前后端联调与访问验证
部署完成后验证流程:浏览器打开http://服务器IP→ 跳登录页 → 用 admin 登录 → 进检验录入页面新增一条来料检验单 → 提交后端 → 在列表页看到数据 → 打开报表页看统计数据。全流程通了,这套系统就算正式跑起来了。
这里提醒一个很多人会忽略的问题:云服务器安全组、服务器自带防火墙都要放行 80 端口。以前帮朋友排查过很多次,代码完全没问题,就是访问不了,最后发现是安全组没开端口。检查顺序:先本机curl,再局域网 IP 访问,最后再考虑公网映射。
6. 常见问题与排查技巧实录
6.1 开发环境跨域与Vite代理配置
开发时前端和后端端口不同,第一道坎就是跨域。解法不是在后端加@CrossOrigin或者写一堆 CORS 配置类的代码,那只是治标。最干净的方式是用 Vite 代理:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样前端代码里的/api/login在开发时会被 Vite 服务转发到localhost:8080,浏览器认为请求发给了同源的前端服务器,跨域问题根本不存在。后端不需要额外配 CORS,生产环境也不依赖它。
6.2 Vue 打包后的两个经典问题
第一个是打包后页面白屏、资源 404。大多数情况是静态资源路径用了绝对路径/assets/xxx,部署在子路径下就找不到。解决办法是在vite.config.js里设置base: './',让资源路径变成相对路径。
第二个是打包后刷新页面 404。如果前端路由用了 history 模式,刷新时请求会发给 Nginx,Nginx 找不到对应文件就回 404。前面 Nginx 配置里的try_files ... /index.html就是针对这个问题;如果实在不想折腾 Nginx,把路由模式改成 hash 也能解决,URL 会多个#/,对内部管理系统完全不影响使用。另外"打包后布局异常"这个问题,多半是父容器没有约束宽度,或者某台机器浏览器版本过旧导致现代 CSS 特性没生效,先检查启动时有没有报错,再逐级看样式。
6.3 MySQL 8 连接与账号授权问题
MySQL 8 用caching_sha2_password作为默认认证插件,老的 MySQL 驱动和部分客户端工具会连不上、报Public Key Retrieval is not allowed。解决方法是 JDBC URL 加上allowPublicKeyRetrieval=true&useSSL=false,同时在授权时也可以考虑创建使用mysql_native_password插件的账号。
还有一个小坑是 MySQL 8 默认只在本机监听 3306,如果后端部署在其他服务器,需要检查bind-address配置,并创建一个允许远程访问的账号:
CREATE USER 'qms'@'%' IDENTIFIED BY 'qms123456'; GRANT ALL PRIVILEGES ON qms.* TO 'qms'@'%'; FLUSH PRIVILEGES;6.4 MyBatis 动态 SQL 的隐藏坑
热词里有一条“mybatis 单个数字字符比较”,这个我遇到过,说出来都是泪。在<if test="type == '1'.toString()">这种写法里,直接把单个字符放到==上比较,很可能不生效。原因是 MyBatis 的 OGNL 表达式解析对'1'会当作char类型处理,和字符串"1"比较时结果不可预期。以后写这种条件判断,要么写成下面这样:
<if test="type != null and type == '1'.toString()">要么在 Java 代码里先处理好,再传一个布尔值进来。避免在 XML 里写单个字符的等值判断,就少踩一个坑。
6.5 Spring Boot 版本太高引发的连锁反应
我在 3.1 节就提醒过版本问题,这里再展开。Spring Boot 3.x 是一次大版本跃迁,最明显的是javax包名全面替换为jakarta。如果你在网上拷贝的代码里有import javax.servlet.*,在 Spring Boot 3 项目里直接编译不通过。同时 MyBatis 的 starter 版本、PageHelper 版本、各种工具类都要选适配 Spring Boot 3 的版本,误配低版本会出现奇怪的启动异常。这套质量系统的源码是按 Spring Boot 2.7.x 编写的,用户直接用配套依赖即可,如果你对 Spring Boot 3 的差异不熟悉,不建议贸然升级。
写在最后
再分享一点实际使用中的经验。质量管理系统上线后能不能被工厂真正用起来,关键不在功能列表有多全,而在录入是否高效、报表是否看得懂。建议先从来料检验和成品检验两个模块推起,跑顺了再加过程检验和不合格品管理,不要想一口吃成胖子。质检员一天要录很多单据,交互上一定要尽量少点几次鼠标,明细项能默认就默认,检验结论能自动判定就自动判定。
批号的规则一定要在上线前和车间、仓库、生管几个部门对齐,这是整个追溯体系的命脉。还有就是定期备份数据库,中小工厂往往没有专职DBA,我用的是一个简单的定时任务每天夜里mysqldump一次,出过一次事故的人会懂这件事有多重要。
整套系统从设计到落地,前后花了大概三周时间。单一技术栈并不是什么新鲜东西,但把它组合成一个贴合制造业质量管理场景的完整闭环,再整理成一套可部署的源码和文档,这件事本身就是价值。如果在部署或二次开发中遇到问题,欢迎按项目文档里的思路先自查,很多问题都能在配置层面找到答案。