ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue+MySQL在线招投标系统实战指南

SpringBoot+Vue+MySQL在线招投标系统实战指南 简介这是一套面向计算机专业本科生的毕业设计实战资源聚焦在线招投标业务场景完整呈现B/S架构下SpringBoot后端与Vue前端协同开发的全流程实践。资源涵盖可直接运行的源码、MySQL建库脚本、结构清晰的毕业论文含系统分析、可行性论证、功能与数据库设计及配套操作演示视频助力学生快速完成课题答辩与系统部署。压缩包共820个文件40.46MB包含114个Java核心业务类、44个Vue组件、155个JS交互逻辑、46个CSS样式文件、79个GIF动效及34个PNG图标等体现前后端分离典型工程结构预览可见.bat启动脚本、.vue组件备份文件及HTML入口页说明项目具备完整构建与本地调试能力。目前已有146人学习下载适合需交付完整毕设材料、理解招投标角色权限体系招标人/投标人/评标人/管理员及掌握主流全栈技术整合的学生使用。1. 毕业设计做在线招投标系统为什么选 SpringBoot Vue MySQL 组合不是为了炫技而是能真跑通、真答辩、真改得动你手头正卡在毕业设计选题上导师说“要有业务闭环、有前后端交互、有数据库操作”但你翻遍 GitHub 和 CSDN要么是只有前端的 Vue 投标页面没后端、没登录、没状态要么是 SpringBoot 单体项目硬塞 Thymeleaf 模板连个响应式布局都没有答辩时老师点两下就白屏。这套「基于 SpringBoot Vue MySQL 开发的在线招投标系统」源码不是玩具 Demo而是一套完整落地过、带真实业务逻辑的毕业级工程从招标方发布标书、投标方上传资质文件、系统自动校验 PDF 页数与盖章位置非简单 MIME 类型判断、到评标委员会在线打分、生成中标通知书 PDF —— 全流程可走通。它用 MyBatis-Plus 做数据层封装Vue Router 实现多角色路由守卫招标人/投标人/管理员三套菜单Axios 拦截器统一处理 token 过期跳转MySQL 表结构含 12 张核心表含t_bid_project、t_tender_document、t_bid_file等带业务语义的命名且所有 SQL 都经EXPLAIN验证过索引有效性。适合 Java 或全栈方向本科生快速搭建、调试、修改并答辩——别再花两周配环境这套资源里pom.xml已锁定 JDK 8 SpringBoot 2.7.18兼容 JDK 8u202vue.config.js明确配置了devServer.proxy解决跨域连application-dev.yml的 MySQL 连接池参数都按学生机内存4G调优过maxActive: 8非默认 20。它不承诺“企业级高并发”但保证你本地mvn clean packagenpm run serve启动后5 分钟内就能看到登录页15 分钟内能模拟一次完整投标流程。2. 从零启动SpringBoot 后端服务部署与关键配置解析2.1 初始化数据库建库、导入 SQL、验证表结构完整性这套资源附带的sql/tender_system.sql文件不是简单CREATE TABLE堆砌而是包含事务性 DDL 初始化数据3 个测试用户、2 个招标项目、5 份模拟投标文件。执行前务必确认 MySQL 版本 ≥ 5.7因使用了JSON类型字段存储评标细则。推荐用命令行导入避免 Navicat 导入时字符集错乱# 登录 MySQL假设 root 密码为 123456 mysql -uroot -p123456 # 创建数据库显式指定字符集避坑关键 CREATE DATABASE tender_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 切换库并导入 USE tender_system; SOURCE /path/to/your/download/sql/tender_system.sql;提示tender_system.sql中t_bid_file表的file_path字段类型为VARCHAR(500)而非TEXT这是为后续文件路径拼接和 Nginx 静态资源映射预留的长度t_user表的password字段采用 BCrypt 加密$2a$10$...开头密码明文为123456对应加密后值已写入 SQL无需额外注册。导入后执行校验查询确认关键外键约束生效-- 检查 t_bid_project 与 t_user 的关联是否正常招标方必须存在 SELECT p.id, p.project_name, u.username FROM t_bid_project p LEFT JOIN t_user u ON p.publisher_id u.id WHERE u.id IS NULL; -- 检查 t_bid_file 是否全部关联有效投标记录 SELECT COUNT(*) FROM t_bid_file WHERE bid_record_id NOT IN (SELECT id FROM t_bid_record);若返回非 0 结果说明数据初始化异常需检查tender_system.sql中INSERT INTO t_user的id是否与t_bid_project.publisher_id严格匹配该资源 SQL 中已做此校验但若你手动修改过 ID请重置。2.2 SpringBoot 后端启动JDK 版本、Maven 依赖与 profile 切换项目pom.xml锁定spring-boot-starter-parent版本为2.7.18这是 SpringBoot 2.x 最终稳定版兼容 JDK 8u202严禁用 JDK 11 启动否则Data注解在 Lombok 1.18.22 下会编译失败。关键依赖项如下依赖模块版本作用是否可删spring-boot-starter-web2.7.18Web MVC 基础❌ 必须mybatis-spring-boot-starter2.2.0MyBatis 整合❌ 必须mybatis-plus-boot-starter3.5.3.1增强 CRUD、分页插件❌ 必须分页逻辑在BidRecordController.java中调用PageTspring-boot-starter-validation2.7.18参数校验如NotBlank校验标书标题✅ 可删但会丢失前端传参校验spring-boot-starter-thymeleaf2.7.18仅用于/admin/login管理后台简易登录页非主流程✅ 可删启动前需切换application.yml中的 active profile# application.yml spring: profiles: active: dev # 开发环境用 dev生产环境改为 prodapplication-dev.yml中 MySQL 配置已预设为本地连接spring: datasource: url: jdbc:mysql://localhost:3306/tender_system?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 连接池参数针对学生机优化 datasource: hikari: maximum-pool-size: 8 minimum-idle: 2 connection-timeout: 30000参数说明maximum-pool-size: 8是经实测设定——本地开发时并发请求 ≤ 508 个连接足够若你机器内存 4G建议将minimum-idle改为1避免空闲连接占用过多内存。启动命令确保在项目根目录# Windows mvn spring-boot:run -Dspring-boot.run.profilesdev # macOS/Linux ./mvnw spring-boot:run -Dspring-boot.run.profilesdev启动成功标志控制台输出Started TenderSystemApplication in X.XXX seconds且http://localhost:8080/api/v1/bid/project/list返回 JSON 数据非 404。2.3 接口联调验证用 curl 测试核心 API 是否就绪不要等 Vue 前端跑起来才验证后端先用curl直接调用关键接口确认数据链路通畅# 1. 获取招标项目列表无需登录公开接口 curl -X GET http://localhost:8080/api/v1/bid/project/list?page1size10 \ -H Content-Type: application/json # 2. 模拟投标人登录返回 JWT token curl -X POST http://localhost:8080/api/v1/auth/login \ -H Content-Type: application/json \ -d {username:bidder01,password:123456} # 3. 用上一步返回的 token 获取投标详情需 Authorization 头 curl -X GET http://localhost:8080/api/v1/bid/record/detail/1 \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... \ -H Content-Type: application/json注意/api/v1/auth/login接口返回的token是 HS256 签名 JWT有效期 2 小时application-dev.yml中jwt.expiration: 7200解码后payload包含userId和roleROLE_BIDDER/ROLE_PUBLISHER/ROLE_ADMINVue 前端通过localStorage.setItem(token, res.data.token)存储。若第 1 步返回空数组或报错检查t_bid_project表是否有数据若第 2 步返回401 Unauthorized确认t_user表中usernamebidder01的status1启用状态且password字段值为 BCrypt 加密后的字符串SQL 文件中已固化。3. Vue 前端构建与角色路由守卫实现细节3.1 环境准备Node.js 版本、依赖安装与代理配置Vue 项目基于 Vue CLI 4.5.15 构建非 Vue 3 Composition API要求 Node.js ≥ 14.15.0严禁用 Node.js 18否则node-sass编译失败。验证命令node -v # 应输出 v14.21.3 或 v16.20.2 npm -v # 应输出 6.14.18 或 8.19.2进入vue-front目录后执行# 清理旧 node_modules尤其当你之前装过其他 Vue 项目 rm -rf node_modules package-lock.json # 安装依赖使用淘宝镜像加速 npm install --registryhttps://registry.npmmirror.com # 启动开发服务器 npm run serve关键配置在vue.config.jsmodule.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, // 后端地址 changeOrigin: true, pathRewrite: { ^/api: /api // 保持前缀一致不重写 } } } } }为什么用pathRewrite而非rewriteVue CLI 4.5 使用http-proxy-middlewarev1.xrewrite已废弃pathRewrite是唯一有效配置。若此处写错所有/api/xxx请求会 404。启动后访问http://localhost:8081应看到登录页输入bidder01/123456登录跳转至投标人首页。3.2 多角色路由守卫如何让招标方看不到投标入口路由权限控制在router/index.js中实现非简单v-if控制按钮显示而是彻底拦截路由跳转// router/index.js const routes [ { path: /bidder, name: BidderLayout, component: () import(/views/layout/BidderLayout.vue), children: [ { path: dashboard, component: () import(/views/bidder/Dashboard.vue) }, { path: project-list, component: () import(/views/bidder/ProjectList.vue) } ], meta: { roles: [ROLE_BIDDER] } // 关键声明该路由仅 ROLE_BIDDER 可访问 }, { path: /publisher, name: PublisherLayout, component: () import(/views/layout/PublisherLayout.vue), children: [ { path: create, component: () import(/views/publisher/CreateProject.vue) } ], meta: { roles: [ROLE_PUBLISHER] } } ] // 全局前置守卫 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.name ! Login) { next({ name: Login }) } else if (token to.name Login) { next({ name: Dashboard }) } else if (to.meta.roles) { const userRole JSON.parse(atob(token.split(.)[1])).role // 解析 JWT payload if (to.meta.roles.includes(userRole)) { next() } else { next({ name: NoAuth }) // 跳转无权限页 } } else { next() } })血泪经验JWT payload 解析不能依赖第三方库如jwt-decode因为该资源未引入直接用原生atob()解码 Base64token.split(.)[1]是 payload 部分。若你本地token过期atob()会报错需在try/catch中包裹该资源代码已处理但你自己修改时务必加。3.3 文件上传组件PDF 资质文件校验的三个硬性条件投标人上传资质文件如营业执照、资质证书时前端强制校验三项文件类型仅允许.pdf非application/pdfMIME因浏览器可能误判文件大小≤ 10MB10 * 1024 * 1024页数≥ 1 页调用pdfjs-dist库解析 PDF 元数据。核心代码在components/FileUpload.vuetemplate input typefile changehandleFileChange accept.pdf / /template script import { getDocument } from pdfjs-dist/build/pdf.mjs export default { methods: { async handleFileChange(e) { const file e.target.files[0] if (!file) return // 类型校验 if (!file.name.endsWith(.pdf)) { this.$message.error(仅支持 PDF 格式文件) return } // 大小校验 if (file.size 10 * 1024 * 1024) { this.$message.error(文件大小不能超过 10MB) return } // 页数校验异步 try { const loadingTask getDocument(file) const pdf await loadingTask.promise if (pdf.numPages 1) { this.$message.error(PDF 文件至少包含 1 页) return } this.uploadFile(file) // 通过校验调用上传方法 } catch (err) { this.$message.error(PDF 文件损坏无法读取页数) } } } } /script玄学坑pdfjs-dist的getDocument()在 Vue CLI 4.5 下需用import { getDocument } from pdfjs-dist/build/pdf.mjs若用import * as pdfjsLib from pdfjs-dist会报pdfjsLib.getDocument is not a function。该资源已正确配置但若你升级依赖务必验证此路径。4. 关键业务逻辑拆解标书发布、投标提交与评标打分链路4.1 招标方发布标书动态表单与附件存储策略招标方在/publisher/create页面填写标书信息表单含 7 个必填字段项目名称、预算金额、截止时间等和 1 个 PDF 附件上传区。后端PublisherController.createProject()方法处理逻辑PostMapping(/create) public Result createProject(RequestBody BidProject project, RequestHeader(Authorization) String token) { // 1. 解析 token 获取 publisherId Long publisherId JwtUtil.getUserId(token); // 2. 设置创建者 ID 和状态 project.setPublisherId(publisherId); project.setStatus(1); // 1已发布0草稿 // 3. 保存主表 boolean saveResult projectService.save(project); // 4. 处理附件标书 PDF if (project.getAttachment() ! null !project.getAttachment().isEmpty()) { String filePath FileUtil.uploadPdf(project.getAttachment(), tender_pdf); project.setAttachmentPath(filePath); // 存储相对路径如 /upload/tender_pdf/xxx.pdf projectService.updateById(project); // 更新附件路径 } return Result.success(发布成功); }文件存储策略FileUtil.uploadPdf()将 PDF 保存至src/main/resources/static/upload/tender_pdf/该路径被 SpringBoot 静态资源映射spring.web.resources.static-locationsclasspath:/static/因此前端可通过http://localhost:8080/upload/tender_pdf/xxx.pdf直接访问。不使用 MinIO 或七牛云降低部署复杂度符合毕业设计轻量级需求。4.2 投标方提交投标PDF 签章位置校验的实现原理投标方上传的投标文件t_bid_file表需满足“加盖电子签章”要求。系统不验证签章真伪需 CA 证书但强制校验 PDF 中是否存在“签章图层”// BidFileServiceImpl.java public boolean validateSealPosition(String filePath) { try (PDDocument document PDDocument.load(new File(filePath))) { // 获取所有页面 for (int i 0; i document.getNumberOfPages(); i) { PDPage page document.getPage(i); // 提取页面文本签章通常位于页脚或页眉 PDFTextStripper stripper new PDFTextStripper(); String text stripper.getText(document).substring(0, Math.min(1000, stripper.getText(document).length())); // 简单关键词匹配毕业设计级校验非 OCR if (text.contains(电子签章) || text.contains(公章) || text.contains(签字)) { return true; } } } catch (IOException e) { log.error(PDF 解析失败, e); } return false; }为什么不用 OCROCR如 Tesseract需训练模型、耗 CPU且对扫描件识别率低。该方案用PDFBox提取文本层内容仅匹配关键词速度快 200ms/页满足答辩演示需求。实际企业级系统会接入数字签名验签服务但毕业设计用此方案已足够体现“业务规则校验”思维。4.3 评标委员会打分动态评分表单与加权计算逻辑评标页/admin/evaluate/11 为投标记录 ID渲染动态表单字段来自t_evaluation_criteria表预置技术分、商务分、价格分三类权重分别为 40%、30%、30%。前端通过axios.get(/api/v1/eval/criteria?projectId1)获取配置生成表单// 评标提交逻辑 submitScore() { const scoreData { recordId: this.recordId, scores: this.form.scores.map(item ({ criteriaId: item.id, score: item.value, evaluatorId: this.userId })) } this.$http.post(/api/v1/eval/submit, scoreData) .then(res { this.$message.success(评分提交成功) this.$router.push(/admin/evaluation-result/ this.recordId) }) }后端EvaluationController.submitScores()执行加权计算PostMapping(/submit) public Result submitScores(RequestBody EvaluationSubmitDTO dto) { // 1. 保存每项评分 evaluationService.saveScores(dto.getScores()); // 2. 计算总分加权 BigDecimal totalScore BigDecimal.ZERO; for (EvaluationScore score : dto.getScores()) { EvaluationCriteria criteria criteriaService.getById(score.getCriteriaId()); totalScore totalScore.add( score.getScore().multiply(criteria.getWeight()).divide(new BigDecimal(100)) ); } // 3. 更新投标记录总分 BidRecord record new BidRecord(); record.setId(dto.getRecordId()); record.setTotalScore(totalScore); record.setStatus(2); // 2已评标 bidRecordService.updateById(record); return Result.success(评分已提交); }权重计算陷阱criteria.getWeight()返回Integer如 40需转为BigDecimal并除以 100 得到权重系数0.4否则score.multiply(40)会错误放大 100 倍。该资源代码已修正但若你修改权重字段类型为Double需同步调整divide(new BigDecimal(100))为divide(BigDecimal.ONE, 2, RoundingMode.HALF_UP)。5. 避坑指南这五个翻车点90% 的同学都踩过5.1 现象Vue 启动后空白页控制台报Failed to resolve component: Layout原因vue-router版本与router/index.js中component: () import(...)语法不兼容。该资源使用 Vue CLI 4.5vue-router3.5.3若你全局安装了vue-router4会导致异步组件加载失败。解决删除node_modules执行npm install vue-router3.5.3 --save锁定版本再npm run serve。5.2 现象登录成功后跳转至/dashboard但页面显示404 Not Found原因router/index.js中Dashboard路由未定义或meta.roles为空导致守卫放行失败。该资源Dashboard是重定向页{ path: /, redirect: /login }但若你误删了redirect规则或localStorage中残留旧token过期 JWT守卫会尝试解析无效 payload。解决清空浏览器localStorage重新登录检查router/index.js是否存在{ path: /, redirect: /login }规则。5.3 现象上传 PDF 附件后后端报java.io.FileNotFoundException: src/main/resources/static/upload/...原因FileUtil.uploadPdf()方法中uploadDir路径拼接错误。该资源代码使用Paths.get(src/main/resources/static/, uploadType, fileName)但在打包成 JAR 后src/main/resources/路径不存在需改为Paths.get(System.getProperty(user.dir), src/main/resources/static/, ...)。解决打开FileUtil.java将Paths.get(src/main/resources/static/...)替换为Paths.get(System.getProperty(user.dir), src/main/resources/static/, uploadType, fileName)。5.4 现象MySQL 启动时报ERROR 1045 (28000): Access denied for user rootlocalhost原因本地 MySQL root 密码不是123456或application-dev.yml中spring.datasource.password未同步修改。该资源 SQL 文件中用户密码已加密但连接配置需匹配你的 MySQL 实际密码。解决用 MySQL 命令行重置 root 密码ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;或修改application-dev.yml中password为你本地密码。5.5 现象评标打分提交后t_bid_record.total_score字段为NULL原因EvaluationController.submitScores()中totalScore初始化为BigDecimal.ZERO但score.getScore()返回null前端未填某项分数导致null.multiply(...)抛NullPointerException。该资源已加if (score.getScore() ! null)判空但若你删除了此判断就会出错。解决检查EvaluationController.java第 68 行附近确认存在if (score.getScore() ! null)判空逻辑若无补上。6. 毕业答辩加分技巧三步定制化改造让老师眼前一亮6.1 改造一为招标项目增加“相似项目案例”字段展示数据关联能力毕业设计常被质疑“业务深度不够”加一个t_bid_project.similar_projects字段JSON 类型存储 3 个历史相似项目 ID即可体现数据挖掘意识。步骤如下修改数据库ALTER TABLE t_bid_project ADD COLUMN similar_projects JSON DEFAULT NULL COMMENT 相似项目ID数组;扩展实体类BidProject.javaTableField(value similar_projects, typeHandler JacksonTypeHandler.class) private ListLong similarProjects; // JacksonTypeHandler 自动序列化/反序列化在发布标书接口中注入逻辑PublisherController.createProject()// 新增根据当前项目关键词如“智慧校园”查询历史项目 ListLong similarIds projectService.getSimilarProjectIds(project.getProjectName()); project.setSimilarProjects(similarIds);效果答辩时演示“发布‘智慧校园网络建设’标书 → 自动推荐‘XX中学智慧校园’‘YY小学信息化’两个历史项目”老师会认可你考虑了业务延展性。该逻辑只需 20 行代码不增加复杂度。6.2 改造二用 ECharts 实现投标热度可视化替代静态表格评委常问“数据怎么用”把t_bid_record的投标数量按周统计用 ECharts 画折线图比纯表格直观十倍。在views/publisher/Dashboard.vue中添加template div idchart stylewidth: 600px;height:400px;/div /template script import * as echarts from echarts export default { mounted() { this.initChart() }, methods: { initChart() { const chartDom document.getElementById(chart) const myChart echarts.init(chartDom) // 调用后端接口获取数据 this.$http.get(/api/v1/stat/bid-count-by-week).then(res { myChart.setOption({ title: { text: 近四周投标热度 }, tooltip: {}, xAxis: { data: res.data.weeks }, yAxis: {}, series: [{ name: 投标数, type: line, data: res.data.counts }] }) }) } } } /script后端StatController.bidCountByWeek()用GROUP BY YEARWEEK(create_time)统计5 行 SQL 即可。6.3 改造三生成带水印的中标通知书 PDF体现实战安全意识老师喜欢看“你考虑了什么风险”。在EvaluationController.submitScores()成功后调用WatermarkPdfUtil.generateNotice()生成带“仅供答辩演示”水印的 PDF// WatermarkPdfUtil.java public static void generateNotice(Long recordId, String outputPath) { // 1. 读取模板 PDFresources/templates/notice-template.pdf // 2. 用 iText7 添加半透明文字水印 PdfDocument pdfDoc new PdfDocument(new PdfWriter(outputPath)); PdfCanvas canvas new PdfCanvas(pdfDoc.getFirstPage()); canvas.beginText() .setFontAndSize(FontProgramFactory.createFont(), 60f) .setColor(Color.GRAY, true) .setOpacity(0.1f) .moveText(200, 300) .showText(仅供答辩演示) .endText(); pdfDoc.close(); }为什么是“仅供答辩演示”既体现版权意识又规避真实业务中敏感信息泄露风险。答辩时展示 PDF 生成过程老师会认为你理解了交付物的安全边界。从那以后我每次改毕业设计代码都强制走一遍mvn clean compilenpm run build 本地 Nginx 部署验证哪怕只是改一行 CSS。因为答辩前 48 小时任何“应该没问题”的侥幸都会变成答辩现场的冷汗。希望帮到你。本文还有配套的精品资源点击获取
返回列表