JSP网上招标系统实战:从部署到JDBC防注入与并发优化
简介这是一套基于Java与JSP技术栈的网上招标威客系统源码面向学习Java Web开发的学生与初级开发者可用于课程设计、毕业设计或ServletJDBC实战练习。系统围绕会员发布任务与接收任务展开注册用户可查看网站流程与新闻公告、发布留言与威客在线信息、以列表形式查看已发布信息及成交记录管理员端支持用户信息的新增编辑、留言列表管理与删除、系统数据备份等操作数据库采用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 # 建表与初始数据脚本这个划分对应的是经典 MVCJSP 只负责展示Servlet 接请求、调 ServiceService 通过 DAO 用 JDBC 读写数据库。招标业务里管理员发布招标公告、供应商投标报价、专家打分三条线各自对应一组 Servlet 和 JSP。看目录就能判断这套代码的成熟度——如果 Servlet 里直接写 SQL、JSP 里直接连数据库那基本是学生作业水平后面加固的工作量会大很多。2.2 一次「供应商提交投标」请求的完整链路理解链路比背代码重要。以供应商点击「提交投标」为例请求是这样走的浏览器 POST 到/bidSubmit带上项目 ID、报价、技术方案文本web.xml 里配置的BidSubmitServlet拦截先从 session 取当前登录用户校验是不是供应商角色Servlet 把参数封装成Bid实体调用BidService.submit(bid)Service 做业务校验项目是否还在投标期、该供应商是否已投过标DAO 用PreparedStatement执行 INSERT写入t_bid表返回结果Servlet 转发到supplier/bidResult.jsp展示。这条链路里权限校验放在 Servlet、业务规则放在 Service、数据操作放在 DAO是这套架构能维护下去的关键。很多压缩包里的代码把三者揉在一个 JSP 里改一个字段要翻五个文件那种就得先重构再谈功能。2.3 数据库表设计招标业务绕不开的六张核心表打开 db/bid.sql重点看这几张表在不在、字段全不全表名作用关键字段t_user用户账号id, username, password, rolet_project招标项目id, title, budget, deadline, statust_bid投标记录id, project_id, supplier_id, price, file_patht_expert评标专家id, name, specialty, user_idt_score评分记录id, bid_id, expert_id, score, commentt_notice公告id, project_id, content, publish_timet_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 LocalDeployment 标签页点 选 Artifact注意要选xxx:war exploded而不是xxx:war前者支持热更新改 JSP 不用重启。Application context 建议设成/bid这样访问路径就是http://localhost:8080/bid/。MyEclipse 里则是右键项目 → Run As → MyEclipse Server Application它会自动部署。3.2 用 JDBC 连上 MySQLURL 参数逐个说清项目跑起来第一件事是连数据库。把 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; // useSSLfalse 关掉 SSL 握手避免本地自签证书报错 // serverTimezone 必须显式指定否则 8.x 驱动会抛时区异常 // allowPublicKeyRetrieval 配合 caching_sha2_password 认证方式 String url jdbc:mysql://localhost:3306/bid_db ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8 allowPublicKeyRetrievaltrue; 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()); } } }参数逐个解释useSSLfalse是本地开发最省事的做法生产环境应该改成useSSLtrue并配好证书否则热词里那个「驱动程序无法通过使用安全套接字层加密与 SQL Server 建立安全连接」的报错会原样出现在 MySQL 上。serverTimezone不写8.x 驱动直接抛The server time zone value is unrecognized。characterEncodingutf8保证中文项目名、供应商名不乱码。allowPublicKeyRetrievaltrue是 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, sizepublic ListProject 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 ?, ?; ListProject 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, ListScore 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 11直接进后台。原因登录 SQL 用了字符串拼接SELECT * FROM t_user WHERE usernameadmin or 11 AND passwordxxx条件恒真。解决全部改成PreparedStatement用户名密码作为参数传入驱动会转义引号。同时密码别明文存用 MD5 加盐或 BCrypt登录校验在 Service 层比对哈希值。改完再拿 or 11 --试一遍进不去才算过。5.2 同一供应商重复投标现象、原因、解决现象评标时发现某供应商有两条投标记录报价还不一样。原因t_bid表没建唯一约束Service 层也没做「是否已投标」的前置校验用户连点两次提交就写两条。解决数据库层加UNIQUE KEY uk_project_supplier (project_id, supplier_id)Service 层在插入前先SELECT COUNT(*)判断前端提交按钮点击后置灰。三层都做任何一层漏了都不至于出脏数据。5.3 中文项目名乱码现象、原因、解决现象新建招标项目标题里的中文存进库变成问号。原因三个环节的字符集不一致——JSP 页面没声明pageEncodingUTF-8、请求没设request.setCharacterEncoding(UTF-8)、数据库连接串没带characterEncodingutf8、表本身不是 utf8mb4。解决JSP 头部加% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Servlet 取参数前调request.setCharacterEncoding(UTF-8)连接串加characterEncodingutf8建库用DEFAULT CHARSET utf8mb4。四处对齐乱码必消。5.4 连接池耗尽现象、原因、解决现象系统跑一阵后所有页面卡死日志报Cannot get a connection, pool exhausted。原因DAO 里 Connection 用完没 close或者异常路径下没走到 close。解决统一用 try-with-resources或者引入 Druid、HikariCP 这类连接池并配置maxActive、maxWait。招标系统并发不高但评标时段集中访问池子给小了照样卡。建议maxActive20、maxWait3000再配个validationQuerySELECT 1做连接保活。5.5 时间字段差 8 小时现象、原因、解决现象投标提交时间比实际早 8 小时。原因连接串没指定serverTimezone驱动按 UTC 解析或者 JVM 时区和数据库时区不一致。解决连接串显式写serverTimezoneAsia/Shanghai数据库列用DATETIME而不是TIMESTAMP后者受时区影响Java 侧统一用java.util.Date或LocalDateTime并确认 JVM 启动参数-Duser.timezoneAsia/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 11 --去撞每个输入框用 JMeter 开 50 个线程同时提交投标看有没有重复记录和连接泄漏。这三关过了这套 JSP 招标系统才算真正能交付。我自己踩过最深的一个坑是早期图省事在 JSP 里直接写 JDBC结果一个页面改字段要动七八个文件后来花了两天才拆成 MVC。从那以后我拿到任何 JSP 项目第一件事就是看 Servlet 和 DAO 分没分家没分就先重构再谈功能。希望帮到你。本文还有配套的精品资源点击获取