如果你正为毕业设计选题发愁,手头刚好有个“基于Spring Boot的招标系统”这样的题目,那这篇文章就是为你准备的。我不是来讲PPT式废话的,而是把这个题目从需求拆分、技术选型、数据库设计,到核心功能实现、踩坑实录,一条龙讲清楚。这个课题能锻炼的东西很实在:Spring Boot的核心用法、权限控制、文件上传、状态流转、以及一套完整业务的前后端联调逻辑。适合想拿它当毕设、或者刚入门Java后端想做一个“真正完整系统”的读者。全文我会按自己做项目时的思考顺序来写,你跟着走一遍,就能知道该从哪下手、每个环节到底图什么。
1. 选题拆解:先搞懂招标系统到底在做一件什么事
1.1 业务角色与核心链路
拿到这个题目,第一步不是急着建项目、引入依赖,而是先弄清楚招标系统要解决什么问题。招标的线下场景很好理解:一家单位需要采购一批设备,但直接指定供应商容易出问题,于是公开招标,让多家供应商来投标,由评标专家打分,选出一个综合最优的。线上系统就是把这个过程搬上网。
所以你首先要识别出系统里的角色。一般至少有四类:管理员(系统运维)、招标方(发布项目)、投标方(供应商)、评标专家。管理员管用户和权限;招标方发起一个招标项目,填写公告信息;投标方看到公告后报名,在截止时间前上传投标文件;等到开标时间,专家登录系统给各家投标文件打分;最后系统或管理员汇总分数,发布中标结果。
这个核心链路对应到系统功能上就是:用户登录注册、招标项目管理、投标报名与标书上传、评标打分、中标公告。再把链路外围补上,就需要数据字典管理、公告管理、操作日志、或者简单的消息提醒。我见过很多同学一上来就堆功能,弄了一堆花哨但没闭环的模块。其实毕设好不好,首先看核心链路是否完整走通,这比功能数量重要得多。
画一张业务流程图(我强烈建议你准备答辩时画清楚这张图),把以上几个环节按照时间顺序串起来:招标方发布 → 供应商投标 → 截止 → 开标 → 专家评标 → 公示结果。这个图就是你整个系统的骨架,所有表结构的设计都得围绕它展开。
1.2 为什么Spring Boot是这道题的标准答案
你可能会疑惑,Servlet、SSH、SSM也能做,为什么题目偏偏写Spring Boot?因为Spring Boot把Spring生态的配置复杂度压缩到了最低,让开发者能把注意力集中在业务逻辑上。它通过自动装配机制,把原来需要手写一大堆XML配置的组件一次性注入容器;你引入一个spring-boot-starter-web,内嵌Tomcat,main方法一跑,项目就起来了。
从毕设角度来说,选Spring Boot有三个实际好处。第一,招聘市场需求大,面试官普遍认可,答辩时技术含量不会被质疑。第二,社区资料非常多,任何模块遇到问题都能搜到解决方案,这对单打独斗的学生非常友好。第三,它有清晰的工程结构约定,你按规范写出来的代码,就算细节粗糙一些,整体观感也不会太差。
不过要提醒一点,Spring Boot的自动装配虽然省事,但答辩时很容易被追问原理。你必须理解几个关键点:@SpringBootApplication是由@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan组合而来的;自动装配的核心是读取META-INF/spring.factories(或Spring Boot 2.7之后有新的导入方式)里的自动配置类列表,再由@ConditionalOnXxx条件注解按需生效。这些是面试的高频问题,也是你证明自己不是只会“跑起来”的关键。
2. 技术栈选型:每一层都要能说清“为什么”
2.1 版本选型的避坑指南
技术栈不是越新越好。Spring Boot目前主流有三代:2.x系列(2.7.x是2代最后一个稳定小版本)、3.x系列(需要JDK17)。很多学校机房或者你自己电脑装的还是JDK8,那Spring Boot 3.x根本启动不了。所以我的建议很直接:如果没有硬性要求,选Spring Boot 2.7.18 + JDK8最稳妥,它能搭配MyBatis、MyBatis-Plus、Sa-Token、Redis等绝大多数常用中间件,资料全、坑少、跑起来顺。
有的学校要求新,或者你确实想用JDK17,那可以选Spring Boot 3.2.x。但要注意几个连带问题:JDK17下Lombok版本必须够新;MyBatis-Plus 3.5.x以上才原生支持Spring Boot 3;部分网上老教程的依赖写法在新版本里会直接启动报错。所以版本选型不只是改一个数字,而是连带着整个依赖矩阵的兼容性。我个人的观点是,毕业设计的关键是稳定出成果,2.7.x是风险最低的路线。
再看配套组件:数据库用MySQL 5.7或8.0都行,但要注意MySQL 8的驱动类名和时区配置与5.7不同。Redis如果做缓存或记录在线状态,选一个就行,别为了凑技术点硬上一堆中间件,后面联调时全是噩梦。
2.2 ORM、权限、前端怎么配
ORM层我推荐MyBatis-Plus,理由是它正好卡在“原生MyBatis需要写大量XML”和“JPA自动化程度过高、复杂查询难控”之间。它提供BaseMapper通用的单表CRUD、分页插件、条件构造器LambdaQueryWrapper,写代码效率很高;同时又保留了你手写SQL的余地。对标答辩常问的“为什么不用JPA”,你可以说:招标系统存在大量的动态条件查询和表关联统计,MyBatis-Plus的查询可控性更好,而且性能调优时定位SQL更直观。
权限控制有两个主流路线:Spring Security + JWT,或者Sa-Token。Spring Security功能最强,但上手曲线特别陡,很多同学配置半天连登录都弹不出来;Sa-Token是国人开源的一套轻量鉴权框架,提供了登录、权限认证、踢人下线等现成API,对毕设来说足够且理解成本低。我个人诚实的建议是,如果时间充裕且想提升含金量,可以啃一啃Spring Security;如果目标是两周之内把系统完整跑通,那Sa-Token能帮你省下大量时间。
前端方案有两条路:用模板引擎Thymeleaf做服务端渲染,或者用Vue做前后端分离。如果答辩时需要展示“前后端分离架构”,那就上Vue 2 + Element UI或Vue 3 + Element Plus,搭配Axios调接口。如果前端你不太熟,又想省事,Thymeleaf结合Bootstrap也完全够用。但要清楚一个区别:前者意味着你要处理跨域、Token存储、接口联调,工作量更大却更“像企业项目”;后者前端简单,后端把页面和数据一起返回,架构会被认为偏传统。这个取舍你根据自己精力来定,不用盲目跟风。
2.3 项目结构:包名和分层是给答辩老师的第一印象
代码结构的分层,直接影响老师对你的第一印象。我见过不少项目所有代码堆在controller包底下,一个Service都没有,业务逻辑全写在接口里,这种即使功能跑通也会被质疑工程能力。一个规范的Spring Boot项目,应该至少有如下结构:
- entity(数据库实体映射)
- mapper(数据访问层接口)
- service(接口定义)和service.impl(业务实现)
- controller(接口层)
- config(配置类:跨域、线程池、过滤器注册)
- common(公共类:统一返回体、异常处理、常量)
- util(工具类:JWT工具、文件存储工具)
分层思想很简单:Controller只做参数接收和结果封装,不写业务逻辑;Service负责业务规则,比如校验截止时间、防止重复投标;Mapper只做数据读写。这样你答辩时说“我用了分层架构,各层职责清晰、便于维护”,才有底气。另外,统一返回体也建议一开始就定义好,比如Result对象包含code、msg、data三个字段,所有接口返回它,前端处理数据时就能统一拦截错误状态,不必每个接口单独写判断。
3. 数据库设计:招标系统的地基
3.1 核心表清单与关系梳理
数据库是这类系统最见功夫的部分。别急着建表,先把实体关系画清楚。我梳理出来的一套最简核心表结构如下,你可以根据自己业务增减:
- sys_user(用户表):存账号、密码(BCrypt加密存储)、姓名、手机号、角色等;
- sys_role(角色表):角色编码、名称;
- user_role(用户角色关联表):用户和角色是多对多,一个人可以既是管理员又是评标专家,所以用中间表;
- bid_project(招标项目表):存项目名称、编号、招标方、预算金额、报名开始/截止时间、开标时间、当前状态等;
- bid_project_attachment(项目附件表):存招标文件的附件,一个项目可多个附件;
- bid_record(投标记录表):一个供应商对某一个项目的一次投标,存投标企业、联系人、报价金额、上传文件路径、投标时间;
- evaluation_score(评标评分表):专家对某个投标记录的某项指标打分;
- evaluation_committee(评标委员会或评标任务分配表):记录哪个项目由哪些专家参与评标;
- notification(公告/通知表):存中标公告等公示信息。
概括一下:用户和项目是多对多(通过投标记录关联),项目和专家是多对多(通过评标任务关联),投标记录和评分表是一对多。表之间的外键逻辑必须明确,不能用“一个字段搞定所有关联”的设计。重点提醒:外键在物理上我建议不建,除非老师有要求。因为企业开发普遍不使用物理外键,而是用逻辑外键保证灵活性和性能,这类细节在答辩时能体现出你接触过真实项目规范。
3.2 两张必须花心思设计的表
第一张是招标项目表。状态字段我建议用int类型存储,而不是字符串。比如0代表草稿、1代表已发布、2代表投标中、3代表已开标、4代表评标中、5代表已定标、6代表已结束。用整数的好处是查询和判断都方便,还方便扩展新状态;到前端展示时再用枚举或字典翻译成中文。预算金额必须用decimal,比如decimal(12,2),绝对不能使用double,因为浮点数会有精度误差,最后算评标得分、算中标金额都会出错。
第二张是评标评分表。它承载了系统的“打分”核心逻辑。建议字段至少包括:evaluation_score表主键id、评标任务id或项目id、专家id、投标记录id、评分项(如商务分、技术分、价格分)、单项得分、备注。每个专家可以给同一份标书多个评分项打分,所以表的设计要能支持“按专家-按投标文件”唯一约束,防止同一专家对同一标书重复打分。最终总分可以实时计算,也可以加一个冗余字段存放汇总结果——毕设阶段用实时计算更省事,还能让代码逻辑更清晰。
3.3 让流程状态可追踪
很多同学做这类系统,把状态变化直接写在业务代码里,今天加一个状态,明天改一个判断,越写越乱。更稳的方式是设计一套“状态机”。虽然不一定需要引入状态机框架,但脑子里要有一个清晰的流转图:草稿只能被发布,发布后进入投标中,投标截止后进入已开标,开标后才能评标,评标结束才能定标。
这个流转的好处是每个操作都要校验当前状态是否允许。例如在Service层写一个ensureProjectStatus(project, expectedStatus)方法,状态不对就抛业务异常。在并发情况下,还可以用MyBatis-Plus的@Version乐观锁,或者更新SQL里带上WHERE status = oldStatus。比如执行“开标”操作时,条件拼接上WHERE id=? AND status=2,若返回影响行数为0,说明有人在并发操作,拒绝执行。这种细节能直接成为答辩时讲“如何保证数据一致性”的素材。
4. 核心功能落地:从需求到可运行的代码
4.1 登录与权限:RBAC模型的落地方式
登录认证我建议按RBAC来。所谓RBAC,就是用户-角色-权限三层关系。用户登录后,后端返回一个Token,前端之后每次请求都把它放在请求头Authorization里;后端的登录拦截器或鉴权框架会解析Token,拿到当前用户ID和角色,再判断有无权限访问对应接口。
如果用Sa-Token,核心流程是:首次登录校验账号密码,密码用BCrypt加密对比,成功后调用StpUtil.login(userId),后续接口直接用@SaCheckPermission("system:user:add")这类注解控制权限。如果自己写拦截器,逻辑也不复杂:写一个HandlerInterceptor,在preHandle方法里校验Header中的Token,再把用户信息存入ThreadLocal。注意一点:拦截器里必须放行登录接口、静态资源和Swagger路径,不然会出现“登录页能打开,但验证码加载不出来”这类奇怪问题。
注册功能还要注意:招标系统里供应商注册后要填写企业名称、统一社会信用代码、法人、营业执照等信息,需要审核通过后才能参与投标。这个“注册→审核→才可投标”的动作不要省略,否则系统语义就不完整了。你可以让管理员在用户管理里点“通过审核”,用一个字段account_status来控制(0待审核、1正常、2禁用)。
4.2 招标公告发布与分页查询
招标公告发布的后端接口是所有页面数据流转的起点。一个典型的Controller方法大概长这样:接收前端传来的BidProject表单与附件,先做参数校验(比如截止时间不能在当前时间之前),再存入数据库,返回统一的Result对象。Service内部要做的不仅是insert一行,还要给项目生成一个唯一的项目编号,比如取“ZB + 日期 + 随机数”,方便前端展示和用户搜索。
列表查询在招标系统中出现频率最高:招标方要查自己发过哪些项目,供应商要查处于“投标中”状态的项目,管理员要查看全量项目。这些都可以用MyBatis-Plus的LambdaQueryWrapper动态拼接查询条件来实现。比如下面的写法:
LambdaQueryWrapper<BidProject> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(keyword)) { wrapper.like(BidProject::getProjectName, keyword); } if (projectStatus != null) { wrapper.eq(BidProject::getStatus, projectStatus); } if (isSupplier) { wrapper.eq(BidProject::getStatus, 2); // 供应商只看投标中的 } wrapper.orderByDesc(BidProject::getCreateTime); Page<BidProject> page = projectMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);分页插件在MyBatis-Plus里只需配置一个MybatisPlusInterceptor注入PaginationInnerInterceptor,之后selectPage就会自动拼接LIMIT语句。答辩的时候,你可以主动讲一下MySQL的limit偏移量深分页问题,说明你知道数据量大了以后offset会越来越慢,有优化的意识——这很加分。
4.3 投标文件上传与下载:最容易出Bug的环节
文件上传这个功能,看起来就是接一个MultipartFile,但真正做起来全是坑。首先是存储目录,我强烈建议不要写死相对路径,因为你打成jar包后程序的工作目录很可能不是你想的那个。正确的做法是在application.yml里配置:
upload: file-dir: /data/bid-system/upload/然后在启动时检查目录是否存在,不存在就创建。文件名必须重命名,千万别直接用用户上传的原始文件名,因为你无法保证它没有中文、特殊字符、甚至路径穿越风险。我通常用UUID + 原始扩展名来命名,存数据库时记录新文件名、旧文件名和文件大小。上传和下载接口都要限制类型,招投标场景一般限定PDF、DOC、DOCX、ZIP,白名单一定要做,扩展名校验不能只依赖前端。
下载时还有一个中文乱码问题。设置响应头时,纯ASCII文件名没问题,但中文文件名需要做URL编码,否则浏览器拿到的是乱码:
response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + URLEncoder.encode(fileName, "UTF-8"));还有一个容易忽略的点:上传文件大小限制。Spring Boot默认单文件最大1MB,超出直接报错,你在application.yml里要把以下配置调大:
spring: servlet: multipart: max-file-size: 100MB max-request-size: 300MB不然你测试时放入一个几兆的标书,会莫名其妙收到异常。另外,因为是招标项目,投标截止时间后不应该允许再上传标书。这个判断逻辑在Service里实现,上传接口先查项目状态和当前时间,再做文件落盘——别把判断放在前端,否则绕过后端接口就直接越权了。
4.4 评标打分:从不带脑子的CRUD到带业务逻辑
评标是招标系统区别于一般管理系统的灵魂模块,也是最容易做出“业务复杂度”的地方。评标流程设计得合理,整个项目的含金量立刻不一样。我的建议是不要做一个单一的“总分提交”接口,而是拆成两步:第一,设置评分项和权重(如商务分40%、技术分40%、价格分20%);第二,专家逐项打分,系统实时计算总分。
在数据库部分,如果评分规则不复杂,可以直接用一个evaluation_score表,每个评分项一行。前端页面按“投标文件 ←→ 评分项”二维表格来展示,专家填写分数,提交时后端校验每项是否在0到满分范围内。计算总分时有两种方式:算术平均分或者加权平均分——毕设用加权平均分更能突出设计感。
打分的核心业务逻辑有三个:第一,专家只能对自己分配到的投标文件打分,不能看别人的打分结果(隔离评审);第二,同一专家不能重复给同一投标记录打分;第三,项目必须处于“评标中”状态才可以打分。这三条用代码实现时,其实就是在Service层先做权限和状态校验,再使用数据库唯一约束做最后一层兜底。建议在evaluation_score表上建一个联合唯一索引(evaluation_task_id, expert_id, bid_record_id),从底层挡住重复提交。这个设计细节,答辩时讲出来绝对是一个加分点。
5. 细节打磨与常见踩坑实录
5.1 安全类:越权、XSS、SQL注入
毕设系统最容易出的安全漏洞,就是水平越权。最简单的例子:用户A把URL里的id改成用户B的id,就能查到B的投标信息。所以凡是涉及根据id查询的业务,先判断当前登录用户是否拥有该数据。比如查询投标记录详情时,不能只放行供应商角色,还必须校验bid_record里的user_id是不是当前登录用户。我在代码里习惯写一个checkPermission方法,所有敏感业务入口都先调一遍,宁可多写几行,也不要留着这个答辩话柄。
XSS攻击在招标系统里也不少见,因为公告、项目名称都涉及用户输入。你可以写一个全局过滤器,或者更简单一点,引入Hutool的XssUtil对请求参数做清洗。这里有个容易踩的坑:如果你写了全局过滤器,一定要对multipart/form-data类型的请求放行,否则过滤器提前读取了输入流,后面Spring MVC就再也没法解析上传的文件了。热门词里那条“springboot项目全局过滤器处理上传pdf文件时xss攻击”描述的就是这个坑。我的处理方式是在过滤器里判断ContentType,是multipart的直接跳过,非文件请求再走清洗逻辑。
SQL注入方面,用MyBatis-Plus的LambdaQueryWrapper能天然避免拼接SQL带来的注入风险。但要注意,如果你需要用ORDER BY某个动态字段,千万别直接把前端传来的字段名拼进SQL,而要做映射白名单校验。例如前端传sortField = “createTime”,后端只能从预设Map<String,String>里取出对应的数据库列名,取不到就报参数错误。
5.2 前后端联调:跨域、JSON精度与统一异常
前后端分离时,前端页面在8080端口,后端在8081端口,浏览器会拦截跨域请求。解决方式是在后端写一个CORS配置类,允许指定源访问。如果用了Spring Security或Sa-Token,还要确认跨域配置和认证过滤器的执行顺序,否则请求会先被过滤器拦截返回401,根本走不到CORS处理,前端看到的现象就是“明明设置了跨域还是报错”。
JSON序列化还藏着一个很隐蔽的问题:数据库ID如果用了雪花算法或者MyBatis-Plus默认的assignedId,它是一个Long类型,而JavaScript能精确表示的整数范围有限,后端返回的长ID在浏览器里会自动丢精度。比如1812332095285395461会变成1812332095285395400。解决方法是给Long类型的ID字段加上@JsonSerialize(using = ToStringSerializer.class),或者在Jackson的全局配置里把Long统一转成String。这个Bug不查到原因,你会怀疑人生很久。
还有全局异常处理,强烈建议统一用@RestControllerAdvice,把所有业务异常、参数校验异常、兜底异常做好响应。否则前端每遇到一个报错都弹出一大堆英文堆栈,观感极差。统一异常处理后,前端只需要读Result的code和msg,就能给用户友好的提示。调试接口建议再引入springdoc-openapi或Knife4j,自动生成接口文档,省得自己手写。
5.3 部署与打包:本地能跑不代表打完jar还能跑
很多项目在IDEA里跑得好好的,一打包部署就出问题。原因多半出在路径和配置文件上。首先,打包前确认是否使用了Spring Boot的Maven插件,没有这个插件打出来的jar不能直接运行。其次,文件上传目录不要打进jar包内,要放到外部,比如前面提到的/data/bid-system/upload/。因为jar包是一层压缩文件,运行时无法正常往里写入数据,除非你拆成war包或做特殊处理。所以上传、导出、模板等所有涉及写文件的逻辑,统一走配置目录。
部署方式上,毕设阶段最省力的是直接用Docker跑。写一个简单的Dockerfile,基础镜像选JDK8的alpine版本,然后用docker run映射端口和挂载上传目录。如果你不想折腾Docker,直接在服务器上java -jar xxx.jar也行,但注意控制台日志最好用nohup重定向到文件,不然关闭终端进程就挂了:
nohup java -jar springboot-bid-system.jar --spring.profiles.active=prod > app.log 2>&1 &5.4 借鉴现成项目的正确姿势
我理解很多同学会去网上找一个现成的招标系统源码来“参考”,甚至有人会把下载到的jar包反编译成项目再看结构。关于这一点我直说:反编译代码、复制粘贴,只会让你在答辩时露怯,因为老师随便问一个业务细节你都答不上来。正确的方式是把开源项目当作“需求说明书”来读——比如看它的表结构设计、看它的权限模型、看它怎么组织包结构,然后自己动手重新实现一遍。这样你既学会了设计思路,又能真正驾驭每一行代码。
如果时间实在紧张,可以拆解一个成熟项目中的两三个关键类仔细精读。比如别从头到尾全盘照搬,而是把它当作“词典”,遇到某个功能不会做,去查一下人家的实现思路,再用自己的编码习惯写出来。这个过程才是提升能力的地方。
6. 从毕设到工程化:三个可以加分的扩展方向
6.1 用工作流引擎改造审批流
招标项目从发布到定标,中间有不少步骤可以由人工确认转为流程化操作。如果你学有余力,可以引入Flowable或Activiti这类工作流引擎,把“招标审核”“中标审批”做成可配置的流程。这样一来,管理员在界面上拖动节点就能改变审批顺序,比硬编码在业务代码里灵活得多。不过我得提醒:工作流引擎会显著增加系统复杂度,做好后再放到毕设里才是加分项,如果做不完反而拖慢主线,建议放在“系统展望”章节里提一下即可。
6.2 文件服务独立化
目前的文件上传逻辑是存在本地的,这适合毕设,但不符合真实企业场景。你可以把文件存储模块抽象出来,比如定义一个FileStorageService接口,本地盘和MinIO分别是它的实现类。这样替换存储层时,只需要改配置,而不需要动业务代码。用MinIO的理由很简单:它是开源的、API兼容S3,社区资料多,搭建一个单机实例也就几分钟。如果毕设里能这样写,说明你对存储解耦有清晰认识。
6.3 消息通知与审计日志
招标系统天然需要事件提醒:投标截止前三天,供应商还没上传标书,系统要不要发提醒?评标结束后,中标结果要不要通知所有供应商?这些可以用Spring的事件机制或者消息队列实现。如果你不想引入MQ,用Spring自带的@Async异步方法就够了。顺手再设计一张操作日志表,记录谁在什么时间做了什么操作,比如“专家XXX在2024-05-12 14:30:03提交了评分”。这块东西虽然不显眼,但它让系统具备可追溯性,答辩时讲“我实现了日志审计功能”,会让整体评价上一个台阶。
说实话,做这类毕设课题,最大的收获并不是那个“能跑的系统”,而是在设计过程中想清楚每一个决策背后的理由。我在实际做这个项目时,光是数据库表就改了四版,从最开始只有四张表,到最后拆出完整的评分模型,中间踩过的坑——状态字段用字符串导致判断混乱、上传文件时在过滤器里被流抢占、Long型ID返回到前端丢失精度——都成了现在写文章时最想讲给你的经验。你在做的时候,也一定不要一上来就写代码,先把业务链路梳理清楚,把技术选型的理由想透。其实“选择什么”不难,难的是你能否在老师和面试官面前,把每一步“为什么”都答得有理有据。把这个问题解决了,你的毕设就不仅是一个项目,而是一段拿得出手的作品。