☰
JSP网上招标系统实战:从部署到JDBC防注入与并发优化
2026/9/26 18:31:03 网站建设 项目流程

简介:这是一套基于Java与JSP技术栈的网上招标(威客)系统源码,面向学习Java Web开发的学生与初级开发者,可用于课程设计、毕业设计或Servlet+JDBC实战练习。系统围绕会员发布任务与接收任务展开,注册用户可查看网站流程与新闻公告、发布留言与威客在线信息、以列表形式查看已发布信息及成交记录;管理员端支持用户信息的新增编辑、留言列表管理与删除、系统数据备份等操作,数据库采用SQL Server,开发工具为MyEclipse。压缩包为zip格式,大小约4.7MB,文件总数与类型明细上游暂未提供。目前已有5304人浏览学习,热度较高。读者可从中获得一套结构完整的招标类Web项目参考,涵盖Servlet控制层、JDBC数据访问与JSP页面展示的典型分层写法,便于理解用户权限区分、留言与任务信息管理、成交记录查询等模块的实现思路,适合对照学习或在此基础上二次开发。

1. 从一份 jsp网上招标系统.zip 说起:这套老技术栈到底还能不能打

把 jsp网上招标系统.zip 解压开,你大概率会看到一堆 .jsp、WEB-INF/web.xml、lib 下的 mysql-connector-java,还有几张 .sql 建表脚本。这不是什么新鲜玩意,但它每年仍然是大量课程设计、中小型企业内部工具、甚至某些区县级采购平台的主力形态。原因很朴素:招标业务的核心是表单流转、角色权限、数据留痕,JSP + Servlet + JDBC 这套组合刚好能把这三件事用最低的部署成本干完,一台装了 Tomcat 的机器就能跑,不需要额外中间件。

这篇文章不聊情怀,只解决四个问题:这套系统由哪些模块构成、怎么在本地把它跑起来、JDBC 连 MySQL 时那些参数到底怎么填、以及上线前必须堵住的 SQL 注入和并发坑。适合两类人:一是拿到类似压缩包但不知道从哪下手的新手,二是想把这套老架构改稳、改安全的一线开发者。下面按「先看懂结构、再动手跑通、最后避坑加固」的顺序推。

2. 拆开 jsp网上招标系统.zip:模块划分与请求流转

2.1 招标系统的典型目录结构与各层职责

拿到压缩包先别急着导入 IDE,先在文件管理器里把目录看一遍。一个结构规整的 JSP 招标系统,通常长这样:

jsp网上招标系统/ ├── src/ # Java 源码 │ ├── com.bid.dao/ # JDBC 数据访问层 │ ├── com.bid.service/ # 业务逻辑层 │ ├── com.bid.servlet/ # 控制器,处理请求 │ └── com.bid.entity/ # 实体类,对应数据库表 ├── WebContent/ 或 web/ # Web 根目录 │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、过滤器配置 │ │ └── lib/ # 第三方 jar,如 mysql-connector │ ├── admin/ # 管理员页面 │ ├── supplier/ # 供应商页面 │ ├── expert/ # 评标专家页面 │ └── index.jsp # 登录入口 └── db/bid.sql # 建表与初始数据脚本

这个划分对应的是经典 MVC:JSP 只负责展示,Servlet 接请求、调 Service,Service 通过 DAO 用 JDBC 读写数据库。招标业务里,管理员发布招标公告、供应商投标报价、专家打分,三条线各自对应一组 Servlet 和 JSP。看目录就能判断这套代码的成熟度——如果 Servlet 里直接写 SQL、JSP 里直接连数据库,那基本是学生作业水平,后面加固的工作量会大很多。

2.2 一次「供应商提交投标」请求的完整链路

理解链路比背代码重要。以供应商点击「提交投标」为例,请求是这样走的:

  1. 浏览器 POST 到/bidSubmit,带上项目 ID、报价、技术方案文本;
  2. web.xml 里配置的BidSubmitServlet拦截,先从 session 取当前登录用户,校验是不是供应商角色;
  3. Servlet 把参数封装成Bid实体,调用BidService.submit(bid);
  4. Service 做业务校验:项目是否还在投标期、该供应商是否已投过标;
  5. DAO 用PreparedStatement执行 INSERT,写入t_bid表;
  6. 返回结果,Servlet 转发到supplier/bidResult.jsp展示。

这条链路里,权限校验放在 Servlet、业务规则放在 Service、数据操作放在 DAO,是这套架构能维护下去的关键。很多压缩包里的代码把三者揉在一个 JSP 里,改一个字段要翻五个文件,那种就得先重构再谈功能。

2.3 数据库表设计:招标业务绕不开的六张核心表

打开 db/bid.sql,重点看这几张表在不在、字段全不全:

表名作用关键字段
t_user用户账号id, username, password, role
t_project招标项目id, title, budget, deadline, status
t_bid投标记录id, project_id, supplier_id, price, file_path
t_expert评标专家id, name, specialty, user_id
t_score评分记录id, bid_id, expert_id, score, comment
t_notice公告id, project_id, content, publish_time

t_bid表上一定要有(project_id, supplier_id)的唯一约束,否则同一家供应商重复投标会让后面的评标逻辑彻底乱套。t_project.status用整型枚举(0 待发布、1 投标中、2 评标中、3 已结束)比用字符串好,查询和状态机判断都省事。这几张表的关系理清了,整个系统的数据流就通了。

3. 在 MyEclipse 或 IDEA 里把项目跑起来

3.1 导入项目与配置 Tomcat 的具体步骤

热词里 myeclipse导入项目、idea新建jsp项目 问的人最多,两条路都走一遍。MyEclipse 的流程是:File → Import → Existing Projects into Workspace,选中解压后的目录,如果项目没有 .project 文件就选「Import as General Project」,再右键 Configure → Add Project Facets,勾上 Dynamic Web Module 和 Java。IDEA 的话,File → New → Project from Existing Sources,选目录后一路 Next,最后在 Project Structure 里把 Web 资源目录指到 WebContent,把 src 标成 Sources Root。

Tomcat 配置这一步最容易翻车。在 IDEA 里 Run → Edit Configurations → 加一个 Tomcat Local,Deployment 标签页点 + 选 Artifact,注意要选xxx:war exploded而不是xxx:war,前者支持热更新,改 JSP 不用重启。Application context 建议设成/bid,这样访问路径就是http://localhost:8080/bid/。MyEclipse 里则是右键项目 → Run As → MyEclipse Server Application,它会自动部署。

3.2 用 JDBC 连上 MySQL:URL 参数逐个说清

项目跑起来第一件事是连数据库。把 mysql-connector-java 的 jar 放进 WEB-INF/lib,然后写一个测试类验证连通性:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbTest { public static void main(String[] args) { // 8.x 驱动类名,5.x 用 com.mysql.jdbc.Driver String driver = "com.mysql.cj.jdbc.Driver"; // useSSL=false 关掉 SSL 握手,避免本地自签证书报错 // serverTimezone 必须显式指定,否则 8.x 驱动会抛时区异常 // allowPublicKeyRetrieval 配合 caching_sha2_password 认证方式 String url = "jdbc:mysql://localhost:3306/bid_db" + "?useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&characterEncoding=utf8" + "&allowPublicKeyRetrieval=true"; String user = "root"; String password = "your_password"; try { Class.forName(driver); Connection conn = DriverManager.getConnection(url, user, password); System.out.println("连接成功:" + conn.getMetaData().getURL()); conn.close(); } catch (ClassNotFoundException e) { System.out.println("驱动没加载上,检查 jar 是否在 classpath"); } catch (SQLException e) { System.out.println("连接失败:" + e.getMessage()); } } }

参数逐个解释:useSSL=false是本地开发最省事的做法,生产环境应该改成useSSL=true并配好证书,否则热词里那个「驱动程序无法通过使用安全套接字层加密与 SQL Server 建立安全连接」的报错会原样出现在 MySQL 上。serverTimezone不写,8.x 驱动直接抛The server time zone value is unrecognized。characterEncoding=utf8保证中文项目名、供应商名不乱码。allowPublicKeyRetrieval=true是 MySQL 8 默认caching_sha2_password认证方式下的必要参数,不加会报Public Key Retrieval is not allowed。

3.3 从建库到登录:最小可运行验证

数据库连上后,按顺序执行:

# 1. 建库 mysql -u root -p -e "CREATE DATABASE bid_db DEFAULT CHARSET utf8mb4;" # 2. 导入建表脚本 mysql -u root -p bid_db < db/bid.sql # 3. 确认表和数据都在 mysql -u root -p bid_db -e "SHOW TABLES; SELECT username, role FROM t_user LIMIT 5;"

三步都过了,再启动 Tomcat,浏览器打开http://localhost:8080/bid/,用脚本里的初始账号登录。如果登录页能出来但提交报 500,八成是 DAO 里的 SQL 字段和实际表结构对不上,去 Tomcat 日志里找SQLSyntaxErrorException,它会直接告诉你哪个字段名错了。这一步跑通,说明环境、驱动、数据库、Web 容器四者已经打通,后面才谈得上改功能。

4. 招标业务里 JDBC 增删改查怎么写才不出事

4.1 用 PreparedStatement 写投标插入与防注入

招标系统里最敏感的操作就是投标数据写入,这里必须用PreparedStatement,绝不能拼字符串:

public boolean submitBid(Bid bid) { // 占位符 ? 由驱动负责转义,从根上堵住 SQL 注入 String sql = "INSERT INTO t_bid(project_id, supplier_id, price, file_path, submit_time) " + "VALUES(?, ?, ?, ?, NOW())"; try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, bid.getProjectId()); ps.setInt(2, bid.getSupplierId()); // 报价用 BigDecimal,别用 double,金额精度不能丢 ps.setBigDecimal(3, bid.getPrice()); ps.setString(4, bid.getFilePath()); return ps.executeUpdate() == 1; } catch (SQLException e) { // 唯一约束冲突会走到这里,说明重复投标 System.out.println("投标写入失败:" + e.getMessage()); return false; } }

逻辑说明:try-with-resources保证 Connection 和 PreparedStatement 自动关闭,避免连接池耗尽。参数按位置从 1 开始设置,类型要和数据库列类型对应,price用setBigDecimal而不是setDouble,招标报价差一分钱都可能废标。捕获 SQLException 时不要吞掉,至少打日志,唯一约束冲突(重复投标)和连接失败要能区分开。

4.2 分页查询与慢 SQL:招标列表怎么查才快

招标公告列表是访问最频繁的页面,数据量上来后必须分页。MySQL 用LIMIT offset, size:

public List<Project> listProjects(int page, int size, String keyword) { // 先查总数用于算总页数,再查当前页数据 String countSql = "SELECT COUNT(*) FROM t_project WHERE title LIKE ?"; String dataSql = "SELECT id, title, budget, deadline, status FROM t_project " + "WHERE title LIKE ? ORDER BY publish_time DESC LIMIT ?, ?"; List<Project> list = new ArrayList<>(); try (Connection conn = DbUtil.getConnection()) { int total; try (PreparedStatement ps = conn.prepareStatement(countSql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { rs.next(); total = rs.getInt(1); } } try (PreparedStatement ps = conn.prepareStatement(dataSql)) { ps.setString(1, "%" + keyword + "%"); ps.setInt(2, (page - 1) * size); ps.setInt(3, size); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Project p = new Project(); p.setId(rs.getInt("id")); p.setTitle(rs.getString("title")); p.setBudget(rs.getBigDecimal("budget")); list.add(p); } } } } catch (SQLException e) { System.out.println("查询失败:" + e.getMessage()); } return list; }

参数说明:page从 1 开始,offset = (page-1)*size。title上要建索引,但LIKE '%keyword%'前置通配符会让索引失效,数据量大时这是典型慢 SQL。优化方向是改成前缀匹配keyword%,或者上全文索引。ORDER BY publish_time的字段也要有索引,否则每次排序都是全表扫描。招标系统数据量通常不大,但公告表累积几年后一样会拖慢,提前把索引建好。

4.3 事务控制:评标打分必须整体成功或整体回滚

评标环节一个专家给多个投标打分,要么全写入,要么一条都不写,否则分数统计会错乱:

public boolean submitScores(int expertId, List<Score> scores) { String sql = "INSERT INTO t_score(bid_id, expert_id, score, comment) VALUES(?, ?, ?, ?)"; Connection conn = null; try { conn = DbUtil.getConnection(); // 关闭自动提交,手动控制事务边界 conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement(sql)) { for (Score s : scores) { ps.setInt(1, s.getBidId()); ps.setInt(2, expertId); ps.setBigDecimal(3, s.getScore()); ps.setString(4, s.getComment()); ps.addBatch(); } ps.executeBatch(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { /* 记录回滚失败 */ } } System.out.println("打分提交失败,已回滚:" + e.getMessage()); return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { /* 忽略 */ } } } }

关键点:setAutoCommit(false)之后所有操作在同一个事务里,executeBatch批量提交减少网络往返,出错时rollback保证一致性。finally 里要把连接还回池子,并且恢复 autoCommit,否则这个连接被复用时会带着未提交的事务,是连接池场景下的经典坑。招标系统里评标、定标这类操作都该走事务,别图省事一条条 insert。

5. 上线前必须堵住的坑:SQL 注入、并发与编码

5.1 登录框被万能密码绕过:现象、原因、解决

现象:登录页输入admin' or '1'='1直接进后台。原因:登录 SQL 用了字符串拼接,SELECT * FROM t_user WHERE username='admin' or '1'='1' AND password='xxx',条件恒真。解决:全部改成PreparedStatement,用户名密码作为参数传入,驱动会转义引号。同时密码别明文存,用 MD5 加盐或 BCrypt,登录校验在 Service 层比对哈希值。改完再拿' or 1=1 --试一遍,进不去才算过。

5.2 同一供应商重复投标:现象、原因、解决

现象:评标时发现某供应商有两条投标记录,报价还不一样。原因:t_bid表没建唯一约束,Service 层也没做「是否已投标」的前置校验,用户连点两次提交就写两条。解决:数据库层加UNIQUE KEY uk_project_supplier (project_id, supplier_id),Service 层在插入前先SELECT COUNT(*)判断,前端提交按钮点击后置灰。三层都做,任何一层漏了都不至于出脏数据。

5.3 中文项目名乱码:现象、原因、解决

现象:新建招标项目,标题里的中文存进库变成问号。原因:三个环节的字符集不一致——JSP 页面没声明pageEncoding="UTF-8"、请求没设request.setCharacterEncoding("UTF-8")、数据库连接串没带characterEncoding=utf8、表本身不是 utf8mb4。解决:JSP 头部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,Servlet 取参数前调request.setCharacterEncoding("UTF-8"),连接串加characterEncoding=utf8,建库用DEFAULT CHARSET utf8mb4。四处对齐,乱码必消。

5.4 连接池耗尽:现象、原因、解决

现象:系统跑一阵后所有页面卡死,日志报Cannot get a connection, pool exhausted。原因:DAO 里 Connection 用完没 close,或者异常路径下没走到 close。解决:统一用 try-with-resources,或者引入 Druid、HikariCP 这类连接池并配置maxActive、maxWait。招标系统并发不高,但评标时段集中访问,池子给小了照样卡。建议maxActive=20、maxWait=3000,再配个validationQuery=SELECT 1做连接保活。

5.5 时间字段差 8 小时:现象、原因、解决

现象:投标提交时间比实际早 8 小时。原因:连接串没指定serverTimezone,驱动按 UTC 解析,或者 JVM 时区和数据库时区不一致。解决:连接串显式写serverTimezone=Asia/Shanghai,数据库列用DATETIME而不是TIMESTAMP(后者受时区影响),Java 侧统一用java.util.Date或LocalDateTime并确认 JVM 启动参数-Duser.timezone=Asia/Shanghai。三处统一,时间就准了。

6. 把这套老系统改稳的两个进阶技巧

第一个技巧是给 DAO 层加一层薄封装,把连接获取、参数设置、结果映射抽出来,业务代码只写 SQL 和映射逻辑。招标系统里表不多,但每个 DAO 都重复写 try-catch-finally 很烦,抽一个BaseDao出来,queryForList(sql, params, rowMapper)一个方法覆盖八成查询,代码量能砍掉一半,出错点也集中了。我一般会保留一个DbUtil管连接池,BaseDao管模板方法,具体 DAO 只写 SQL 和 RowMapper。

第二个技巧是给关键操作加审计日志。招标业务对留痕要求高,谁在什么时候发布了公告、谁改了投标状态,都要能查。做法很简单:在 Service 层每个写操作后插一条t_audit_log,记录user_id、action、target_id、create_time。别用 AOP 硬切,招标系统方法不多,手动插更可控,也方便在日志里带上业务上下文。日志表按月分表或者定期归档,不然几年后它自己就成了慢 SQL 源头。

验证改造是否到位,有个笨办法但很有效:把useSSL、serverTimezone这些参数故意写错,看系统报错信息是否清晰;拿' or 1=1 --去撞每个输入框;用 JMeter 开 50 个线程同时提交投标,看有没有重复记录和连接泄漏。这三关过了,这套 JSP 招标系统才算真正能交付。

我自己踩过最深的一个坑,是早期图省事在 JSP 里直接写 JDBC,结果一个页面改字段要动七八个文件,后来花了两天才拆成 MVC。从那以后我拿到任何 JSP 项目,第一件事就是看 Servlet 和 DAO 分没分家,没分就先重构再谈功能。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询