news 2026/10/11 2:34:29

SpringBoot+Vue3项目申报系统实战:从设计到部署的关键决策

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue3项目申报系统实战:从设计到部署的关键决策

做完整套项目申报系统,前后花了大约三周时间。这个项目的技术栈就是标题里列的:Java + SpringBoot + Vue3 + MyBatis + MySQL,前后端分离,典型的Web管理类项目。之所以选这套组合,不是因为追新,而是因为它足够稳、足够快,而且拆出来的每一块都能单独复用。项目申报这种场景,业务逻辑看着不复杂,但流程状态多、角色权限杂、附件和审批记录一多就容易乱,真正做下来才发现坑都在细节里。

这篇文章不打算做教程式流水账,而是把我从设计到落地的关键决策、踩过的坑、以及最终沉淀下来的可复用方案一次性讲清楚。无论你是刚接触SpringBoot和Vue3的初学者,还是要做类似审批流项目的开发者,这套思路和代码结构都能直接抄作业。

1. 项目选型与整体设计思路拆解

1.1 为什么是SpringBoot+Vue3+MyBatis+MySQL

先说后端。SpringBoot早就不是“新框架”了,但在这个体量的项目里它依然是最省心的选择。不需要配置繁琐的XML容器,内嵌Tomcat,一个jar包跑起来,这在中后台业务系统里是刚需。项目申报系统本质上就是一个围绕“表单+审批+记录”的CRUD增强型应用,SpringBoot的约定优于配置能帮我把80%的基础设施问题直接抹掉。

MyBatis在这个项目里的定位很明确——它比JPA更可控。申报系统的查询场景非常杂:按状态筛选、按申报人查、按时间区间查、关键词模糊搜,再加上和附件表、评审记录表的多表关联。JPA那种自动生成SQL的抽象,在这种复杂的查询需求面前反而是一种束缚。MyBatis把SQL交回你手里,配合动态SQL标签,查询逻辑怎么写、怎么优化,心里一清二楚。面试题里常问的MyBatis缓存、TypeHandler之类的机制,在这个项目里也都有实战场景可以对照。

Vue3这边没有太多悬念。组件化开发本来就适合这种页面结构高度相似的管理系统,而Vue3的Composition API让逻辑复用有了质的提升。比如申报表单和评审表单,它们的基础字段不同,但都有编辑态和详情态,如果用Options API复制粘贴,后期维护就是灾难。用Composition API写一个useFormState,状态切换逻辑直接抽成公共函数,这才是干活的姿势。

MySQL选型没有任何纠结。单机事务、结构化表单数据、带复杂查询,MySQL 8.0全部能扛,而且环境普及率高,部署时不会遇到奇奇怪怪的兼容性问题。单表数据量到了几十万级,加几个组合索引完全够用。真到需要分库分表的规模,这个项目形态本身就该先考虑架构升级了。

1.2 前后端分离的价值边界

前后端分离不是银弹,但在这个项目里确实是正确的选择。核心原因有三个:一是申报系统需要同时支撑PC端管理后台,后续很可能还要做移动端审批入口,API在后端单独成层,多端复用是白赚的;二是前端构建和后端发布可以完全独立,后端改个状态流转逻辑不用牵连前端重新打包,Vue3项目调试和部署效率高很多;三是前后端各自有独立的进程和错误日志,线上排查问题的时候能快速定位是哪一侧出了故障。

不过分离也有代价,最大问题是跨域。开发阶段我用Vite的proxy把/api转发到后端8080端口,线上用Nginx统一反向代理。这个方案能规避绝大多数CORS相关的坑,比后端加@CrossOrigin注解可靠得多,因为生产环境的跨域配置是Nginx层面统一处理的,后端代码里就不需要再放开所有来源了。

1.3 业务模型的迂回建模

做申报系统之前,最忌讳的是直接照搬“表单+列表”的简单模型。真正跑过业务就明白,申报系统本质上是“一条数据在一个状态机里流转”。我需要处理的角色包括申报人、二级单位审核人、科技处管理员、评审专家,每个角色在不同状态下能看的字段和能做的操作都不一样。

所以项目里我建了一个核心状态枚举类,定义申报项目的完整生命周期:草稿、已提交、单位初审通过、单位初审退回、科技处受理、专家评审中、评审通过待立项、已立项、已驳回。这个状态机是整个后端service层的心脏,所有状态变更都通过统一的状态流转方法处理,不允许Controller里直接去修改申报记录的状态字段,否则流程一复杂,代码就失控了。

2. 后端核心实现与实操要点

2.1 SpringBoot工程结构与分层约定

一个中型管理类项目,工程结构不要贪多,但分层必须清晰。我的划分方式是标准四层:controller、service、mapper、entity,然后额外加了一个common包放统一返回结果、异常处理、JWT工具类和枚举定义。

有个细节值得说:entity不要直接扔给前端。数据库字段和前端需要的VO字段经常对不上,比如申报表里存的是user_id,前端列表要展示的是申报人的姓名和部门。所以我又加了一个dto层做数据装配。虽然代码量多一点,但避免了把数据库结构直接暴露给前端,后续改表结构的时候,改动范围被限制在service层内,不会波及API接口。

2.2 MyBatis动态SQL和一对多查询

申报列表页是查询压力最大的接口。它要同时支持按项目名称模糊搜索、按申报人精确筛选、按状态分类筛选、按申报时间排序。如果用Java代码去拼SQL字符串,不仅丑,还容易出注入漏洞。MyBatis的<where>、<if>、<foreach>组合拳正好解决这个问题。

<select id="selectPageList" resultType="com.example.entity.DeclarationProject"> SELECT p.id, p.project_name, p.project_type, p.budget, p.status, u.real_name AS applicantName, d.dept_name AS applicantDept FROM declaration_project p LEFT JOIN sys_user u ON p.user_id = u.id LEFT JOIN sys_dept d ON u.dept_id = d.id <where> <if test="projectName != null and projectName != ''"> AND p.project_name LIKE CONCAT('%', #{projectName}, '%') </if> <if test="status != null"> AND p.status = #{status} </if> <if test="userId != null"> AND p.user_id = #{userId} </if> </where> ORDER BY p.create_time DESC </select>

这段SQL里有几个值得注意的操作细节。第一个是LIKE CONCAT('%', #{projectName}, '%'),不使用'%${projectName}%'的写法,后者存在SQL注入风险,线上环境绝对不能那么写。第二个是LEFT JOIN查申报人信息,一次关联查出来,避免在Java层循环查库,这在数据量上来以后性能差距非常明显。

申报详情页还有一个一对多场景:一条申报项目下面挂多条附件记录、多条审批记录。我用了MyBatis的collection标签做嵌套结果映射,一条SQL把主表和子表数据都查出来。

<resultMap id="DeclarationDetailMap" type="com.example.dto.DeclarationDetailDTO"> <id property="id" column="id"/> <result property="projectName" column="project_name"/> <result property="status" column="status"/> <collection property="attachmentList" ofType="com.example.entity.Attachment"> <id property="id" column="attach_id"/> <result property="fileName" column="file_name"/> <result property="filePath" column="file_path"/> </collection> </resultMap>

大字段和列表字段不混合查询是很重要的优化经验。申报书内容动辄上千字,如果列表页的查询也把正文查出来,响应体直接膨胀好几倍。我最终把列表查询单独写了一个轻量SQL,只查摘要字段,详情接口才查完整内容。

2.3 JWT登录鉴权和权限控制

管理系统绕不开登录鉴权。我用了JWT + 拦截器的方案,没有引入Spring Security这种重量级框架。原因很实在:这个项目只需要角色鉴权,不需要OAuth2、不需要方法级细粒度权限表达式,Spring Security的过滤器链反而增加理解成本。

JWT的工具类大家都会写,但有一个坑必须提醒:token过期时间的设置不能太短也不能太长。我设的是8小时,配合前端路由守卫做免登录跳转。过了有效期,后端拦截器会抛401,前端Axios响应拦截器捕获后清理本地token然后跳转登录页。

拦截器逻辑里有个容易被忽略的细节:白名单。登录接口、验证码接口、Swagger文档这些不需要token就能访问,必须显式配置到白名单里,否则登录页面第一次加载就会被自己的拦截器挡住。我在实际项目里见到过好多次这种问题,排了半天查出来是拦截器把登录接口也拦了。

2.4 状态流转的事务保证

申报提交、审批通过这类操作,往往不是一个update语句就能完成的。比如审批通过,至少要做三件事:更新申报状态、插入审批记录、如果是“评审通过待立项”还要在立项表里插入一条关联记录。这三个操作必须在一个事务里,任何一个失败都要全部回滚。

我在service层用@Transactional(rollbackFor = Exception.class)做事务管理。注意一定要指定rollbackFor参数,默认情况下Spring只对RuntimeException回滚,检查异常不会回滚,这个坑深得很。

状态流转方法还有一个业务层面的设计:每次状态变更都要记录操作人和操作意见。我封装了一个stateChange方法,传染入当前申报ID、目标状态、审批意见和操作人ID,方法内部开启事务执行状态更新+审批记录插入。所有入口都走这一个方法,状态操作逻辑就完全收敛了,不会出现这里改一下、那里改一下的状态失控。

3. 前端Vue3关键实现与工程化实践

3.1 Composition API带来的组织方式变化

Vue3最核心的变化就是Composition API。刚开始从Vue2过来的人会不适应,觉得代码结构变散了,但实际用下来会发现,它把“功能的逻辑”而不是“选项的类型”作为组织单位,这更符合人类认知。

举个例子,申报表单页有三个子模块:项目基本信息、附件上传、历史审批记录。选项式API下,你要去data找数据、methods找事件函数、computed找派生状态,一个模块的前端逻辑被拆散到三个地方。Composition API下,我直接建一个useDeclarationForm.js,里面用ref声明状态、用function定义操作逻辑,页面组件里setup一个函数引入即可。

export function useDeclarationForm() { const formRef = ref() const formData = reactive({ projectName: '', projectType: '', budget: 0, summary: '' }) const rules = { projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }] } async function submitForm() { await formRef.value.validate() // 提交逻辑 } return { formRef, formData, rules, submitForm } }

页面里直接解构使用,不同的页面之间如果逻辑相似,可以复用同一个hook。比如项目申报和项目变更,表单字段不同,但提交前校验和提交后的状态反馈逻辑完全一致,这个优势在管理系统这种高度相似页面场景里体现得特别明显。

3.2 Pinia状态管理和路由守卫

状态管理我选了Pinia,没用Vuex。不是跟风,是Pinia本身确实更轻——没有mutations概念,直接改state,TypeScript支持也好。在这个项目里,全局要存的数据就三类:用户信息、token、权限标识。

路由守卫是前端鉴权的关键。我写的router.beforeEach里做了三件事:判断是否有token、如果没有token且去的是白名单页面就直接放行否则跳登录、如果有token再去对比当前用户角色和路由meta里的roles字段,不匹配就拦下。

有一个体验优化值得提:路由守卫里如果发现用户信息为空但token存在,要主动调用getUserInfo接口拉取用户数据。这是因为刷新页面时Pinia里的用户信息会全部丢失,如果不重新拉取,很多页面会显示空白或者误判权限。

3.3 Axios拦截器和文件上传交互

管理系统90%的交互都是请求后端数据,Axios封装得好不好,直接决定开发体验。我统一封装了request工具类,做了三层拦截:请求前自动附带token、响应后统一解包{ code, data, msg }结构、遇到401自动清理登录状态并跳转登录页。

文件上传是申报系统的刚需,申报书附件、研报表、经费说明都得上传。我做了进度条上传控件,基于Element Plus的Upload组件,关键点是上传后要把后端返回的附件ID和文件名存储到表单数据里,不能只在组件内部展示。否则用户拖拽完附件觉得保存了,结果提交表单时才发现附件根本没有绑定到申报记录上。

这里还有一个隐藏的坑:要限制上传文件类型和大小。后端接口不仅要校验类型,还要在Nginx层设置client_max_body_size,否则大文件直接导致请求被截断,前端报错还看不出原因。

4. 数据库表设计与MyBatis实践细节

4.1 核心表结构设计与索引策略

申报系统的核心表设计其实没有那么神秘。我在设计的时候一共建了六张表:用户表、部门表、申报项目表、附件表、审批记录表、立项结果表。核心表是申报项目表,其他表都通过外键和它关联。

申报项目表里有两个字段一定要预留:status状态字段和version版本字段。status用tinyint类型存枚举值,查询用整数比字符串效率高。version字段用于乐观锁控制,防止多人同时编辑同一条申报数据导致相互覆盖。

索引设计是性能的关键。我在declaration_project表上建了三个索引:user_id单列索引、status单列索引、create_time降序索引。实测下来,列表页最常用的查询条件就是这三个字段的组合。组合索引要不要建,得看实际查询频率,如果大部分查询都是status + create_time,那建组合索引更划算。我有一个原则:联合索引的字段顺序要从最常查询的字段排起。

4.2 MySQL 8.0环境配置与连接参数

MySQL 8.0和5.7最明显的差异就是驱动名和时区配置。如果你还在用com.mysql.jdbc.Driver,连MySQL 8.0直接报错。正确写法是:

spring: datasource: url: jdbc:mysql://localhost:3306/declaration_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver

serverTimezone=Asia/Shanghai这个参数一定不能少,不写的话连接时会报时区错误。另外,如果你的MySQL是装在本地Windows上,安装时选UTF-8字符集很重要,否则建表默认字符集是latin1,中文存进去会直接乱码。MySQL 8.0默认字符集已经是utf8mb4了,安装时我建议再确认一下。

排序规则我统一用utf8mb4_unicode_ci,中英文混排的排序逻辑比较符合预期。之前遇到过一个奇怪现象,都是中文项目名称,但是用utf8mb4_general_ci排序时部分中文排序不对,换成utf8mb4_unicode_ci就正常了。

4.3 MyBatis的TypeHandler和自定义枚举映射

MyBatis新手最容易踩的坑,是Java枚举和数据库tinyint之间的转换。默认情况下,MyBatis把枚举存进数据库时用的是枚举的name()字符串,存成VARCHAR,这样虽然能用,但查询性能和存储空间都不理想。

我的做法是自己写TypeHandler,让枚举和数据库里的整数互转。

@MappedTypes(ProjectStatusEnum.class) public class ProjectStatusTypeHandler extends BaseTypeHandler<ProjectStatusEnum> { @Override public void setNonNullParameter(PreparedStatement ps, int i, ProjectStatusEnum parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getCode()); } @Override public ProjectStatusEnum getNullableResult(ResultSet rs, String columnName) throws SQLException { return ProjectStatusEnum.fromCode(rs.getInt(columnName)); } // 其他两个getNullableResult方法重写 }

在Mapper XML的resultMap里用typeHandler属性指定,Java代码里就可以直接用枚举类型做查询条件和set值,代码的可读性大幅提升。这也是面试题里MyBatis TypeHandler的工作流程在实际项目里最典型的应用场景。

5. 前后端联调、部署上线与问题排查实录

5.1 开发环境的跨域配置和接口联调

前后端分离项目,开发环境第一个拦路虎就是跨域。我这边开发时是Vite起前端8081端口,后端SpringBoot跑8080端口。Vite的proxy配置非常简单:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

前端所有请求仍然发到/api开头的地址,Vite在开发环境把请求转发到后端,这样浏览器看到的请求是同源的,不存在跨域问题。生产环境则交给Nginx处理,配置一个location /api反向代理块。

联调阶段最烦的是接口格式不一致。通常后端返回{ code: 200, data: {...}, msg: '成功' },但偶尔有个接口偷懒返回了裸对象,前端拿到以后还要做兼容判断。我在联调初期就把所有接口的返回格式统一规范了,后端统一用Result<T>包装类,前端Axios拦截器解包到data,后续所有页面接收到的都是干净的T类型数据。

5.2 Vue打包部署到SpringBoot的两种方式

部署方式有两种都试过。第一种是前后端完全分离,前端build产物扔到Nginx的html目录,后端jar独立运行,Nginx把/api请求反向代理到后端。这种方式扩展性好,适合后续前端要做独立CDN加速的场景。

第二种是把Vue打包产物放到SpringBoot的src/main/resources/static目录下,然后整个项目打成一个jar包直接运行。这种方式适合小型内部系统,不用额外装Nginx,一个jar包拷到哪都能跑。

做第二种方式时有一个大坑:前端路由的history模式。Vue Router用history模式时,访问http://ip:8080/declaration/list刷新页面,SpringBoot会尝试寻找对应Controller,找不到就404。解决办法是在后端加一个转发规则,把非API请求都转发到index.html。

@Controller public class PageForwardController { @RequestMapping(value = "/{path:[^\\.]*}", produces = "text/html") public String forward() { return "forward:/index.html"; } }

这个配置必须放在项目里才能保证前端路由刷新后不白屏。用hash模式就没这个问题,但URL难看,我还是建议用history模式加正向转发。

5.3 常见问题和排查速查表

做完这个项目后沉淀了不少排障经验,这里直接整理成表格方便大家对照定位。

问题现象可能原因解决方法
前端请求跨域生产环境Nginx没配代理在Nginx增加location /api反向代理配置
LocalDateTime返回格式不对缺少Jackson时间格式化配置配置spring.jackson.date-format或在字段上加@JsonFormat
MyBatis查询不到数据但不报错实体类字段和数据库列名对不上开启map-underscore-to-camel-case: true或显式配置resultMap
上传大文件失败Nginx限制请求体大小设置client_max_body_size 50m
分页查询总数错误分页插件和复杂SQL冲突检查COUNT SQL是否包含多表LEFT JOIN的重复数据
刷新页面404Vue Router history模式和SpringBoot冲突增加PageForwardController转发规则
数据库连接报时区错误JDBC URL没配serverTimezone添加serverTimezone=Asia/Shanghai

5.4 分页查询的完整实现

列表页的分页查询是管理系统高频场景。我直接用MyBatis手动分页,不引入PageHelper等插件,核心原因是想保持SQL完全透明可控。手动分页只要两条SQL:一条查总数,一条查当前页数据。

public PageResult<DeclarationProjectVO> pageQuery(PageQuery query) { // 查询总数 Long total = declarationMapper.countByCondition(query); // 查询当前页数据,计算limit偏移量 query.setOffset((query.getPageNum() - 1) * query.getPageSize()); List<DeclarationProjectVO> records = declarationMapper.selectPageList(query); return PageResult.of(total, records); }

计算偏移量时注意(pageNum - 1) * pageSize这个公式,pageNum从1开始,不是从0开始。前端表格组件有的从0开始,联调时经常会遇到第一页正常、第二页开始数据错位的情况,基本都是这个边界问题。

性能优化的小技巧:count查询的SQL不要带ORDER BY,会白白浪费排序开销。MyBatis的动态SQL里,count语句和select语句可以共用同一组<where>条件,只要把select部分换成COUNT(*)就行,用一个SQL片段引用可以避免两处条件不一致的低级错误。

5.5 登录会话管理的一些心得

最后聊一个容易被忽略但实际很影响体验的点:用户会话管理。JWT方案无状态,服务端重启token不会失效,登录态能一直保持,方便但也意味着token一旦泄露就相当于账号被脱库。我的做法是在JWT工具类里加上签发时间的记录,服务端做一个简单的在线用户管理,管理员可以强制下线指定用户。

同时,前端Axios拦截器里要对token的过期做提前判断,在token还剩10分钟时弹窗提醒用户续期,而不是等到401才被动跳登录页,那样用户正在填的申报表单内容会全部丢失。这个体验细节对申报系统这种需要长时间填写表单的场景真的非常重要。

还有一个比较隐蔽的坑:登录时如果连续输错密码,没有做验证码保护,很容易被暴力破解。我加了验证码机制,并且输错3次之后锁定账号5分钟。虽然增加了一点开发量,但作为管理系统这是底线要求,真等出了安全问题再补就晚了。

最后再分享一个小技巧。申报系统里经常要导出Excel,我一开始用POI写原生操作类,手写日期格式转换、单元格样式,代码又长又难维护。后来发现直接用EasyExcel这种封装库,实体类上加注解就能导出,日期格式、列宽、表头全都可以注解化配置,效率提升不是一点点。项目里凡是涉及Excel导入导出的地方,别再自己造轮子了,封装好的工具库一定要用起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 2:33:59

Muse开源SDK:打破AI Agent的屏幕囚笼

1. 从 App Store 榜首到开源 SDK&#xff1a;一个信号&#xff0c;而非偶然事件“Muse 登顶 App Store 并开源 SDK”——这八个字背后没有一句多余的话&#xff0c;但信息密度极高。它不是又一个“AI 工具上线”的常规新闻&#xff0c;而是一次技术演进路径的显性确认&#xff…

作者头像 李华
网站建设 2026/10/11 2:31:04

Page Assist:本地运行DeepSeek模型的离线Web UI

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/11 2:28:27

IDEA 2024 配置 Tomcat 与 Servlet 全攻略:从版本选型到部署运行

第一次接触 Tomcat 和 Servlet&#xff0c;很多人都会在 IDEA 里兜圈子&#xff1a;代码明明照着写了&#xff0c;点运行却跳出 404&#xff1b;或者明明用的是老教程&#xff0c;放进新版 IDEA 却直接编译失败。 这类事情我前前后后折腾过很多回&#xff0c;每次帮朋友排查最后…

作者头像 李华
网站建设 2026/10/11 2:27:41

C# WinForms自绘漂亮登录窗体:无边框、圆角渐变与交互细节

简介&#xff1a;这份C#登录窗体资源是一套可直接参考的WinForms登录界面项目&#xff0c;面向桌面应用入门开发者及需要快速搭建账号登录模块的C#程序员。压缩包共70个文件&#xff0c;大小约550KB&#xff1b;11个.cs源文件配合4个.resx资源描述构成主工程核心&#xff0c;3个…

作者头像 李华
网站建设 2026/10/11 2:27:15

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0全栈服装商城系统实战解析

搞Java Web电商类的项目&#xff0c;这两年最常见的需求就是“给我一个能跑、能看、能交差的全栈商城”。而网上服装商城这类带业务闭环的项目&#xff0c;正好是面试和毕设里出镜率最高的一种。今天这篇&#xff0c;我打算把手头这套SpringBoot2 Vue3 MyBatis-Plus MySQL8.…

作者头像 李华
网站建设 2026/10/11 2:26:00

告别鼠标:系统整理高效快捷键思维与实操技巧

你们有没有遇到过这种场面&#xff1a;旁边同事噼里啪啦一顿操作&#xff0c;窗口切换、文件另存、表格求和&#xff0c;鼠标基本不碰&#xff0c;几分钟搞定你磨蹭半小时的活儿。你不好意思问&#xff0c;只能默默盯着屏幕&#xff0c;心想这人是不是开了什么外挂。其实没啥神…

作者头像 李华