
2. 项目申报系统的核心业务流程拆解拿到“项目申报系统”这个题目先别急着打开IDE第一件要干的事情是把业务流程理顺。这类系统说白了就是把原本线下的纸质申报流程搬到线上让申报人、审核人、管理员三方在同一个平台上协作而不是靠邮件来回传Excel表格。一套典型的项目申报系统核心流程大概是这样申报人填写项目信息并提交 - 管理员或审核人进行初审 - 初审通过后进入专家评审或领导审批环节 - 审批结果公示 - 申报人查看结果并下载相关文件。流程看着简单落地到数据库设计和接口开发时就全是细节。比如一个项目在不同阶段的状态怎么表示审核意见要不要留痕项目附件怎么存储这些都会直接影响代码的复杂度和后续维护的体验。我在设计这类系统时一般把角色分成三类申报人普通用户、审核人管理员/评审、系统管理员超级用户。三类角色的权限边界清晰了后端的接口设计才不会乱。下面这张表格是我常用的一种角色-权限-功能划分方式可以直接拿来当参考。角色核心权限典型功能申报人申报、查看、撤回在线填报、上传附件、查看审核进度、查看审核意见审核人审核、退回、通过待审列表、项目详情查看、填写审核意见、批量通过系统管理员用户管理、数据统计角色分配、重置密码、申报数据导出、系统参数配置这里有一个很关键的设计决策必须提前想清楚审核流程是单级还是多级。如果只是“提交-审核-通过”的单级流程后端只需要一个审核状态字段就够了但如果是“系部初审-专家评审-领导终审”的多级流程那就不能只用一个字段硬扛而是得单独建一张审核记录表把每一级的审核人、审核时间、审核意见、审核状态都存下来。很多初学者就是在这里栽了跟头后面加需求时把代码改得面目全非。另外申报系统还有一个容易被忽略的业务点申报窗口期控制。不是什么时候都能提交申报的管理员需要设置一个申报的起止时间。这个时间判断不能只靠前端隐藏按钮后端接口里也要校验否则有人直接拿着Postman调接口就能绕过限制。我在项目里一般会把申报起止时间存到系统参数表里提交申报时先查当前时间是否在窗口期内不在就直接抛异常。搞清楚这些问题之后数据库的表结构基本就有雏形了用户表、项目申报表、审核记录表、附件表、系统参数表再加一个通知公告表就够了。接下来我们就从数据库开始一步步把系统搭起来。2.1 项目申报表的状态字段怎么设计状态字段是申报系统的灵魂几乎每个列表页都要按状态筛选每一条业务逻辑都和状态相关。我用过的方案有两种一种是直接用一个整数字段比如status0表示草稿1表示待审核2表示审核通过3表示已驳回另一种是用字符串字段存状态码比如PENDING、APPROVED、REJECTED。两种方式我都实测过如实说整数字段在小项目里更顺手排序、筛选、条件判断都方便写在代码里也不容易眼花。但要注意状态字段不能只在项目申报表里放一个如果审核是多级的每一级的审核结果都得有地方存。我的做法是申报表里存“当前状态”审核记录表里存“每一级的审核明细”。当前状态是用来给列表页快速筛选的审核明细是用来给详情页展示流程轨迹的。两者配合既不丢上下文查询效率也高。还有一个细节值得留意就是状态变更的历史记录。虽然审核记录表已经留了痕但如果申报人在审核前修改了自己的申报内容这个修改动作也该记录下来。我习惯在申报表里加update_time同时用version字段配合乐观锁防止并发提交把别人的修改覆盖掉。实测下来这个投入非常值尤其是在申报截止日期前后大量用户同时提交的场景下乐观锁能避免很多莫名其妙的脏数据问题。状态机的流转逻辑我建议在Service层封装一个专门的方法不要散落在Controller里。比如submitApprove、approvePass、approveReject、withdraw每个方法里校验当前状态是否允许执行这个操作不允许就直接抛业务异常。这样代码的可读性会好很多后面如果要加状态也只需要改一个类。2.2 附件上传与预览的实现细节做项目申报系统附件是跑不掉的。申报书、可行性报告、营业执照扫描件、预算表……我见过不少项目在这一块栽跟头主要问题集中在文件类型校验不严、文件大小没控制、存储路径一团乱。这里我强烈建议一个方案web项目里的文件上传本地存储和对象存储两者都考虑但是本地存储始终是优先可用的兜底方案——毕竟学生做毕设、小团队做内部系统不可能真的去买一个对象存储服务本地存储配合Nginx静态访问已经完全够用。附件表我一般这么设计id、project_id关联申报表、file_name、file_path、file_size、file_type、upload_time。这里有个关键点数据库里存的是文件的相对路径不是文件的二进制内容。文件本体放服务器磁盘路径存库两者配合使用。这样设计的好处是显而易见的数据库性能不会被大字段拖垮文件预览和下载只需要拼个URL就行。上传接口要做三个校验扩展名校验、大小校验、MIME类型校验。扩展名校验是最基础的但也不能只信前端传的扩展名后端要根据文件头去判断真实类型。比如JPG文件的文件头是FF D8 FFPDF文件头是25 50 44 46用Apache Tika或者手动读文件头都可以。初学者可以先用扩展名白名单顶着但心里要清楚这是个隐患。文件预览这块我实测下来的经验是图片直接返回文件流让浏览器自己展示PDF用Content-Disposition头搞成内联预览Word和Excel文件处理起来比较复杂只能提供下载。很多项目管理系统的附件预览都折在Office文件上如果有人硬要预览可以花功夫集成OnlyOffice或者kkFileView这类在线预览服务但成本会高不少。对于毕设或中小型项目提供下载就足够了不用为了这个功能把自己搞得太累。3. 后端工程落地的核心逻辑从POM依赖到权限认证后端选用SpringBoot 2.7.x MyBatis-Plus 3.5.x这一套组合是经过反复比较后确定的。SpringBoot2.7是2.x系列的最后几个版本稳定性和社区资料储备都是最充分的坑基本都被踩平了。MyBatis-Plus作为MyBatis的增强工具不用写XML就能完成大部分单表CRUD内置的分页插件、逻辑删除、代码生成器都是实战频率极高的功能对申报系统这种表单密集型项目来说开发效率能翻一倍。有人可能会问为什么不直接用Spring Data JPA说实话JPA在复杂查询和SQL调优上不如MyBatis-Plus来得直接。申报系统里“按状态统计”“按申报时间范围过滤”“多条件联合查询”这类需求太多了JPA的Specification写起来冗长且不好读而MyBatis-Plus的LambdaQueryWrapper写起来就像自然语言一样流畅。更重要的是国内做Java后端的技术栈MyBatis/MyBatis-Plus的占有率极高团队协作时大家上手更快网上的现成方案也更多。后端工程的包结构推荐按业务模块划分而不是按技术层划分。我习惯这样组织controller—— 只做参数接收和响应封装不写业务逻辑service—— 业务逻辑的所在地接口实现类的形式方便做事务控制mapper—— 继承 MyBatis-Plus 的BaseMapper复杂的SQL用注解或XML补充entity—— 数据库表对应的实体类字段用驼峰命名映射下划线列名config—— 配置类比如MyBatis-Plus分页插件、CORS配置、拦截器注册common—— 统一响应体、异常处理、工具类事务这块要特别强调审核、申报这种操作必须加Transactional。比如提交申报时要同时更新申报表状态、写入审核记录、可能还要给审核人发站内信任何一个步骤失败了都不能留半截数据。3.1 MyBatis-Plus代码生成器把CRUD代码一键生成这也是我用MyBatis-Plus最上头的功能代码生成器。写好数据库表之后用生成器一次性生成实体类、Mapper接口、Service接口、Service实现类和Controller基础CRUD代码基本不用手写。配置起来也不复杂在测试类里跑一个main方法就行。生成之前要先想好表前缀处理。比如表名叫t_project生成实体时默认会变成tProject看着很别扭。生成器配置里有一个DbConfig把表前缀去掉就行实体类就叫Project对应的表MP自动识别为t_project一行配置都不用多写。生成的Mapper接口继承BaseMapperProject之后selectById、insert、updateById、deleteById这些基础方法就全都有了。常用的批量操作像selectBatchIds也是内置的不需要自己写循环。至于条件查询不用提前生成直接在业务代码里用LambdaQueryWrapper动态拼接就行。我用下来最大的感受是别再羡慕别人家的项目为什么写代码那么快了靠的就是这些工具在自己挖空心思想名字的时候早就把代码生成了。但代码生成器生成的东西也有需要调整的地方。比如Controller返回的格式生成器默认给的是ModelAndView或裸对象但实际项目里需要统一的响应体格式。我一般会改一下模板把返回类型统一换成ResultT不用每次都手动修。另一个要改的是Controller里的Swagger注解生成的注解字段不完整要自己补上不然生成的API文档会缺参数说明看着不够专业。3.2 统一响应体与全局异常处理前端在对接接口时最怕遇到什么就是后端返回格式不统一。一会儿返回String一会儿返回Map一会儿直接返回404页面。前端axios拦都拦不住根本没法写。所以在第一版框架里就必须统一响应格式。我用的是最简单也最好用的方案定义一个泛型类ResultT固定包含三个字段code状态码、message提示信息、data业务数据。成功时code为200业务失败时code为500或自定义错误码未登录时code为401。Controller每个接口都返回这个类型由Spring的ResponseBody自动序列化成JSON传给前端。对应配套的是RestControllerAdvice全局异常处理器。这一段是很多人忽略但极其关键的部分。如果不做全局异常处理MyBatis-Plus抛出的DuplicateKeyException、业务层抛出的自定义异常、参数校验失败的MethodArgumentNotValidException默认的异常响应在数据格式上五花八门前端根本没办法统一处理。我的做法是自定义一个BusinessException携带错误信息和错误码业务代码里直接throw new BusinessException(申报窗口已关闭)全局异常处理器里写ExceptionHandler(BusinessException.class)把错误码和消息放进取Result返回其他未知异常统一返回500并且日志打印异常堆栈方便定位问题参数校验这块我直接用JSR 303的Validated注解加在Controller入参上实体类的字段上用NotBlank、NotNull、Min这些注解标记规则。比如申报标题字段加NotBlank(message 申报标题不能为空)提交表单时如果没填全局处理器就会捕获校验异常自动把message返回给前端。这套组合拳打下来controller里的代码会非常干净都是纯粹的参数接收和调用service一堆校验逻辑全都不用写在业务代码里。3.3 登录鉴权与接口拦截申报系统的登录鉴权我用的是JWT拦截器的方案算是SpringBoot里最主流也最容易上手的一套。让用户拿着用户名密码来换取一个token字符串后续每次请求都在Header里带上这个token。服务端不需要保存会话状态天然适合前后端分离的应用。如果要更省事用Spring Security加JWT也行但配置复杂度高不少对入门级项目是杀鸡用牛刀。JWT的生成和校验推荐用io.jsonwebtoken:jjwt这个库。生成token时要设置过期时间我一般设置2小时或者更长一点按业务需求调整。登录成功后把token返回给前端前端存在localStorage里之后每次请求都通过axios的请求拦截器加上Authorization头。拦截器这块要处理好两个问题。一个是排除名单比如登录接口、注册接口、验证码接口、静态资源路径都不需要登录一个是用户信息的传递。我习惯在拦截器里解析token之后把用户ID和角色信放进ThreadLocal或者request的attribute里这样Controller层直接取用不用每个接口都自己解析一遍token。这里有个实测中的坑开发时前端用axios请求后端拦截器发现在不在放行名单里的接口就会直接抛401前端明明带了token也进不来。排查了半天发现是token里解析出来的用户ID是null因为生成token时没把用户ID放进去。所以生成token时除了放用户名用户ID和角色这两个字段也必须放进去后面所有接口要取当前登录人信息都从这里来。权限控制的方法直接在拦截器里做或者用注解都行。想简单点就在拦截器里判断角色的字符串管理员的接口如果被申报人访问就直接拒绝。想优雅点就写一个RequireRole(ADMIN)注解用AOP拦截。两者效果差不多看个人喜好。面试的时候能说出来“通过AOP自定义注解实现权限控制”这东西的含金量马上不一样。3.4 分页条件查询的标准写法和性能优化申报列表页、审核列表页、用户列表页全部都是分页条件查询的组合。MyBatis-Plus的分页插件配置很简单就是一个PaginationInnerInterceptor配置到MybatisPlusInterceptor里就完事了。之后在Service里调page方法传入Page对象就不会有全量查询的问题。组合条件查询的标准写法我用的是LambdaQueryWrapper动态拼接。比如管理员在前端输入项目名称、选择状态、选择申报时间段前端把这些参数传过来后端在wrapper里逐个判断参数是否为空不为空就拼接条件LambdaQueryWrapperProject wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(projectName), Project::getName, projectName) .eq(projectStatus ! null, Project::getStatus, projectStatus) .ge(startTime ! null, Project::getCreateTime, startTime) .le(endTime ! null, Project::getCreateTime, endTime); PageProject page projectMapper.selectPage(new Page(pageNum, pageSize), wrapper);MyBatis-Plus这种Condition形式的写法天然就适配“参数可不传”的场景条件不满足就自动忽略。如果参数之间还有复杂的“或”逻辑就得用and(innerWrapper)嵌套或者直接SQL联查没必要强行用wrapper。我实测的体会是wrapper适合解决90%的单表条件查询超过这个范围的直接写SQL反而更清晰。列表接口返回的数据量要控制好尽量别把大字段比如项目申报的详细内容、附件路径列表全量返回不然列表页加载速度会很感人。我的方案是列表接口只返回概要字段项目详细内容单独用一个详情接口去查。附件列表可能比较多详情接口里再一并处理。前端列表页的体验和数据体量之间的平衡往往就是在这种细节里体现的。4. Vue3 前端工程的搭建思路Composition API与接口层设计前端这块用Vue3搭配Vite和Element Plus是当前Vue生态里最顺手的组合。Vite的冷启动速度比Webpack快一个数量级开发体验完全不在一个水平线上。Vue3核心的Composition API让逻辑复用变得优雅多了同一个项目里如果某个功能在两个页面都要用可以抽成一个useXxx函数各页面引进来各用各的互不干扰。很多刚从Vue2切过来的同学容易犯一个毛病就是「旧瓶子装新酒」——依然用Options API写法把逻辑塞在data、methods、watch这些旧选项里对setup函数和ref、reactive这些API本能地抗拒。这样写也不是不能跑但完全没吃到Vue3的红利。我的建议是既然用到Vue3了就认真用Composition API把逻辑拆开你会明显感觉到代码的条理性比Vue2时期好很多。Element Plus是目前Vue3配得最舒服的UI组件库表单、表格、弹窗、上传、日期选择器全都齐。这个项目里的申报表单、审核列表、用户管理页面基本都能用现成组件拼出来。我唯一要提醒的是Element Plus的组件是按需自动导入的用官方推荐的unplugin-auto-import和unplugin-vue-components两个插件时组件会在模板里自动注册不用手动引入但图标还是要自己按需引入才行。这里有经典的一幕大家追着问“为什么我拿示例代码跑起来图标不显示”十有八九就是漏了这一步。4.1 axios封装与请求拦截器的正确写法前端所有请求都从axios一个实例发出去所以必须在入口处做好统一封装。我的标准封装里必备两个拦截器请求拦截器和响应拦截器。请求拦截器负责从localStorage拿token放到请求头里响应拦截器负责统一处理业务码和HTTP状态码把后端返回的Result结构解包把核心数据和可能的异常消息暴露给调用方。响应拦截器里一个重要的决策是全部用resolve回调还是只有成功才resolve。我建议的写法是HTTP状态200时进入resolve分支然后校验业务code如果业务code不是200其实在后端眼里这已经是业务失败了但我不会在拦截器里把Promise断掉——弹错误提示然后resolve一个“假的失败数据”让调用方自己判断这样代码读起来更顺。但更推荐的做法是业务code失败时reject掉调用方用try/catch捕获这样不会漏掉错误情况的处理。举个例子调用申报接口时export function submitApproval(data) { return request({ url: /project/submit, method: post, data }) }页面里调用方这样写清晰得很try { await submitApproval(formData) ElMessage.success(提交成功) } catch (e) { console.log(提交失败:, e) }如果后端统一了一套异常返回axios拦截器里只要写一处ElMessage.error整个项目的错误弹窗体感就都统一了。关键就是要保证后端返回的code和message是规范统一的这是前后端合作的重中之重后端不统一前端封装得再好也是白搭。4.2 申报表单的校验规则与动态字段申报表单是整个系统里最复杂的表单可能包含项目名称、申报类别、负责人信息、预算金额、项目简介、附件上传等等。Element Plus的el-form配合rules做校验是标准操作但有几个细节值得注意。第一日期选择器校验。我用type: date来校验这个字段但动态表单里日期组件的返回实际是一个Date对象如果后端需要字符串就得在提交时做格式化或者在v-model上绑定一个自定义格式化函数。这个坑很多同学都踩过前端校验通过后端收到发现类型不对来回排查头疼得很。第二动态增减字段。有的申报表单会有“项目成员名单”这种可动态增删的区块每行都是一个子表单。这种场景用v-for渲染一组el-form-item校验规则的写法要特别注意给每个字段的prop起一个带索引的名字比如members.0.name。这样一行填写出错校验时能精确指向是哪一行的问题体验才完整。第三附件上传。Element Plus的el-upload默认行为是选完就自动上传如果想延后到表单一起提交就得配置auto-uploadfalse手动拿到fileList在提交时一起发送。这是申报系统非常常见的需求却经常被当默认功能处理。我建议的做法是上传附件时立即传给后端后端存好路径并返回附件ID前端表单里的附件字段只存这个ID列表。这样即使申报人最后没提交整个表单附件也挺在后端存着纯本地等表单一起提交的话最终只会遇到请求体过大或者路径对不上的无解问题。4.3 列表页的筛选、分页与状态标签列表页无论是申报人视角还是审核人视角基本形态都一样顶部一个筛选栏关键字搜索、状态下拉、时间范围中间一个数据表格底部一个分页器。在Vue3里把这些组合起来前我建议先封装一个usePagination的Composable把当前页、页大小、总数、加载列表这些重复逻辑都抽出来。状态标签这块推荐用Element Plus的el-tag配合枚举映射显示。比如后端返回的状态值是数字1、2、3前端映射成一个包含文字、颜色的配置对象展示的时候根据值取对应的配置渲染。审核列表里待审核的显示成橙色标签已通过的显示成绿色标签已驳回的显示成红色标签一眼扫过去状态非常清楚。前端不要写死if/switch去逐条判断搞一个映射表才是正经做法新增状态只需加一行配置。操作列是列表页的另一个重头戏。申报人可以操作用的按钮就有查看详情、修改、提交、撤回、删除审核人则是通过、驳回、查看。按钮要根据状态显示或隐藏比如已经提交的项目就不能再点修改按钮已经驳回的项目要允许重新提交。这块逻辑建议抽出一个getActions(row)函数根据状态动态返回按钮配置组件里用v-for渲染模板会非常干净。4.4 路由守卫与登录态的持久化前端路由守卫是登录鉴权的第一道关卡。用Vue Router的beforeEach钩子在路由跳转前检查用户是否已登录读取localStorage里的token没有token就让路由去登录页。有token但没登录信息比如页面刷新后用户信息丢失调用一次获取用户信息的接口来恢复登录态。这套逻辑写下来用户一旦没登录即使在地址栏手动敲进入管理页的路由也会被挡在登录页外面。角色权限的路由控制可以在路由的meta字段里标上roles: [ADMIN]守卫里判断当前用户的角色是否在meta的角色列表内不在就跳到一个403页面或者提示无权限。实现起来非常简单但效果很明显——不同角色登录系统后看到的菜单和页面天然不同。登录态持久化方面要强调一下token放localStorage用户基本信息也是。刷新页面后从localStorage直接恢复不用再请求一次。但如果用户改了密码或者管理员拉黑了一个用户为了稳妥可以在请求时接口返回特定code比如401响应拦截器就清空localStorage并跳转登录页。这是目前最成熟省心的前端鉴权模型项目的绝大部分场景都能覆盖。5. 开发环境搭建手册JDK、MySQL8.0与Node环境一站式跑通整这套环境我踩过不少坑把相对顺滑的流程写出来。先列一份清单JDK 1.8或11、MySQL 8.0、Maven 3.6、Node.js 16、Vite 4。JDK版本首选8兼容性最稳但如果要体验新特性也可以上11。Maven镜像源建议直接换成阿里云不然每次下载依赖都会等到怀疑人生。MySQL 8.0的安装Windows下用官方installer最省事。安装过程中会要求设置root密码顺手记好。最容易被忽略的一步是安装完成后默认只监听localhost后端连接串如果写127.0.0.1没问题但如果是远程拿Navicat去连记得要改用户或者建一个远程访问账号。MySQL8.0的认证插件默认是caching_sha2_password有的老版本驱动不支持后端JDBC连接报错就是这种问题连接串加一行allowPublicKeyRetrievaltrue就能解决或者把用户认证改回mysql_native_password。这个坑十个人里八个会遇到。Node.js环境安装完Vite项目跑起来的命令就两句话npm install和npm run dev。如果npm install慢就用registryhttps://registry.npmmirror.com这个淘宝镜像源。我通常直接在项目根目录加一个.npmrc文件写明registry一劳永逸。Vite默认端口是5173跟后端的8080不是同一个源所以跨域问题必现接下来这部分我们专门聊。5.1 前后端跨域问题的三种解决方案开发阶段最常见的就是跨域报错浏览器控制台刷一串红了CORS policy、Access-Control-Allow-Origin。三种主流方案里项目级别的方案是后端加全局CORS配置这种方式最省心。SpringBoot里写一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这样配置完前端的请求就能直接打到后端8080端口。但要注意加了spring security或者拦截器的话预检请求OPTIONS得放行不然跨域能过但认证不过请求还是失败。这个细节坑过我两次拦截器里把OPTIONS的路径加进放行名单才消停。另一种方案是靠前端开发服务器代理在Vite的vite.config.js里配置proxy。绕开浏览器同源策略让前端的/api路径代理到后端的http://localhost:8080。这个方案我实测比CORS配置还要顺手因为生产环境部署时反向代理也是这么搞的不用改后端代码。第三种方案是Nginx统一代理这个属于生产部署的话题了本地开发一般用不着但知道有这么个思路就行。5.2 本地启动整套系统的完整步骤把后端和前端都跑起来很快按下面这套来就顺了。后端项目打开后第一步改配文件application.yml。数据库名、用户名、密码、端口都填好连接串里注意MySQL8.0的驱动类名是com.mysql.cj.jdbc.DriverURL里要加serverTimezoneAsia/Shanghai。然后直接启动SpringBoot的启动类控制台会输出Tomcat启动日志。如果启动报错优先怀疑数据库没连上日志里看得到连接失败的明确报错。前端项目打开后第一步npm install装完依赖再npm run dev浏览器自动跳到localhost:5173。如果登录时报错就先打开浏览器的开发者工具看Network是请求没发出发出了但被CORS挡了还是发出了后端返回了500。这一步能过滤掉90%的问题。首次登录建议先用管理员账号我在SQL脚本里默认插入了admin/admin123这个账号登录后先进用户管理页面重置申报人密码再去验证完整流程。自己系统的账号密码建议写到项目README里上机演示时能少一多半的时间到处翻文件夹。5.3 生产环境的部署思路前后端分离的Nginx配置毕设答辩或者企业真正上线时本地跑前后端两个进程肯定不行。标准做法是后端打成jar包跑在服务器上前端编译成静态文件交给Nginx托管Nginx再配一层反向代理把/api转发到后端。Vue3项目跑npm run build产物会生成到dist目录把这个目录整个扔到Nginx的html目录下就行。典型的Nginx配置片段长这个样子server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这里最核心的是try_files $uri $uri/ /index.html这一行它解决了Vue Router的history模式刷新404问题。没有这一行用户在系统里按F5刷新路由跳转到/project/list会直接404。加上了所有不存在的路径都会被回退到首页由前端路由接管。实测下来刚接触部署的同学最容易漏的就是这一行配置。心跳保活和MIME类型这些细节Nginx都会自动处理不用额外操心。HTTPS现在也是标配但在毕设项目里可以先不配答辩时能跑通HTTP就已经符合要求了。6. 高频踩坑实录从MyBatis-Plus批量操作到MySQL8.0驱动疑难这里把我在开发申报系统过程中真实踩过、排查过的高频问题系统整理一遍。这些问题在官方文档里不一定写得那么直白但任何一个都能让新手卡住半天。第一个MyBatis-Plus的批量插入。MP内置的saveBatch方法是依赖JDBC批量处理来提升效率的但有两个前提必须满足连接串里必须加rewriteBatchedStatementstrue否则批量操作其实是一条条执行性能完全没体现数据库方言和驱动版本也得匹配。加了这行配置后批量插入的耗时按比例下降得非常明显。有一条两千行的Excel导入数据时这个配置直接让导入从卡了二十多秒变成三秒不到。第二个LambdaQueryWrapper的like条件在字段值为空时会报错。原因是没有传入condition参数null值的情况下like仍然执行了一次MyBatis-Plus在拼接SQL时就会产生异常或多余的条件严重时直接导致SQL语法错误。正确写法是wrapper.like(StringUtils.isNotBlank(name), Project::getName, name)把条件放第一个参数位这一点最容易掉坑。第三个MySQL8.0驱动版本与连接串问题。驱动类名要用com.mysql.cj.jdbc.DriverURL里的serverTimezone一定别丢时区错误报错时语法怪异且不明不白。我见过有人在连接串写useSSLfalse还配了sha256密码报错其实压根不是加密的问题就是时区没写对。这个排查经验值得记下来。第四个MySQL8.0用户名密码丢失了怎么办。Windows下编辑一下my.ini加一行skip-grant-tables重启MySQL服务无密码登录进去改密码改完把这行配置删掉再重启。这一套是 MySQL DBA 圈子里最经典也最实在的抢救手段写下来备用挺好。第五个前端npm install失败建议先查镜像源不要一条一条挨个试直接看registry是不是默认的官方源换成国内镜像基本都解决了。Vite构建报错基本都是ESLint或TypeScript校验的锅临时可以先跳过校验。第六个前后端联调时后端改了代码前端没生效。最坑的时候排查了半天发现是浏览器缓存都没清后端重启过、前端热更新过但浏览器里跑的依旧是旧版JS。开发时给Vite配置server.hmr.overlay或者手动强制刷新能规避大半的莫名其妙问题。我把这些按“问题症状、原因、解决步骤”的格式整理成一张表方便你留在项目文档里也方便答辩或者项目评审时快速查阅问题现象根本原因解决步骤saveBatch批量插入极慢缺少rewriteBatchedStatementstrue连接参数加上该配置增删改查时字段值为空竟然拼接SQL并报错like未传condition参数条件判断用StringUtils.isNotBlankMySQL连接报时区错误URL未配置serverTimezone连接串加上serverTimezoneAsia/Shanghai前端跨域报CORS错误前后端不同源未配置代理/CORS配置Vite代理或后端CorsConfignpm install卡死默认仓库源速度慢切换npm镜像源并验证registry后端改了代码前端无反应浏览器缓存/HMR失效强制刷新检查热更新状态Vue路由刷新404部署时缺少try_files指令配置try_files $uri /index.html7. 文档编写与项目交付的要点建议最后这部分聊聊很多人到了收尾阶段才开始慌的事情。整个项目代码写完了跑通了突然发现老师或者接手的人问项目文档呢系统说明书呢这个瞬间大概是最真实的毕业设计疼痛了。我的建议是文档不要拖到最后从建表那天就开始写。每次改完数据库、写完某个功能模块顺手往文档里补几行。最后汇总时就不是焦头烂额地凑字而是静心梳理思路而已。写系统说明书时我一般按这么几个章节来组织项目背景与需求分析、技术选型说明、数据库设计关键表结构E-R图、系统功能设计分角色描述用例、核心功能实现说明贴关键代码并配解释、系统测试与运行效果。核心代码不要大段大段贴截取关键方法的逻辑就好了配上时序图或流程图说明调用关系。把“为什么用这个技术、这块实现难在哪、我当时怎么解决的”写清楚。这样一份文档的含金量可比单纯贴出几百行代码高太多了。答辩时最容易翻车的点就是对核心业务流程的表述不流畅。比如“项目提交以后状态会变成待审核审核人在待办列表里能看到点击审核通过后项目变为已通过申报人可以在我的申报里看到结果”。这个链路的因果关系必须门儿清就照着数据库里的状态流转说即可。交付时还有一个细节完整源码目录里必须包含初始化SQL脚本和README。SQL脚本要能用数据库客户端一导入就完整建表不要再让接手的人手动造表了。README里写清楚系统账号、运行环境要求、启动步骤、部署说明。这几份文件放到位整个项目交付体验会非常专业也能给评审留下好印象。我很负责任的讲跟着这个思路走一遍从数据库建模到前后端联调再到文档交付整个项目申报系统的开发周期会有很显著的缩短。技术选型贴合实际场景代码结构清晰干净业务流程完整闭环无论是毕业答辩还是真实业务落地这套组合都能站得住脚。如果你正在做类似的项目最大的建议是先花一个晚上理清楚业务流程再动手写代码后面你会感谢当时沉住气的自己。