这次聊的项目,是一套完整的前后端分离源码:Java SpringBoot + Vue3 + MyBatis + MySQL 的扶贫助农系统。标题里“系统系统”是重复了,但不影响项目本身的价值,它是能直接跑起来、而且业务闭环比较完整的真实项目。这个系统解决什么问题?简单说就是农产品产销对接的数字化:农户发布农产品,村级管理员审核,运营人员在后台管理商品和订单,帮扶干部看帮扶数据,采购方直接下单采购,整个链路用一套系统串起来。
对想练全栈的同学、准备Java后端面试的同行,以及需要快速交付毕业设计或课程设计的人来说,这个项目的参考价值都比较大。它不是教学demo那种“图书管理系统”,而是模块划分、接口设计、数据表结构都能直接落地到生产环境的工程。我在实际开发中踩过的坑、验证过的方案,都会在下面聊到,尽量把每个“为什么这么做”讲透。
这个系统的技术选型非常典型,基本就是国内中小团队做业务系统的标准组合。接下来我从整体设计、数据库建模、后端实现、Vue3前端、联调部署与排错这五个角度,把这个项目完整拆开。
1. 项目整体设计与技术选型
1.1 扶贫助农系统的业务场景与核心矛盾
先把业务理解透,再谈技术。扶贫助农系统不是普通电商商城,它的核心矛盾在三个环节:供给侧、需求侧、管理侧。供给侧是农户,特点是分散、信息化程度低,上传一条农产品信息可能都需要别人帮忙;需求侧是采购商和城市消费者,他们关心产品是否真实、价格是否合理、发货是否靠谱;管理侧是村级管理员和帮扶干部,他们需要审核信息、保证数据的准确性,还要能拿出统计数据来评估帮扶效果。
所以这个系统的角色不能简单做成“用户/管理员”两档。实际项目里我把用户分成农户、村级管理员、运营人员、帮扶干部四类,四类人在同一套系统里干不同的事。农户管自己的农产品发布和订单;村级管理员审核所属区域的农户信息;运营人员处理商品上下架和异常订单;帮扶干部不直接操作业务,主要看数据和跟进情况。这个角色划分直接影响数据库设计、接口权限设计和前端菜单设计,可以说是整个项目的地基。
1.2 技术选型:为什么是SpringBoot + Vue3 + MyBatis + MySQL
后端用SpringBoot没什么悬念,生态成熟、上手门槛低,配合各种starter,连依赖版本都不用自己操心。实战中我一般用Spring Initializr生成基础工程,把web、validation、mybatis相关依赖勾上,几分钟就能得到一个可以开始写接口的骨架。版本上不追求最新,JDK 8 + Spring Boot 2.7.x反而是最稳的组合,真正在生产跑的项目,很多还在JDK 8上,升级JDK 17的团队其实不多。
Vue3的选择理由也很直接。Vue2已经停止维护,新项目没必要从旧语法开始;Vue3的Composition API对中后台这种大量“数据联动、接口调用、逻辑复用”的场景非常友好。配合Element Plus的表格、表单、弹窗组件,后台管理系统的基本交互很快就能搭出来。项目用了TypeScript,我建议加上,接口返回的数据结构复杂时,类型定义能挡掉很多低级错误。
MyBatis这里值得多说两句。有人会问,既然用了SpringBoot,为什么不用JPA?我做业务系统更习惯MyBatis,原因就一个:SQL可控。这个系统里有大量多表关联查询、动态条件筛选、聚合统计,用MyBatis的XML写出来,每一行SQL心里都有数。虽然Mapper接口和XML文件会让代码量比JPA多一些,但在调性能、排查问题的时候,这种“笨”反而是优势。而且MyBatis是Java面试高频考点,把动态SQL和缓存机制吃透,面试时能聊的东西很多。
MySQL是数据层最稳妥的选择。这个项目的数据模型是强关联的:用户要下单、订单要关联商品、商品要归属农户、统计数据要跨多张表聚合,这种场景天然适合关系型数据库,事务机制能保证库存扣减和订单状态的一致性。数据库版本建议8.0,字符集统一用utf8mb4,否则emoji和生僻字入库会变成乱码。MySQL在Windows或Linux上的安装其实都不难,注意选对版本就行,5.7和8.0的安装过程略有差异,8.0默认的认证插件对老客户端不太友好,这一点后文部署部分会详细说。
1.3 前后端分离的架构边界
前后端分离不是简单把页面和后端代码拆开,关键是职责边界要清楚。前端只负责渲染和交互,不碰业务规则;后端只提供接口和数据,不产生任何HTML页面。开发时我是这么分工的:前端用Vite起一个开发服务器,所有请求通过代理转发到后端8080端口;后端专注写Controller和Service。这样两个开发环境完全独立,改前端不用重启后端,改后端不用管浏览器缓存。
接口约定用一个简单的JSON格式作为统一返回:code、msg、data。成功返回code=200,失败返回非200并附带msg。前端根据code判断业务成功与否,HTTP状态码只用来表达“请求本身”的问题,比如404、500、401。这个约定虽然简单,但比“每个接口返回结构都不一样”要好维护得多。接口文档我建议用Apifox或类似工具管理,请求示例和返回结构都固化下来,前后端并行开发时能减少大量沟通成本。
2. 核心功能模块梳理与数据库建模
2.1 用户体系与角色权限设计
用户体系是整个系统的地基。建表时用了t_user一张表,字段包括username、password、real_name、role、phone、status。role用tinyint存,0是农户,1是村级管理员,2是运营人员,3是帮扶干部。权限控制没有引入Spring Security那套复杂体系,而是在拦截器里判断角色,因为业务本身是后台管理系统,接口数量几十个,引入厚重框架反而增加学习成本。
密码存储必须用BCrypt加密,绝不能明文存库。这里有个细节:很多教程用MD5加密,MD5虽然简单,但在当前算力下已经不够安全,加盐的BCrypt是更稳的选择。Spring Security的crypto模块自带BCryptPasswordEncoder,只引入这一个类就能用,不需要引入整套Security。登录时用matches方法校验密码,千万不要把密码写到日志里,更不要在后端返回数据时把password字段暴露给前端。
2.2 农产品管理:状态机与审核流
农产品表是这个系统最核心的业务表,字段大致包括name、category_id、origin(产地)、price、stock、image_url、description、status、farmer_id、create_time。整体建表SQL大概长这样:
CREATE TABLE `t_product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `category_id` int(11) DEFAULT NULL COMMENT '分类ID', `origin` varchar(100) DEFAULT NULL COMMENT '产地', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int(11) DEFAULT 0 COMMENT '库存', `image_url` varchar(255) DEFAULT NULL COMMENT '主图地址', `description` text COMMENT '商品描述', `status` tinyint(4) DEFAULT 0 COMMENT '0-待审核 1-上架 2-下架 3-审核拒绝', `farmer_id` bigint(20) DEFAULT NULL COMMENT '发布农户ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_farmer` (`farmer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='农产品表';这个表里有两个设计点值得说。第一是status字段,产品要经历“待审核 -> 上架 -> 下架”的生命周期,再加上“审核拒绝”这个终态。为什么需要审核?因为农户自己发布信息很容易出现图片错拍、定价明显离谱、重复铺货的情况,如果直接上架,后面的订单、物流、售后都会乱。所以状态机里农户只能看到自己的草稿和上架商品,管理员看到的是待审核列表,审核通过才允许对外销售。
第二是冗余设计。订单里下单时的商品价格和名称,一定要在订单表里留一份快照,而不是下单时实时去关联产品表查。农产品价格波动快,如果买家下单后农户改了价,订单里的金额跟着变,那就乱了。我在t_order表里直接冗余了product_name和product_price,下单那一刻把商品信息拷贝一份,后续商品怎么改都不影响历史订单。这个经验在真实业务里非常实用,教科书里不会讲到。
2.3 订单与帮扶对接:核心交易链
订单表的核心字段包括order_no、product_id、buyer_id、quantity、amount、status、address、create_time。order_no用唯一索引,业务上通常不用自增id直接作为订单号给用户看,一个是太长不好记,另一个是自增id容易暴露销量,所以我用时间戳加随机数生成一个32位以内的字符串订单号。
CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `product_id` bigint(20) DEFAULT NULL COMMENT '商品ID', `product_name` varchar(100) DEFAULT NULL COMMENT '下单时商品名称快照', `product_price` decimal(10,2) DEFAULT NULL COMMENT '下单时商品价格快照', `buyer_id` bigint(20) DEFAULT NULL COMMENT '买家ID', `quantity` int(11) DEFAULT 1 COMMENT '数量', `amount` decimal(10,2) DEFAULT NULL COMMENT '订单金额', `status` tinyint(4) DEFAULT 0 COMMENT '0-待支付 1-已支付 2-待发货 3-已发货 4-已签收 5-已取消', `address` varchar(255) DEFAULT NULL COMMENT '收货地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';库存扣减是订单模块最容易出bug的地方。农户的库存就是农产品数量,下单时必须同时判断库存是否够用,然后扣减,这个操作必须在事务里做,防止并发超卖。最简单可靠的方式是SQL语句里带上条件:UPDATE t_product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity},如果影响行数为0就说明库存不足。不要先查出来再判断,先查再更新在高并发下必然有缝隙。
帮扶对接是这个项目区别于普通商城的核心模块。思路是这样的:农户除了直接卖商品,还可以发布供应信息,比如“本村有1万斤土豆待销”;帮扶干部看到对应区域的供应信息,可以帮农户对接采购渠道。我在数据库里加了一张supply_demand表,字段包括type(供应还是需求)、title、content、contact、status、create_user_id。这个机制让平台从“单向卖货”变成了“产销撮合”,对助农场景来说,比硬套一个电商商城要贴切很多。
2.4 帮扶数据统计:聚合查询的写法
统计是管理侧的刚需,要看“本月帮扶了多少农户、卖了多少货、涉及哪个品类、哪个区域销售最高”。数据统计接口我直接用MySQL的聚合查询,按地区、品类、时间维度GROUP BY,一次查询返回汇总结果,前端用表格或柱状图展示。
这里要提醒一句,统计查询不要做多张大表的笛卡尔积关联,尤其不要对text类型的description字段做GROUP BY。我在统计SQL里只查维度字段和聚合字段,需要明细的时候再单开接口。数据量大起来以后,可以考虑建一张统计汇总表,每天定时把前一天的数据算好存进去,页面只查汇总表。这个方案在生产里非常实用,等数据到百万级再想优化就晚了。
3. 后端接口与MyBatis实战细节
3.1 项目骨架与统一返回体
后端包结构我习惯按controller、service、mapper、entity、dto、vo来分,尽量做到一个实体对应一个Mapper、一个Controller,复杂业务在Service层聚合。不要把业务逻辑堆到Controller里,Controller只做参数接收、调用Service、返回结果,这样每个类都短小,排查问题的时候能很快定位。
统一返回体优先级很高。如果每个接口返回结构不一样,前端写Axios封装时会疯掉。我用的是R 泛型类,code、msg、data三个字段:
@Data public class R<T> { private Integer code; private String msg; private T data; public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> R<T> fail(String msg) { R<T> r = new R<>(); r.setCode(500); r.setMsg(msg); return r; } }所有Controller方法统一返回这个类型,分页数据也放在data里,格式固定是{ list, total }。前端拿到之后直接解构,不需要对每个接口单独写判断逻辑。
全局异常处理建议一开始就配好。用@RestControllerAdvice配合@ExceptionHandler,把参数校验异常、业务异常、兜底异常分别处理,统一返回R.fail。如果没有这一层,数据库报错、空指针异常会直接以很丑的堆栈形式返回给前端,既暴露内部逻辑,用户也看不懂。前端拿到统一格式的失败提示,才好弹出友好提示。
3.2 分页查询:PageHelper还是手写Limit
分页是后台管理系统的常规操作,我直接用了PageHelper插件。用法很简单,在查询前调用PageHelper.startPage(page, pageSize),紧接着执行Mapper查询,插件会自动拼接limit并生成count查询,返回PageInfo对象,里面带了total等分页信息。分页参数这部分,后端Service里最好都统一用page和pageSize两个参数接收,不要有的接口用current、有的用limit,前端对接会非常痛苦。
用PageHelper有两个坑务必记住。第一个,startPage之后的第一个查询会被分页,如果中间插了别的查询,分页会作用到错误的SQL上,所以startPage必须紧跟目标查询,中间别打印日志、别做其他数据库操作。第二个,分页插件默认会执行一次count查询,如果原SQL很复杂,count也会很慢,这时候可以手写count SQL优化,或者干脆分页直接手写limit,把count单独写一条SQL。数据量小时差别不大,数据量大时这个优化很关键。
3.3 动态SQL与XML标签使用心得
MyBatis最常用的能力是动态SQL。项目里用得最多的标签是if、where、foreach,偶尔用choose和set。商品列表接口要支持按关键字、按状态、按农户筛选,XML里就是一段where加三个if:
<select id="listProduct" resultType="ProductVO"> SELECT * FROM t_product <where> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="farmerId != null"> AND farmer_id = #{farmerId} </if> </where> ORDER BY create_time DESC </select>这里有个经典坑:where后面直接拼if,如果第一个条件不成立,SQL就变成WHERE AND name = xxx,直接报错。MyBatis的where标签会自动去掉开头的AND或OR,所以能用where标签就尽量别自己拼WHERE 1=1。虽然1=1也能用,但看着不专业,而且部分数据库对1=1的优化不友好。
foreach主要用于批量操作,比如批量上下架、批量删除。用的时候注意collection的类型,参数是List就用list,是数组就用array,是对象里的属性就写属性名。我之前有一次没写对collection,报了一堆奇怪的错误,排查了半天,最后发现就是参数名对不上。
3.4 JWT登录鉴权与密码安全
登录认证用的JWT,流程是:登录成功 -> 后端生成token返回 -> 前端存localStorage -> 后续请求在Header里带Authorization: Bearer token -> 后端拦截器解析token,拿到userId和role,存到ThreadLocal里,后续业务代码可以随时取当前用户。
这里说一个重点:token里不要放敏感信息,只放userId、role和过期时间。过期时间一般设置2小时,太短用户频繁重登,太长有安全风险。如果产品要求“7天免登录”,可以单独设计refresh token机制,而不是简单把token过期时间拉到7天。拦截器里还要放行登录接口和相关静态资源,否则前端第一次访问就会整页401。
密码安全前面提过,用BCrypt加密,登录时用matches方法校验。不要在接口返回值里把password字段暴露给前端,我的做法是VO层直接不映射这个字段,避免“查出来忘了过滤”这种低级问题。很多真实系统泄露用户密码,往往就是后端多返回了一个字段。
4. Vue3前端实现与联调要点
4.1 Vite初始化与项目目录规划
前端用Vite创建项目,命令是npm create vite@latest,选Vue + TypeScript模板。相比旧版Vue CLI,Vite冷启动快太多,开发体验完全不在一个层级。项目内部目录按功能划分:api放接口定义,views放页面组件,components放公共组件,router放路由配置,stores放Pinia状态,utils放Axios封装等工具函数。每个views页面下尽量一个文件夹,里面放index.vue页面主体和可能拆出来的子组件。
Vue3后台管理系统最常搭配的是Element Plus。按需引入用unplugin-auto-import和unplugin-vue-components两个插件,页面里不用手动import ElMessage、ElMessageBox这些组件和样式。刚上手会觉得“这玩意不报错就能用”有点魔法,但用顺手之后效率提升非常明显。
4.2 Composition API和Options API的选择
Vue3新增的Composition API,和Options API相比最大的好处是逻辑聚合。一个订单管理页面,如果按Options API来写,data里的数据、methods里的方法、watch里的监听是分散的,页面逻辑一多就很乱;Composition API把同一业务的变量、方法、watch写在一块,可读性提升非常明显。
实际项目里我统一用script setup语法糖,配合ref和reactive管理数据。ref适合原始类型和单个值,reactive适合对象和数组。这里有个使用经验:list这种从接口拉回来的数组,直接用ref就行,赋值时要用list.value = res.data.list;如果是对象类型的表单数据,用reactive更方便,直接form.name = 'xxx'就行,不用写.value。很多人刚上手时对ref和reactive的区别很迷惑,记住“ref装单个值,reactive装对象”就够了。
computed和watch在后台系统里用得也很多。筛选条件变化时自动重新加载列表,我就在watch里监听筛选条件对象,变化后重置页码并调接口。注意监听对象要开deep,或者用对象里的具体字段做来源,否则监听可能不生效。
4.3 Axios封装、路由守卫与状态管理
Axios封装的核心是拦截器统一处理token、错误提示、401跳转。响应拦截器里判断code,code=200直接返回data,非200弹错误消息并reject。这样做的好处是业务页面里的接口调用代码特别干净,不用每个接口都写try-catch。基础配置里baseURL设成/api,开发环境用Vite代理、生产环境用Nginx代理,前端代码里不写具体的后端IP和端口,切换环境时不用改代码。
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( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg) return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.message || '请求失败') return Promise.reject(error) } ) export default request路由守卫用Vue Router的beforeEach,做两件事:一是判断页面是否需要登录,未登录跳登录页;二是根据当前用户角色过滤菜单和路由。角色权限这块我做了一个简单的动态路由,登录成功之后根据后端返回的role字段,前端判断能访问哪些菜单,不在范围内的路由直接拦截跳403页面。这个方案对后台管理系统来说够用了,不用引入太复杂的前端权限框架。
状态管理用的Pinia。后台系统里比较值得放store的状态是“当前用户信息”和“全局侧边栏折叠状态”。大部分页面数据没必要放store,放页面局部ref就够了。很多人习惯把所有东西都塞到store里,反而让状态不好追踪。我推荐的原则是:多个页面或组件共享的状态才放store,其他老老实实放局部。
4.4 后台管理核心页面实现要点
后台管理系统的高频组件组合是表格加搜索表单加分页加弹窗表单。这个项目里我封装了一个useTable组合式函数,把加载loading、列表数据、总数、页码、查询方法集中在一起,每个页面调用useTable就能获得一套可复用的表格逻辑,代码量直接减半。这是Vue3 Composition API相对Options API最舒服的地方,逻辑复用是纯函数级别的。
图片上传多用Element Plus的el-upload,关键配置是action指向后端上传接口,name设为file。上传成功后会返回图片URL,再把URL回填到商品表单的image_url字段。这里有一个前端很常见的坑:el-upload默认在上传完成后立即触发表单提交,如果没处理好会导致商品先提交了图片还没上传完。处理方式是把auto-upload设为false,文件先存到本地列表,点击表单提交按钮时再统一上传,拿到URL后再调用保存商品接口。
商品详情和图片预览用el-image的preview-src-list,点击图片放大预览,体验很舒服。整体来说,Element Plus把后台系统的常规交互基本都覆盖了,剩下的是业务逻辑,不需要和UI框架较劲,把精力放在接口联调和数据流转上更有价值。
5. 前后端联调、常见问题与排错记录
5.1 跨域、日期格式、Long精度丢失:三个高频联调问题
开发环境前后端端口不同,跨域是第一个问题。我的处理方案是在Vite的vite.config.ts里配置proxy,把/api开头的请求都代理到本地后端端口。这样浏览器里看到的还是同一个origin,根本不会触发跨域。生产环境用Nginx做反向代理,前端所有/api请求转发到后端服务,同样规避跨域。后端也要配好CORS作为兜底,但实际项目里代理方案更常见。
日期格式问题。后端返回LocalDateTime时,默认序列化出来是带T的格式,前端显示很难看。我在配置里全局指定了Jackson的日期格式为“yyyy-MM-dd HH:mm:ss”,并设置TimeZone为GMT+8,否则会存在时差。很多人会在每个字段上加@JsonFormat注解,我建议直接在配置里全局处理,简单统一。
Long精度丢失是经典问题。数据库主键如果用了雪花ID或超长id,前端JS的Number类型超过2的53次方就会丢精度。项目里如果用自增id,数据量小时一般没事,但一旦用了雪花ID,就必须在Jackson里把Long序列化为字符串。我给全局配置了一个ToStringSerializer,所有Long统一转成字符串,前端拿到的id是字符串,传回后端时再转回Long。这个配置能避免“列表里看id是对的,点编辑进去发现id变了”这种诡异问题。
5.2 MyBatis缓存与N+1查询问题
MyBatis的一级缓存默认开启,范围是SqlSession。SpringBoot整合MyBatis后,每次查询默认开启一个SqlSession,执行完就关闭,所以一级缓存平时体会不到。但有个场景要注意:同一个事务里连续查两次相同条件的数据,第二次不会真正查数据库,拿的是第一次的旧值。如果中间数据被别人改了,你会觉得“缓存是不是坏了”。解决方案是明确需要最新数据的查询加flushCache=true,或者不要在同一事务里依赖重复查询。
N+1查询是关联查询里最常见的性能问题。比如查出10个农户,再循环查每个农户的商品列表,就会产生1加10次SQL。解决思路一个是JOIN查询一次查出结果,用ResultMap做映射;另一个是在Mapper里用嵌套查询让MyBatis批量处理。排查N+1最简单的办法是开启MyBatis的SQL日志,看看到底打了多少条SQL,几十条甚至上百条就说明写法有问题。
面试题里常问的二级缓存,我在这个项目里没有开。原因是二级缓存是跨SqlSession的,商品和订单这种数据变动频繁,缓存命中率低,还要处理缓存失效问题,对业务系统来说收益太小。如果面试官问缓存策略,我会先讲清楚一级缓存和二级缓存的区别,然后补充一句“生产系统里一般建议小范围使用,不能无脑开”,这才是懂业务的做法。
5.3 MySQL索引、慢查询与深翻页
数据库性能优化建议从索引开始。商品表必备的索引是status和farmer_id,因为列表页查询的常见条件是状态加农户。联合索引要注意最左前缀原则,where条件里用到多个字段时,索引的字段顺序要按查询频率排。我给订单表建了(status, create_time)的联合索引,因为订单列表最常看“某个状态下的最新订单”,这个索引可以同时过滤状态并排序。
深翻页问题也很常见。普通分页在数据量不大时没问题,但页码到几百页时,MySQL要扫描前面所有偏移量,非常慢。优化方式是用“上一页最后一条记录的id”做游标条件,比如WHERE id > #{lastId} ORDER BY id LIMIT 20。后台表格如果支持直接跳页,还是用普通分页;如果是“加载更多”或“上一页下一页”这种场景,游标分页是更好的方案。
排查慢SQL,建议开启MySQL慢查询日志。临时开启可以用SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;。看到某个SQL耗时高,就用EXPLAIN看执行计划,重点看type列和key列。type如果是ALL就是全表扫描,必须优化;key是空说明没有用上索引。这个检查流程我基本每次排查性能问题都走一遍,简单有效。
5.4 打包部署与资源映射
打包部署有两种方案。第一种是标准的前后端分离部署,前端dist目录交给Nginx,后端打jar包用java -jar跑,Nginx配置/api反向代理到后端。第二种是把前端打包产物放到SpringBoot的src/main/resources/static目录下,打成单jar包,一个Java进程搞定所有事情。标题里的“vue打包放进springboot”说的就是这种。小项目或演示环境建议用第二种,部署省事;正式环境用第一种,前后端可以独立发布。
前端打包时有个细节:Vue项目路由模式用createWebHistory的情况下,Nginx要做try_files配置,否则刷新二级页面会404。如果不想处理Nginx配置,用createWebHashHistory也行,URL带个#号,但SEO不友好。后台管理系统内部使用其实无所谓,我的习惯是history模式配好try_files,一次配置之后省心。
后端jar的配置文件建议外置,不用每次改数据库密码就重新打包。SpringBoot支持jar包同级目录放application.yml,优先级高于jar内部的配置。数据库连接、Redis地址这些环境相关的配置放外部,代码里的配置只留基础项,同一个jar包在不同环境都能跑,不用动代码。MySQL 8.0安装时注意选择utf8mb4字符集,如果用老版本客户端连接8.0,可能需要把认证插件切回mysql_native_password,否则会出现认证失败。
5.5 常见问题速查表
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 前端请求跨域 | 端口不同、Origin不一致 | Vite proxy或Nginx反向代理 |
| 日期显示带T、时差8小时 | Jackson默认序列化格式与时区 | 全局配置日期格式和GMT+8 |
| 订单id在编辑页面变了 | Long超过JS安全整数范围 | 全局Long转字符串返回 |
| 库存超卖 | 先查后改导致并发覆盖 | UPDATE...WHERE stock>=quantity |
| 分页数据错乱 | startPage作用于错误SQL | startPage紧跟目标查询 |
| 刷新页面404 | history路由未配try_files | Nginx配置try_files |
| 图片上传时表单先提交 | el-upload默认立即上传 | auto-upload=false手动控制 |
这个表是我在实际开发中整理出的高频问题,基本每个项目都会碰到其中几条。遇到类似问题时先对照表里排查,能省不少时间。
我在实际开发中还有一个很深的体会:这类管理系统最大的成本不在代码,而在把业务规则在动手前理清楚。状态机、审核流、库存扣减,每一条规则都和真实业务绑在一起,代码反而是最末端的事情。如果你手头正好也要做一个类似的助农、电商、货源管理类系统,我的建议是先花两天把角色、状态、核心流程在纸上理顺,再打开IDE。源码可以直接跑,但真正值钱的是那些SQL写法、事务边界和配置细节,把这些看懂,下次换一个业务场景,你也能搭出同样可靠的一套。