☰
SpringBoot+Vue在线招投标系统:从数据库设计到部署上线的完整实践
2026/10/14 12:24:50 网站建设 项目流程

这两年,Java后端配合Vue前端的组合几乎成了中小型管理系统的事实标准。但如果你要的是“完整能做出来、能跑起来、能拿去讲清楚设计思路”的招投标系统,单纯会写CRUD还真不够。这个项目看起来是“管理系统”,但实际上要处理的远不止增删改查——招标公告的状态流转、投标文件的接收归档、开标评标流程的权限控制,这些才是把你和培训班出来的同学拉开差距的地方。我花了几个周末完整过了一遍这套基于SpringBoot+Vue的在线招投标系统,这篇文章就把整个设计实现、踩坑过程、关键逻辑和最终部署一次性讲透,适合正在做毕设、准备面试项目、或者公司内部需要快速搭一套采购流程系统的朋友直接参考。


1. 项目全景与核心业务拆解

1.1 在线招投标到底解决了什么问题

在没有线上系统之前,招投标业务基本靠线下跑:招标方要打印公告、张贴公示、接收纸质标书、组织现场开标,投标方要一次次跑现场送材料。这里面的痛点非常明显,一是信息不透明,公告贴在哪里、什么时候截止全靠人为通知;二是流程不可追溯,标书什么时候收到、谁经手的、开标过程的记录都容易出纰漏;三是整理归档极其痛苦,一年几十个项目,纸质材料堆成山,审计要查的时候翻箱倒柜。

在线招投标管理系统要解决的,就是用一套标准化流程把招标方和投标方都拉到一个平台上。招标方创建项目、发布公告、审核投标报名、在线开标、录入评标结果,投标方注册登录、浏览公告、在线报名、上传标书、查看中标公示,所有动作都有记录、有时间戳、有权限控制。这不光是把线下搬到线上,更深层的是业务流程的数字化重构。

1.2 系统涉及的两类用户与核心流程

这套系统里用户角色不能只分管理员和普通用户,必须分清楚业务链路中的实际参与者。我这次设计是基于RBAC模型,但角色划分完全跟着业务走:

  • 系统管理员:负责用户管理、角色分配、系统参数配置、日志查看
  • 招标方(采购方):可以创建招标项目、发布公告、审核报名、录入开标信息、维护评标结果
  • 投标方(供应商):可以浏览公告、报名投标、上传投标文件、查看项目动态与中标公告
  • 评标专家(可选扩展角色):在评标阶段查看投标文件、录入评分(如果是完整版可以做,基础版可以先留接口)

这里我要提醒一点:很多同学做系统喜欢先画用例图、再堆功能,结果做着做着发现招标方竟然能修改已发布的公告,投标方竟然能看到所有标书内容,这些就是在设计阶段没把流程权限理清楚导致的。正确做法是先梳理业务状态,再按状态定权限。

1.3 项目的核心模块划分

实际落地时,我把系统拆成了六个核心模块:

  1. 系统管理模块:用户、角色、菜单权限、操作日志
  2. 招标项目管理模块:项目创建、编辑、提交审核、发布、归档
  3. 招标公告模块:公告发布、列表展示、详情查看、状态标识
  4. 投标管理模块:在线报名、投标文件上传、投标列表管理
  5. 开标评标模块:开标信息录入、评标结果登记、中标公示
  6. 个人中心与消息通知:我的项目、我的投标、系统消息

这六个模块是能够形成业务闭环的最小集。我在设计时把“公告”和“项目”分开建表,刚开始觉得麻烦,后来发现这是对的,因为一个招标项目可能分标段、分多次公告,如果混在一张表里,后面做统计分析和状态查询时SQL会写得很痛苦。


2. 技术栈选型的每一次取舍

2.1 为什么是SpringBoot而不是SSH或者SpringCloud

这个问题我在做技术选型时问了自己不止一遍。招投标系统属于典型的中后台管理系统,业务复杂度中等,并发量不算高,但逻辑链路长、状态多、权限要求严格。SpringBoot的优势恰好匹配这个场景。

对比传统的SSH架构,SpringBoot最实际的价值是简化了配置和部署。不需要写一堆XML配置文件,内嵌Tomcat让应用能直接打包成jar跑起来,这对后续部署到服务器上太友好了。对比SpringCloud那套微服务全家桶,招投标系统本质上还是单体应用更合适——你强行拆成订单服务、用户服务、公告服务,反而引入了服务间调用的复杂度,Debug的时候要开好几个进程,完全没有必要。

技术人员最容易犯的毛病就是技术驱动,觉得自己会微服务就非得上SpringCloud。我这里特别想强调一个观点:技术选型是服务业务场景的,招投标系统的核心是流程和权限,单体应用加合理分层完全能扛住,而且开发效率更高、部署更简单。

2.2 Vue 2还是Vue 3的选择逻辑

写前端的时候我纠结过要不要直接用Vue 3,毕竟新项目原则上应该用新版本。但是考虑到这套系统的实际使用场景——公司内部管理系统,需要的是稳定、生态成熟的方案,我最终选了Vue 2加Element UI。

这里有个很现实的原因:Element UI对Vue 2的支持最成熟,组件文档全是针对Vue 2写的,遇到问题搜解决方案,你搜到的答案90%都是Vue 2的。Vue 3对应的是Element Plus,虽然也在不断迭代,但有些第三方库的兼容性还是偶有出入。等到Element Plus完全成熟稳定,这套系统要升级的时候,核心业务逻辑不动,只换视图层组件库,这个迁移成本是可以接受的。

当然我后台的朋友如果是从零开始且没有历史包袱,直接用Vue 3也没问题,但如果你跟我一样,想花最少的时间把项目跑起来、把功能做扎实,那么Vue 2 + Element UI依然是中后台管理系统最稳妥的组合之一。

2.3 MyBatis和MySQL组合的真实原因

ORM框架很多人现在喜欢用MyBatis-Plus,因为它内置了通用Mapper,基础CRUD不用写SQL。但我的选择是原生MyBatis,不是因为MyBatis-Plus不好,而是招投标系统的很多查询天然就是多表联查和复杂条件查询。

举个例子:查询“某个供应商参与过的所有招标项目,附带项目状态、中标情况”,你如果用MyBatis-Plus的Wrapper去写,要么嵌套多个LambdaQueryWrapper,要么最后还是得自己写XML。而原生MyBatis把SQL写在XML里,复杂查询一目了然,方便调优。而且招投标系统里面涉及状态枚举、类型转换,XML里可以用自定义resultMap做映射,字段映射关系清清楚楚,不容易踩坑。

MySQL就更不用多说了,开源免费、性能稳定、运维资料丰富,招投标系统的数据量根本到不了需要上Oracle或PostgreSQL的地步。真正要注意的是建表时字符集必须用utf8mb4,排序规则用utf8mb4_general_ci,否则后面存用户输入的特殊符号时会有编码问题,这个坑我后面细讲。


3. 数据库设计:核心表结构与状态流转

3.1 数据库设计的核心思路

招投标系统的表结构是整个项目的灵魂。我在设计时遵循了一个原则:业务状态字段必须有单独的状态字段来管理,而不是通过数据是否存在来隐式判断。

什么意思呢?比如一个招标项目,它的生命周期是:草稿 -> 待审核 -> 已发布 -> 报名中 -> 已截止 -> 开标中 -> 评标中 -> 已完成。这个状态我用一个status字段存储,而不是通过“是否有关联的公告记录”“是否有关联的开标记录”来判断项目进行到哪一步。因为后者的判断逻辑分散在多个查询条件里,开发时特别容易遗漏状态组合,造成业务逻辑漏洞。

核心表我设计了八张(不包含权限相关的基础表):

表名核心字段用途说明
tb_userid, username, password, real_name, role_id, company_name用户基础信息
tb_roleid, role_name, role_key角色定义
tb_permissionid, perm_name, perm_code权限点定义
tb_role_permissionrole_id, permission_id角色权限关联
tb_projectid, project_name, project_code, category, budget, status, publisher_id, publish_time招标项目主表
tb_announcementid, project_id, title, content, attachment_path, create_time招标公告
tb_bidid, project_id, bidder_id, company_name, bid_amount, bid_file_path, status, submit_time投标记录
tb_bid_openid, project_id, open_time, open_location, recorder_id开标记录
tb_evaluationid, project_id, bid_id, evaluator_id, score, comment评标记录

3.2 招标项目管理表的设计细节

tb_project表是最核心的表,我在里面加了一个project_code字段,格式类似ZB20240001,用年份加自增序列生成。这个字段看起来可有可无,实际用下来非常重要——因为用户在沟通项目时,说“编号ZB20240001的那个项目”比说“那个关于打印机采购的项目”要准确得多,也方便在系统里做快速检索。

状态字段status我用了tinyint类型,0表示草稿,1表示待审核,2表示已发布,3表示报名中,4表示已截止,5表示开标中,6表示评标中,7表示已完成。这里有一个设计细节:我没有单独设计“审核结果”字段,因为审核不通过时直接把项目打回草稿状态,审核通过时进入待发布状态,用状态流转本身就表达了审核结果,无需额外字段。

3.3 投标记录表的并发考虑

tb_bid表里的unique约束非常关键。同一个投标方对同一个项目只能有一条投标记录,这需要在数据库层面去做约束,而不能只依赖后端代码判断。我当时试过只在Java代码里做检查,后来用并发测试工具模拟多个同时提交请求时,发现确实能插进去两条记录,后来加了UNIQUE KEY uk_project_bidder(project_id, bidder_id)才完美解决。

另外bid_amount字段我建议用DECIMAL(12,2),千万不要用FLOAT或DOUBLE。金额字段用浮点类型会出现精度问题,比如99.99实际存储可能是99.98999999,显示出来就很尴尬,审计的时候更是大忌。

3.4 文件路径存储策略

招投标系统绕不开标书文件的上传和下载。文件存储这块,我选择把文件保存在服务器的指定目录下,数据库里只存相对路径,而不是把文件内容直接存到数据库的BLOB字段里。原因有两点:一是数据库会变得臃肿,备份恢复都很慢;二是不方便直接通过Nginx做静态文件服务。

我的存储规则是:/data/bidding/files/项目编号/投标方ID/文件名.pdf,这样在文件管理上很清晰,一个项目的所有文件都在同一个目录下。上传接口用MultipartFile接收,然后按项目编号和投标方ID生成子目录存放。下载时通过鉴权后返回文件流,这样能避免文件直接被浏览器访问导致越权下载。


4. 后端核心模块与关键代码实践

4.1 项目目录结构与分层思想

后端我采用的是经典的四层结构:Controller、Service、Mapper、Entity。不要小看这个分层,很多代码写乱了就是因为Service太厚或者Controller里堆了太多业务。

我的目录结构大致是这样的:

com.bidding.system ├── controller │ ├── AuthController.java │ ├── ProjectController.java │ ├── AnnouncementController.java │ ├── BidController.java │ └── OpenBidController.java ├── service │ ├── impl │ ├── ProjectService.java │ └── ... ├── mapper │ ├── ProjectMapper.java │ └── ... ├── entity │ ├── Project.java │ └── ... ├── common │ ├── Result.java │ ├── StatusCode.java │ └── ... ├── config │ ├── WebMvcConfig.java │ └── ... ├── interceptor │ └── AuthInterceptor.java └── util ├── JwtUtil.java └── FileUtil.java

所有接口的返回统一用Result类封装,里面带code、message、data三个字段。这个不是随便定的,是跟前端约定好的数据契约,前端拿到code为200才取data,否则弹错误信息。如果一个项目里一半接口返回Map、一半返回JsonObject,前端处理起来绝对崩溃。

4.2 认证与权限控制的落地方案

招投标系统的权限控制不能只做登录拦截,还需要做基于角色的接口级权限校验。我用的是JWT加自定义拦截器的方案,没有引入Spring Security。原因是Spring Security对新手来说学习成本偏高,配置不当反而容易出问题,而招投标系统的安全需求用JWT加拦截器完全能覆盖。

登录接口逻辑是这样的:接收用户名密码,校验通过后,用JwtUtil生成Token,Token里包含用户ID和角色信息,有效期设为24小时。前端在后续请求的Header里带上Authorization: Bearer Token,拦截器取出Token解析、验证签名,然后根据用户角色判断当前请求路径是否在允许访问的路径列表中。

这里分享一个我踩过的坑:JWT的签名密钥一定不能写在代码里写死,最好从application.yml里配置读取。我当时在本地开发时把密钥直接写在了代码里,结果有一次打包部署时忘记改,幸好是内部系统没有暴露出去,否则Token签名泄露,别人就能伪造管理员Token了。这是一个必须养成的安全习惯。

4.3 项目状态的原子化更新

招标项目从草稿到发布,审核状态变更只能由特定角色触发,这个转台的原子化更新是后端相对核心的接口。我在实现时,只写了这样一个接口:

@PostMapping("/api/project/updateStatus") public Result updateStatus(@RequestBody StatusUpdateDTO dto) { return projectService.updateStatus(dto); }

updateStatus内部用动态SQL,只更新status字段,附带一个乐观锁条件。我举个例子:

int count = projectMapper.updateStatusWithLock(dto.getProjectId(), dto.getExpectedStatus(), dto.getNewStatus()); if (count == 0) { throw new BusinessException("项目状态已发生变化,请刷新后重试"); }

对应的XML里这样写:

<update id="updateStatusWithLock"> UPDATE tb_project SET status = #{newStatus}, update_time = NOW() WHERE id = #{projectId} AND status = #{expectedStatus} </update>

这里的关键设计是用expectedStatus做乐观锁条件。比如把状态从“待审核”改为“已发布”,SQL条件里就带上AND status = 1,这样如果两个人同时操作,只有一个人能成功更新,另一个人会因为影响行数为0而感知到冲突。这个机制避免了在代码里加synchronized锁的低效方案,也避免了分布式环境下锁失效的问题。

4.4 投标文件上传的实现要点

文件上传是招投标系统的重要环节。这里用MultipartFile接收前端传过来的文件,但有几个细节值得单独拿出来说:

一、文件大小限制。打包成jar部署后,SpringBoot的默认上传大小限制是1MB,这显然不够用。必须要在application.yml里配置:

spring: servlet: multipart: max-file-size: 100MB max-request-size: 200MB

二、文件名处理。接收到的文件不能直接用原始文件名,否则会冲突和乱码。我统一用System.currentTimeMillis()加随机数生成新文件名,但对原始文件名做了保留,存入数据库时把原始文件名存到file_name字段,下载时通过这个字段设置Content-Disposition,用户保存的文件才是有意义的文件名。

三、目录按项目隔离。文件保存路径用项目编号做目录区分,避免不同项目的文件混在一起,后续管理也方便。

4.5 投标截止时间的后端校验

一个隐藏很深的业务漏洞:前端页面把报名截止时间隐藏了、或者用户绕过前端直接调用接口,投标请求照样能提交。我当时在做系统时花了不少气力补这个漏洞。

后端在投标接口里必须重新校验当前时间是否在报名时间窗口内:判断当前时间在项目报名的开始和截止之间才允许提交,否则返回“不在报名时间范围内”。这里的时间必须要取服务器时间,而不是前端传上来的时间,因为前端时间可以被用户修改。这也是一个很经典的业务安全校验,我特意强调一下。

另外,报名截止后,投标记录表的status要由后端定时任务或者状态更新操作置为“已截止”。如果只是在前端隐藏按钮,那用户直接改接口就能继续投,这是绝对不行的。


5. 前端页面设计与交互体验优化

5.1 Vue项目结构与路由设计

前端我用Vue CLI工程化开发,Vue Router管理页面跳转,Axios发起HTTP请求。项目下的目录结构:

src ├── api // 接口请求封装 │ ├── auth.js │ ├── project.js │ └── bid.js ├── assets // 静态资源 ├── components // 公共组件 │ ├── Header.vue │ ├── Sidebar.vue │ └── Pagination.vue ├── router // 路由配置 │ └── index.js ├── store // Vuex状态管理 │ └── modules ├── views // 页面组件 │ ├── Login.vue │ ├── Dashboard.vue │ ├── project │ │ ├── ProjectList.vue │ │ ├── ProjectDetail.vue │ │ └── ProjectEdit.vue │ ├── bid │ │ ├── BidList.vue │ │ └── BidSubmit.vue │ └── system │ ├── UserList.vue │ └── RoleList.vue

路由配置我用的是动态路由方案,根据登录用户的角色信息,动态注册允许访问的路由表。没有权限的页面根本不注册,而不是仅仅在导航栏里隐藏入口。这点很重要,因为Vue是单页应用,路由定义在代码里,用户手动修改URL就能跳转到未授权页面。动态注册路由可以彻底杜绝这个风险。

5.2 登录状态管理与Axios拦截器

登录之后Token存在哪里?这个问题有几种方案,localStorage、sessionStorage、Cookie,各有利弊。我建议用localStorage加Vuex同步的混合方案:Token存localStorage里做持久化,同时存一份到Vuex里方便组件内读取。页面刷新时,在App.vue的created钩子中重新从localStorage恢复Token和用户信息。

Axios请求拦截器统一加上携带Token的逻辑,响应拦截器统一处理鉴权失败的情况。代码大概长这样:

axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = 'Bearer ' + token; } return config; }, error => Promise.reject(error)); axios.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } return Promise.reject(error); } );

这里有个体验细节:如果Token过期了,后端返回401,前端不能只弹一个“登录过期”的提示就完事,最好弹一个全局Message提示“登录已过期,请重新登录”,然后自动跳转到登录页。如果直接静默跳转,用户都不知道发生了什么,体验很糟糕。

5.3 列表页的搜索、分页与状态筛选

招标公告列表是所有用户都会看到的页面,这个页面的交互设计直接影响系统的使用体验。我采用的方案是:顶部搜索区加中间状态页签区加底部表格分页区。

搜索区支持按项目名称模糊搜索、按项目分类下拉选择、按发布时间范围搜索。状态页签区分全部、报名中、已截止、已完成。这种交互方式比只想一个搜索框要好用得多,因为用户在大多数场景下是按状态来找项目的。

分页组件我封装了一个Pagination组件,与后端约定好分页参数:pageNum、pageSize、total。后端用了PageHelper插件做物理分页,返回的数据结构是统一的pageResult,包含list和total。约会:列表接口的返回结构在项目一开始就定好,前端拿数据时直接解构,不用每个页面各写一套。

5.4 投标提交页面的防重复提交技巧

投标提交页面是整个系统操作频率最高的页面之一,用户在填写标书信息和上传文件时可能犹豫很久,提交时如果网络卡顿,容易出现多次点击。我做了两个层面的防重复:

第一层,前端给提交按钮加loading状态,点击提交后按钮变成loading且禁用,请求结束后恢复,这是最基本的一层。

第二层,前端在提交前生成一个随机的requestId,传给后端,后端Redis缓存里记录这个requestId,如果在短时间内收到相同requestId,直接返回“重复提交”。这个方案对前后端分离的项目更有效,防止用户绕过前端、用脚本重复提交接口。

5.5 开标评标页面的数据呈现

开标页面是内部用户才会接触的页面,但体验同样重要。我把开标详情页设计成上下结构:上半部分是项目基本信息展示,下半部分是投标列表,每一行列出投标公司、报价金额、投标状态、文件预览和下载按钮。评标结果单独用一个弹窗,录入时校验分数范围0到100,保存后弹窗关闭,表格数据刷新。

这里有个展示细节:投标金额在不同系统可能单位不统一,有的系统存的是“元”,有的存的是“万元”。为了避免前端显示混乱,我在后端接口返回时统一转成金额字符串,例如“99,800.00元”,而不是返回一个数字让前端自己格式化。这样数字精度格式由后端控制,避免出现百万千分位格式不统一这样很难发现的问题。


6. 常见问题排查与避坑实录

6.1 数据库中文乱码

这是我每次做新项目都会遇到的故障,招投标系统也不例外。尤其是第一次启动好系统,从页面往里录入中文数据,刷新后发现变成了一堆问号。这个排查过程不算轻松,因为乱码可能出现在连接层,也可能出现在存储层或者展示层。

我的排查思路是分开验证:先用数据库客户端直接插入中文,如果插入成功后查询是正常的,说明数据库本身没问题,问题出在Java代码或JDBC连接上。这个场景下90%的原因是JDBC连接串没有加characterEncoding=utf8,直接在URL里加上这个参数。另外建库时指定字符集也很重要,如果建库时没指定,后面改起来很麻烦,得重建。

jdbc:mysql://localhost:3306/bidding_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

这个连接串我建议直接复制保存,后面所有项目都用这一套参数,省得踩坑。

6.2 前端跨域问题

前后端分离开发时,前端跑在8080端口,后端跑在8081端口,直接请求就是跨域。这个问题常规解决方案是后端配置CorsFilter。我在实现时用的是:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600); } }

这里有个注意点:allowCredentials(true)与allowedOrigins("")不能同时存在,要用allowedOriginPatterns("")替代。这个坑我遇到时一度怀疑人生,后来查了文档和源码才发现这是新版本CORS规范的限制。如果你用的是很老的Spring版本可能不报这个错,但新版本一定注意。

6.3 MyBatis参数传递的暗坑

写MyBatis时最恐怖的一个错误是Mapper接口的参数与XML占位符对不上,错误不是启动时报,而是运行到那一次查询才报,线上出问题你才知道。比如接口里传了两个参数:

List<ProjectVO> listProjects(String keyword, int status);

XML里却写了#{keyword}和#{status},运行时就会报There is no getter for property named 'keyword'。因为MyBatis多个参数默认是不带名称的,你必须加@Param注解:

List<ProjectVO> listProjects(@Param("keyword") String keyword, @Param("status") int status);

这样XML里引用keyword和status才能正确对应。这个坑我记忆犹新,排查了一个多小时,Effect最后用@Param统一解决。后来写Mapper接口我所有的参数都规范地加@Param,从源头杜绝这类问题。

6.4 文件下载到浏览器直接打开JSON乱码

招投标系统中我固定用了pdf文件做测试,但文件下载里有隐含的编码问题。当文件名带中文时,如果响应头里的Content-Disposition没有做RFC 5987编码,浏览器下载下来文件名就是乱码。

String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);

这里我强烈建议用filename*语法而不是直接filename=,因为直接等于的方式在Chrome和Firefox下都不靠谱。filename*是按RFC标准实现的,兼容性最好。另外文件名里如果有空格,URLEncoder会转成+号,需要replace("+", "%20"),不然会出现位置对不上的bug。

6.5 部署到服务器后端口占用与路径配置

本地开发一切正常,打包部署到服务器却启动失败,大概率是端口被占用或者路径不存在。SpringBoot默认8080端口,如果服务器上有其他Java应用占用了8080,启动就会报端口冲突。我习惯在application.yml里用一个不太容易冲突的端口,比如8088:

server: port: 8088

文件上传保存路径在本地是写死的绝对路径,部署到服务器就失效,这属于典型的路径配置问题。我把上传根目录配置在application.yml里:

bidding: upload-dir: /data/bidding/files

然后在代码里通过@Value注入,这样同一套代码在不同环境只需要改配置文件,不需要改代码重新打包。这个习惯能省很多运维上的麻烦。


7. 性能优化与安全意识补充

7.1 列表查询的性能优化

招投标系统的列表页是最常被访问的页面,如果不做优化,数据量大了以后会明显变慢。我做了两层优化:第一层是数据库查询层面的优化,给常用的查询字段加了索引,比如tb_project的publisher_id和status,tb_bid的project_id和bidder_id。索引不是越多越好,而是根据查询模式来加,只给where条件和order by字段建索引。

第二层是前端层面的优化。招标公告列表和项目列表接口合并成一个,后端一次性返回列表数据和筛选条件选项,减少请求次数。列表的分页查询用PageHelper插件,设置合理的pageSize,默认10条一页,避免一次返回几百条导致页面卡顿。

7.2 敏感数据的保护策略

招投标系统中,标书文件属于商业敏感数据,它的权限控制要比普通功能更严格。我在做文件下载权限时,没有直接返回文件的静态URL让浏览器访问,而是走了一个下载接口,接口内先校验登录状态,再校验该用户是否有下载这个文件的权限。是投标方的话,只能下载自己提交的文件;是招标方或管理员,可以下载所有投标文件。评标专家仅在评标状态下可以下载查看。

加密方面,用户密码用BCrypt加密存储,同一密码每次生成的哈希值不同,攻击者无法通过彩虹表碰撞。系统内部返回用户信息时,密码字段统一置为null,避免泄露。日志里不允许打印完整密码和Token,这个习惯是从几次教训中总结出来的。

7.3 演示数据与初始账号

系统里要预留一个合理的初始账号体系。我设置了三个初始账号:admin是系统管理员,tenderer是招标方,bidder是投标方。密码初始为123456,登录后强制要求修改密码。这个细节作用很大,给甲方演示系统、给导师演示毕设时,直接用三个不同角色各登录一遍,就能完整展示整个业务链路,不用临时慢慢注册账号。

数据库初始化脚本SQL文件放到项目根目录的docs文件夹下,里面除了建表语句,还预置了这几个初始账号和一些测试项目数据。测试数据是公开的演示项目,不是真实业务数据,让验收方打开系统就有东西可看。我强调一下,如果你在文档里用了昵称“XXX”,记得把密码注释掉,别真的明文写在文档里。


8. 部署上线与个人经验心得

8.1 完整部署流程

整个系统部署我用的是最简单直接的方式,没有上Docker,也没有搞容器编排,因为单机部署一台服务器就完全够用。部署步骤整理在下面,跟着走就行:

  1. 后端打包:在项目根目录执行mvn clean package -DskipTests,得到jar文件
  2. 准备环境:服务器上安装JDK 8或11,数据库安装MySQL 5.7或8.0
  3. 初始化数据库:用Navicat或MySQL命令行执行docs目录下的init.sql脚本
  4. 修改配置:在打包后的jar同目录下创建application.yml,覆盖默认端口、数据库连接和上传路径
  5. 启动后端:执行nohup java -jar bidding-system.jar > logs/bidding.log 2>&1 &
  6. 前端打包:在Vue项目根目录执行npm run build,生成dist目录
  7. 配置Nginx:把dist目录指向Nginx的root,配置反向代理把/api开头的请求转发到后端8088端口
  8. 启动Nginx:nginx -s reload后,访问服务器IP或域名就能看到系统

这里要注意一个常见的坑:前端打包后dist目录里的静态资源路径引用的是绝对路径,如果系统部署在子路径下,比如www.xxx.com/bidding,需要在vue.config.js里设置publicPath为/bidding/。如果直接部署在根路径,不用这个配置也没问题。

8.2 Nginx反向代理的关键配置

Nginx配置里最核心的是对API请求的反向代理。

location /api/ { proxy_pass http://127.0.0.1:8088; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }

这里我踩过一个坑:proxy_pass如果写成http://127.0.0.1:8088/,末尾带斜杠和末尾不带斜杠含义不同。带斜杠会替换掉匹配的location前缀,不带斜杠则保留包含/api/的完整路径。我上面写的就是不带斜杠,后端Controller路径前缀是/api/,所以能正确匹配。如果带了斜杠,后端收到的路径会丢掉/api/,导致404。这个细节必须记牢。

8.3 后续扩展方向

这套系统目前的形态已经能完整支撑一个内部的招投标流程,但后续如果要做得更完善,可以从这几个方向继续扩展:

一是消息通知模块,目前投标方只能通过列表状态变化感知项目进度,可以对接短信或邮件服务,在关键节点自动通知相关人员。

二是评标打分功能,当前只是简单的分数登记,后续可以设计多维度评分表,由多个评标专家独立打分,系统自动计算加权平均分。

三是数据统计分析,可以增加一个Dashboard页面对招标项目数量、金额、中标率等核心指标做可视化展示,用ECharts绘制折线图和饼图,给管理层做决策参考。

四是对接电子签章服务,目前的文件上传只是普通附件,结合第三方电子签章API就能实现具有法律效力的在线合同签署。

8.4 我从这套系统里收获的几点经验

做完这套系统,再回头看,最有价值的反而不是代码本身,而是几个认知上的转变。第一点是:业务流程搞清楚后,技术实现基本都是顺着业务逻辑走,不要先写代码再反过来猜业务。第二点是:设计阶段多花一小时理状态和权限,能省掉开发阶段十小时改Bug的时间。第三个具体的小建议是:写代码时要有一份自查表,字符集、时间时区、金额精度、数据库索引、接口统一返回结构的约定,这些基础事项一次做对了,后面会省掉很多不必要的排查工作。

如果你现在正要动手做类似的管理系统,核心建议就是:先拿一张纸把业务状态流转画出来,把每种角色在每个状态下能做什么列清楚,再开始建表。数据库设计定稿了,整个项目的地基就打牢了。SpringBoot和Vue都只是实现手段,真正帮你扎稳的,是对业务本身的深刻理解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询