1. 项目整体设计与技术选型思路
做项目申报系统这类“信息管理系统”,最忌讳一上来就堆代码。第个要做的事,是把业务模型想清楚:申报谁来做、审批谁来管、数据怎么流转、后期怎么维护。这套系统的价值正在于它覆盖了一条完整的业务链——从用户注册登录、在线填写申报材料,到管理员审核、进度跟踪、结果反馈,每一步都有对应的功能支撑,不是那种只能看看的演示项目。
1.1 为什么选SpringBoot + Vue + MySQL这套组合
技术选型是这个项目最值得聊的部分。先说后端,SpringBoot 在 Java 生态里的地位不需要多讲,它把繁琐的配置大量简化,内嵌 Tomcat,打 jar 包就能跑,非常适合做这类中小型管理系统。你不需要像传统 SSM 项目那样写一堆 XML 配置,注解 + 自动装配基本能把 80% 的重复劳动干掉。更重要的是,SpringBoot 的生态太成熟了,做权限可以用 Spring Security 或 Sa-Token,做接口文档可以用 Knife4j,做持久层可以用 MyBatis-Plus,每一层都有现成的方案可以组合。
前端选 Vue 而不是 JSP 或 Thymeleaf,核心原因是前后端分离的架构更符合现在主流开发模式。Vue 的响应式数据绑定让表单填写、状态切换这类交互变得非常自然,组件化开发也方便复用——比如申报表单里的附件上传、审批流程里的时间线展示,抽成组件之后,后期加需求只需要往组件里填参数,不用到处复制粘贴。
至于 MySQL,它是目前最通用的关系型数据库,事务支持好,数据一致性有保障,查询性能在中小规模数据量下完全没有问题,而且部署和维护成本低。项目申报系统的数据特点是:结构化强(用户表、申报表、审批记录表)、关系清晰(用户-申报、申报-审批)、并发量不高但数据准确性要求高,MySQL 恰好都能满足。三件套各自不追求“最新最炫”,但组合起来的稳定性、可参考的资料量、踩坑后的解决方案丰富度,都是其他方案比不了的。
1.2 功能模块与系统架构设计
这套系统从功能上拆,大致可以分成四个核心模块:
- 系统管理模块:用户管理、角色管理、菜单管理,是整个系统的基础支撑。
- 项目申报模块:申报人填写项目信息、上传附件、提交申报,支持草稿暂存。
- 审核管理模块:管理员(或评审专家)对待审核项目进行初审、复审、驳回或通过。
- 信息查询与统计模块:申报进度查询、历史记录查看、申报数据汇总统计。
模块之间不是孤立的,它们通过状态字段的数据流转串联在一起。比如申报人提交后,项目状态从“草稿”变为“待审核”,审核人处理后变为“已通过”或“已驳回”,驳回时填写意见,申报人首页就能看到结果和修改建议。这套状态机逻辑虽然简单,但它是整个系统的核心脉络,所有页面都围绕它展开。
架构层面,我采用的是标准的前后端分离架构:
- 前端 Vue(开发端口 8080)通过 Axios 反向代理请求后端接口。
- 后端 SpringBoot(端口 8081)提供 RESTful API,统一返回 JSON 数据。
- MySQL 存储业务数据,通过 MyBatis-Plus 完成 ORM 映射和数据库操作。
提示:项目里前后端分离开发时,需要解决跨域问题,这是新手最容易卡住的地方。后面我会详细讲怎么配置。
2. 后端SpringBoot核心实现细节
2.1 工程结构与分层设计
后端工程我习惯按“控制层 - 业务层 - 数据层”三层结构来组织包名。控制层负责接收前端请求、参数校验、返回结果;业务层处理具体逻辑,比如申报流程的状态流转、审核权限的校验;数据层通过 Mapper 接口与数据库交互。举一个实际的项目结构示例:
com.example.application ├── controller // 控制层,RESTful接口入口 │ ├── AuthController.java │ ├── ProjectController.java │ └── AuditController.java ├── service // 业务层,核心逻辑处理 │ ├── ProjectService.java │ ├── AuditService.java │ └── UserService.java ├── mapper // 数据访问层,MyBatis-Plus Mapper接口 │ ├── ProjectMapper.java │ └── UserMapper.java ├── entity // 数据库实体类 ├── dto // 数据传输对象 ├── config // 全局配置类,跨域、拦截器等 └── common // 通用返回结果、异常处理、工具类为什么要分这么多层?核心目的是“各司其职,降低耦合”。如果你把所有逻辑都堆在 Controller 里,前期开发确实快,但一旦业务复杂到需要接口复用、权限变动、多人协作时,你就会发现改一处坏一片。分层之后,Controller 只做“翻译”和“调度”,具体的业务规则放 Service,数据访问细节放 Mapper,排查问题时顺着调用链一层层找,非常清晰。
项目里我统一封装了一个Result类作为接口返回结果,包含code、message、data三个字段。这样做的好处是前端可以对所有接口响应做统一拦截处理,比如 code 为 401 时自动跳转登录页,不需要每个页面单独判断接口成功与否。
2.2 认证授权与权限控制
管理系统最核心的安全问题是权限控制。这个系统里我采用了基于 Token 的认证方式:用户登录成功后,后端生成一个 Token(我用的时 UUID + 用户信息加密后的组合),返回给前端存储在本地。后续每次请求,前端把 Token 放在请求头里,后端通过拦截器校验 Token 是否有效、是否过期。
权限这块用了 SpringBoot 拦截器 + 注解实现。定义一个@RequireRole注解,标注在 Controller 方法上,配合自定义拦截器判断当前用户角色是否匹配。比如审核接口标注“仅管理员可访问”,普通用户就算知道接口地址,请求也会被拦截返回“无权限”提示。这种方式比在每个业务方法里手写 if 判断要优雅得多。
代码大致思路如下:
// 自定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String value() default "admin"; } // 拦截器里校验 public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头获取token,校验用户信息和角色 // 如果无权限,直接返回 Result.fail(403, "无权限访问") return true; } }注意:权限控制不是“加个拦截器就完事了”,你必须考虑 Token 过期后的处理逻辑、用户主动退出时的 Token 失效、以及不同角色访问同一接口的数据隔离问题。这些细节都要在实际开发中逐一完善,否则系统上线后会出现各种“诡异”的越权现象。
2.3 核心业务接口设计与实现
拿项目申报这个核心场景举例,后端接口设计要覆盖:
- 保存草稿:申报人未提交前可以多次编辑保存,不改变状态。
- 提交申报:校验必填字段完整性,将申报状态从“草稿”转为“待审核”。
- 分页查询我的申报:普通用户只看自己的申报记录。
- 管理员审核列表:管理员看所有待审核申报,支持按状态筛选。
- 执行审核操作:通过则更新状态,驳回则记录驳回意见并通知申报人。
分页查询是管理系统的高频操作,我直接用了 MyBatis-Plus 提供的Page对象配合 LambdaQueryWrapper 实现。例如查询“我的申报列表”:
public PageResult<ProjectDTO> queryMyProjects(Long userId, Integer pageNum, Integer pageSize, Integer status) { LambdaQueryWrapper<Project> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Project::getUserId, userId); if (status != null) { wrapper.eq(Project::getStatus, status); } wrapper.orderByDesc(Project::getCreateTime); Page<Project> page = projectMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); // 转换为DTO,补充项目类型名称等冗余字段 return PageResult.of(page); }审核操作需要保证数据一致性。比如审核通过时,要同时更新申报主表状态、插入审核记录表、如果是终审通过还要更新项目的最终编号。这里我加上了@Transactional事务注解,确保要么全部成功,要么全部回滚,避免出现“状态改了但审核记录没写进去”的半成品数据。这一块是我实际测试中发现最容易出问题的地方——不加事务,一旦中间步骤报错,后面的数据就全乱了。
3. 前端Vue界面与数据交互实现
3.1 工程初始化与路由设计
前端我用 Vue CLI 创建的标准工程,配合 Vue Router 做页面路由管理。路由设计直接影响后期扩展的复杂度,我的原则是“按权限拆路由,按模块建目录”。
路由分成三块:公开路由(登录页、注册页)、用户路由(我的申报、项目列表、申报表单)、管理员路由(审核中心、用户管理、数据统计)。通过 Vue Router 的导航守卫,每次路由跳转前判断当前用户的 Token 和角色,如果 Token 不存在跳转到登录页,如果角色不匹配跳转到 403 页面。
// 路由守卫示例 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next('/login'); return; } if (to.meta.role && to.meta.role !== localStorage.getItem('role')) { next('/403'); return; } next(); });实际开发中路由命名也要规范化。我习惯路径名和组件文件名保持一致,比如project/list对应views/project/List.vue,这样只要看到路由配置,就能快速定位到对应组件文件,避免后期自己都找不到代码在哪。
3.2 通用数据请求封装与拦截
前端所有请求我都封装在一个request.js模块里。为什么一定要封装?因为如果不封装,每个页面都各自调用 Axios,你会有大量重复代码,而且一旦后端接口返回格式调整,你就要改全部页面。封装之后,只需要在一个地方统一处理所有逻辑:
// request.js 核心封装 import axios from 'axios'; import { Message } from 'element-ui'; import router from '@/router'; const service = axios.create({ baseURL: '/api', // 统一代理前缀 timeout: 10000 }); // 请求拦截器:自动携带Token service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Token'] = token; } return config; }); // 响应拦截器:统一处理业务错误 service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { Message.error(res.message || '请求失败'); if (res.code === 401) { router.push('/login'); } return Promise.reject(new Error(res.message)); } return res.data; }, error => Message.error('网络异常,请稍后重试') ); export default service;这里有个细节值得注意:开发环境下前端通过代理解决跨域,生产环境则直接把前端打包文件放进 SpringBoot 的static目录下。所以baseURL不用写死成http://localhost:8081,而是用相对路径/api,然后通过 vue.config.js 里的 devServer 代理转发到后端地址。这样后续部署时可以做到“无缝切换”,不用改代码。
3.3 申报表单与审批页面的关键实现
申报表单是这个系统里交互最复杂的页面。字段类型多(输入框、下拉框、日期选择器、附件上传)、校验规则多(必填、格式、长度限制),还支持暂存草稿。我把它拆成了多个子组件,每个子组件负责一个区块:
- 基础信息组件:项目名称、项目类型、负责人信息。
- 资金信息组件:申请金额、预算明细。
- 附件上传组件:支持多文件上传、预览、删除。
- 提交栏:保存草稿、提交审核两个按钮。
表单校验用 Vue 的el-form规则,切换提交方式时校验强度不同——保存草稿只校验必要字段,提交审核则要校验所有字段。这个交互逻辑要写清楚,否则会出现“草稿都没填完,点提交直接报错”的尴尬情况。
审核页面的核心是“审核流程时间线”。我用 Element UI 的el-timeline组件展示申报记录的完整流转过程,从“提交申报”到“初审通过”再到“终审完成”,每一步都有操作人和时间。后端按时间顺序返回记录列表,前端渲染成纵向时间线,用户体验比单纯的表格状态列表直观得多。
4. 数据库设计与初始化数据
4.1 核心表结构与字段设计
数据库设计直接决定系统的扩展性和查询效率。这套系统我设计了 6 张核心表:
| 表名 | 用途 | 核心字段 |
|---|---|---|
| sys_user | 用户表 | id,username,password,role,real_name,phone,email |
| sys_role | 角色表 | id,role_code,role_name,description |
| project_info | 项目申报主表 | id,user_id,project_name,project_type,amount,status,create_time |
| project_attachment | 附件表 | id,project_id,file_name,file_url,upload_time |
| audit_record | 审核记录表 | id,project_id,audit_user_id,audit_result,audit_comment,audit_time |
| sys_notice | 通知公告表 | id,title,content,publish_time,status |
字段设计时有几个细节是我反复调整过的。第一,状态字段不要用字符串随意填,统一用数字枚举(0 草稿、1 待审核、2 已通过、3 已驳回),前后端约定好映射关系,后期做统计更方便。第二,金额字段用decimal(12,2),不能用float,否则会出现精度问题。第三,所有表都要有create_time和update_time字段,排查问题、做数据审计时非常有用。
4.2 初始化数据与索引优化
系统运行需要一些基础数据,比如管理员账号、项目类型字典、审核规则配置。我在 SQL 脚本里通过INSERT语句预置了这些数据。有一个小技巧:管理员账号的密码不要用明文,用 MD5 加密后的值直接写入,避免数据库泄露后账号被直接使用。
索引方面,申报表查询最频繁的是“按用户ID查”和“按状态筛选”,所以我对user_id和status字段分别建了普通索引。另外对于audit_record表,project_id是查询高频字段,也必须建索引。索引不是越多越好,它会拖慢写入速度,所以我只在真正高频查询的字段上加。实测下来,数据量在 10 万条以内,配合索引查询响应基本都在毫秒级,对这类系统完全够用。
提示:如果你接手项目后需要加字段,不要直接改原表结构,尽量用
ALTER TABLE加字段的方式,并且先备份。我在开发中吃过亏,一次误删字段直接丢失了测试数据,重新造数据花了大半天。
5. 本地部署运行与环境配置
5.1 开发环境准备
跑通这套系统,你需要准备的基础环境:
| 软件 | 版本建议 | 备注 |
|---|---|---|
| JDK | 1.8 或 11 | SpringBoot 2.x 对 JDK8 支持最稳定 |
| Maven | 3.6+ | 用于后端依赖管理和打包 |
| Node.js | 14+ | 用于前端依赖安装和构建 |
| MySQL | 5.7 或 8.0 | 需要支持 utf8mb4 编码 |
| IDE | IDEA 或 VSCode | 后端建议 IDEA,前端 VSCode 也够用 |
安装 MySQL 时需要注意字符集问题。我建议在安装时就选择 utf8mb4 字符集,或者在配置文件my.cnf/my.ini中显式设置:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_general_ci否则后面导入 SQL 文件时,中文可能会出现乱码。这一点是我多次遇到的头疼问题——项目代码没问题,但页面展示的中文全是“问号”,排查了半天才发现是数据库字符集不对。
5.2 后端与前端启动步骤
后端启动流程比较固定:
- 用 IDEA 打开后端工程,等待 Maven 下载依赖完成(第一次会比较慢,建议配阿里云镜像加速)。
- 修改
application.yml中的数据库连接信息:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/project_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password- 执行
mvn spring-boot:run或直接运行主启动类。 - 看到
Started Application in X.XXX seconds日志即启动成功。
前端启动流程:
- 进入前端工程目录,执行
npm install安装依赖。 - 检查
vue.config.js中的代理配置:
devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '' } } } }- 执行
npm run serve,浏览器访问http://localhost:8080。
启动成功后,用预置的管理员账号登录,进入系统首页检查菜单和页面是否正常渲染,这就是“可直接运行”的标准验证方式。
5.3 前后端合并部署方案
如果你不想开发时保持前后端分离的模式,也可以直接把前端打包产物放到后端工程里,用一个 SpringBoot 进程跑完整个项目。操作流程:
- 在前端目录执行
npm run build,生成dist目录。 - 把
dist目录下的所有文件复制到后端src/main/resources/static目录下。 - 重新打包后端:
mvn clean package -DskipTests。 - 执行
java -jar application.jar,访问http://localhost:8081即可。
这种部署方式的优点是只需要维护一个进程、一个端口,适合部署到轻量服务器或内网环境。缺点是不能前后端独立升级,每次前端改动都要重新打包后端。对于中小型项目,这种“静态资源内嵌”的方式反而省事,我实际部署时就用的这个方案。
6. 常见问题与踩坑实录
6.1 后端启动报错与数据库连接问题
后端启动最常见的问题是数据库连接失败。报错信息通常是Communications link failure或者Access denied for user。排查步骤:
- 确认 MySQL 服务是否启动,
netstat -an | findstr 3306查看端口监听。 - 确认数据库名、用户名、密码是否和
application.yml一致。 - 确认驱动版本。SpringBoot 2.x 默认用的 MySQL 驱动是
com.mysql.cj.jdbc.Driver,如果连接 5.7 版本数据库需要留意serverTimezone参数是否配置。
另外一个我实际遇到过的问题:Table 'project_db.sys_user' doesn't exist。原因不是表没建,而是连接了错误的库。如果你本机上有多个 MySQL 实例,很容易出现连到了测试库而非业务库的情况。建议在url中指定完整库名,并在启动前先执行show tables确认表存在。
6.2 前端跨域与代理配置问题
开发过程中,前后端分离必然遇到跨域。报错一般就是浏览器的 CORS 错误,提示No 'Access-Control-Allow-Origin' header is present on the requested resource。
解决方式我自己用过两种:
第一种:后端配置全局跨域:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }第二种:前端使用 devServer 代理,完全不触发跨域。这种方式更推荐,因为生产环境不需要跨域配置。关键点在于代理的pathRewrite是否需要重写路径——如果你的后端接口没有/api前缀,就必须用它把/api去掉,否则请求会 404。很多新手卡在这里,报 404 还以为是后端接口没写对。
6.3 前端页面白屏与控制台报错排查
前端启动成功后页面一片空白,这是最常见的另一个问题。排查思路分三步:
- 打开浏览器开发者工具 Console 面板,看有没有报错信息。
- 如果有红色报错,优先看是不是跨域问题、接口请求异常、或者路由配置错误。
- 如果 Console 没有报错,看 Network 面板里接口是否正常返回,再检查组件的
name和路由配置中的component引入路径是否正确。
我遇到过多次“页面空白”其实是路由配置中组件路径写错了,比如把@/views/project/List.vue写成了@/view/project/List.vue,编译不报错但路由匹配不到组件。这种问题最隐蔽,只能靠逐个检查组件的引入路径来定位。
6.4 数据中文乱码问题
中文乱码通常有三个来源:
- 数据库表或字段字符集不是 utf8mb4。解决方式是把表字符集改掉。
- 后端返回数据时编码有问题。检查 SpringBoot 的
server.tomcat.uri-encoding是否设置为 UTF-8,一般默认就是,但如果你修改过就要确认。 - 前端页面本身没有声明字符集。在
index.html中确保有<meta charset="utf-8">。
乱码问题虽然不复杂,但排查起来很耗时,因为要一层层确认“是哪里出了问题”。我的经验是先在数据库客户端里直接查询数据,如果数据库里存的就是乱码,问题出在写入链路;如果数据库里正常但页面上乱码,问题出在读取或展示链路。这样能少走弯路。
7. 项目扩展方向与二次开发建议
系统能“直接运行”只代表拿到了可用的基础版,真正的价值在于二次开发的扩展空间。我梳理了几个值得加的功能方向:
第一,导入导出功能。项目申报系统经常需要统计数据报表,用 Excel 批量导入申报数据、导出审核结果,可以大幅提升工作效率。后端可以用 EasyExcel 或 POI,前端用el-upload上传文件,整体接入成本不高。
第二,消息通知机制。目前审核结果只能登录系统后查看,可以接入邮件或短信通知。SpringBoot 整合 JavaMailSender 实现邮件通知比较简单,用户在提交申报时填写邮箱,审核完成时自动发邮件,体验会好很多。
第三,流程引擎扩展。目前审核流程是固定的“提交 - 审核 - 通过/驳回”,如果业务场景需要多级审批、条件分支、动态指定审批人,可以考虑引入 Flowable 或 Activiti 工作流引擎。但这个方向复杂度较高,建议先把现有功能吃透再动手。
数据库层面还可以加缓存。比如项目类型字典这类读多写少的数据,引入 Redis 缓存后,可以明显减少数据库压力。如果项目后续要集群部署,还需要考虑 Session 共享、文件统一存储(FastDFS 或 OSS)等问题,但这些是后话,不适合在第一版就过度设计。
我个人的建议是:先把这套“基础版”从头到尾跑通,搞清楚每一条数据链路是怎么流转的,再根据实际业务需求决定优先扩展哪个方向。很多刚接触这个项目的人,一上来就想着加新功能,结果连最基本的“为何审核状态没有联动更新”都没搞明白,这是很可惜的——源码的价值不只是跑起来的业务功能,更是理解和学习架构设计的真实样本。