SSM+JSP网上招投标系统:从环境搭建到业务闭环落地
简介面向Java毕业设计与课程设计的SSM网上招投标系统完整项目包适合有Java基础、需要完成实训项目或期末大作业的学生。项目基于SSM框架整合前端采用HTML/JSP/Vue组合代码含注释核心逻辑易于理解。压缩包共812个文件、体积17.32MB主要包含Java源码104个、JS脚本167个、Vue页面43个、HTML页面38个、CSS与图片资源另有SQL数据库脚本和安装/运行/构建批处理工具部署环境建议为IDEA、MySQL 5.7/8.0、Tomcat 7.x/8.x及Maven。系统已经严格调试功能覆盖招投标信息管理、用户后台、数据管理、在线投标流程等模块导入即可运行便于二次开发。目前已有57人学习下载适合作为毕业设计参考、课程设计复现和SSM开发练手。1. 网上招投标系统凭什么还能撑起一届 Java 毕业设计每年到了三月总有一批被“毕业设计”卡住的 Java 学生搜同一个关键词SSM JSP HTML 网上招投标系统。这套组合听起来老却是 Java 毕设里出镜率最高的方案之一。网上招投标系统的业务闭环——招标公告发布、投标报名、开标评标、中标公示——天然适合用 SSMSpring SpringMVC MyBatis这种分层清晰的框架来演示JSP 负责把数据库里的数据渲染成可操作的网页HTML 承担静态页面和展示细节。它解决的不只是“交一份能跑的代码”而是让评委一眼能看到完整的业务逻辑。适合三类人需要快速落地毕业设计的本科生想拿真实业务练手 SSM 整合的初学者以及要给团队做演示项目的带教者。2. 从 JDK 到 Tomcat先把 SSM 工程跑起来再谈招投标业务2.1 环境选型JDK 8 Maven 3.6 Tomcat 8.5 的匹配逻辑网上招投标系统拿到手的第一步不是看代码是先确认环境。SSM 时代的代码是按 JDK 8 Tomcat 8 的标准写的这也是整套架构最稳的版本组合。很多人第一步就栽了装了 JDK 17Tomcat 用 10一启动全是 ClassNotFoundException。原因不复杂JDK 9 之后 Java EE 的 javax.servlet 包从 JDK 里拆了出去Tomcat 10 又把 javax.* 迁移到了 jakarta.*老项目里的 import javax.servlet.http.HttpServletRequest 全部找不到类。我一般部署这类系统时会固定下面这一组版本组件推荐版本备注JDK1.8兼容 Spring 5.x 及以下所有 SSM 常用用法Maven3.6.x3.8 之后对中央仓库访问策略更严格老依赖容易拉不下来Tomcat8.5.x支持 Servlet 3.1和 javax.servlet 完全匹配MySQL5.7 或 8.08.0 记得在 JDBC 连接串里加 serverTimezoneIDEA任意较新版本社区版也能跑只是没有 Tomcat 集成插件这套组合对应的 Java 编译器级别是 1.8。如果本机装了多个 JDK注意把 IDEA 的 Project Structure、Maven 的 JDK for importer、以及运行配置里的 JRE 全部切到 8否则会出现 “bad class file: class file has wrong version” 这种编译期错误摆弄半天还以为是源码坏了。注意装完 Tomcat 8.5 后先手动执行 bin/version.sh 确认版本号输出再继续下一步。很多简化版系统镜像自带的 Tomcat 是 7 或 9JSP 的语法和 Servlet API 行为都有差异后面排查起来很被动。2.2 最小可运行的 SSM 配置pom、web.xml 与 Spring 容器环境对齐之后先确认工程结构。资源包解压之后一般是三块源码工程、SQL 脚本、部署说明文档。标准的 SSM 网上招投标系统是 war 包结构src/main/java 放控制器和业务代码src/main/resources 放配置webapp 下面放 JSP 和静态 HTML。先看 pom.xml 的关键依赖properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target spring.version5.1.20.RELEASE/spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdjavax.servlet/groupId artifactIdservlet-api/artifactId version3.0-alpha-1/version scopeprovided/scope /dependency dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependenciesSpring 的 webmvc 和 jdbc 必须用同一个版本分开写两个 version 会出现容器里同时存在两套 Spring 核心类的情况运行期直接 NoSuchMethodError。mysql-connector-java 如果用 MySQL 8.x换成 8.0.26 并改用 com.mysql.cj.jdbc.Driver 驱动类。servlet-api 的 scope 写 provided 的意思是编译时需要、运行时交给 Tomcat 提供否则打包时会把 Tomcat 自己的 Servlet 实现盖掉启动时出现重复类的冲突。接下来是 web.xml整个 Web 应用的入口web-app ... context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mybatis.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-namespringmvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mapping /web-appcontext-param 和 ContextLoaderListener 加载的是 Spring 根容器也就是数据源、事务、ServiceDispatcherServlet 加载的是 spring-mvc.xml只扫描 Controller。根容器和子容器分开是 SSM 整合的标准做法好处是 Spring 的事务代理不会被 MVC 的组件扫描覆盖否则 Transactional 在部分方法上会失效却没有任何报错。CharacterEncodingFilter 必须放在过滤器链最前面forceEncoding 设为 true这样请求和响应都会被强制转成 UTF-8。spring-mvc.xml 里三行关键配置context:component-scan base-packagecom.example.tender.controller/ mvc:annotation-driven/ mvc:default-servlet-handler/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /beanmvc:default-servlet-handler 是必配项否则页面里引用的 css、js、图片都会被子 Servlet 拦截HTML 能打开但样式全丢。视图解析器指向 WEB-INF/jsp 目录因为 WEB-INF 下的文件不能通过 URL 直接访问用户绕开登录直接打开 JSP 的路径被物理堵死招投标系统的投标文件和报名状态尤其需要这层保护。2.3 用 Maven 打包 war 并部署到本地 Tomcat配置写完后验证整体环境能不能跑常见做法就是一条命令mvn clean package命令执行完target 目录下会生成 .war 文件把它复制到 Tomcat 的 webapps 目录执行bin/catalina.sh run然后访问 http://localhost:8080/项目名/ 就能进入系统首页。catalina.sh run 会把完整日志直接打在终端启动失败时错误信息比 startup.sh 清楚得多部署阶段我一般用 run 而不是 startup。如果你用 IDEA可以配置 Tomcat Server - LocalDeployment 里选 war explodedApplication context 改成 /tender。war exploded 模式的好处是 JSP 修改后不需要重新打包刷新页面就能看到对调页面极其重要。部署时不要勾选 “Deploy applications configured in Tomcat instance”避免 Tomcat 自动部署机制覆盖掉你手动指定的上下文路径。3. 招投标系统的数据库设计五张核心表和状态机才是灵魂3.1 角色权限与核心表设计用户、公告、投标记录网上招投标系统的核心业务按角色流转管理员发布公告和定标投标企业注册后报名并提交投标文件评标人员查看报价和资质最后由管理员统一公示。落到数据库里除了常规的 sys_user 用户表至少要保证五张核心表职责清晰用户表、招标公告表、投标报名表、投标记录表、中标结果表。我见过不少毕设把投标报名和投标文件塞在同一张表里后面写评标结果时不得不加字段代码越写越僵。先给用户表的建表 SQLCREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt密文, user_type TINYINT NOT NULL COMMENT 1管理员 2投标企业 3评标专家, company_name VARCHAR(100) COMMENT 企业全称仅投标企业必填, contact_phone VARCHAR(20) COMMENT 联系电话, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_type (user_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码不要用 MD5 直接存至少加盐。毕业设计答辩时算法与安全这块经常被追问用 BCrypt 或 SHA-256 加盐能少很多尴尬。user_type 用数字而不是字符串方便权限拦截器里直接判断。status 字段比直接删记录合理因为已经报名的项目还需要保留历史关联。接下来是招标公告表和投标记录表。公告表要有标题、编号、正文、预算金额、报名起止时间、投标截止时间、状态、发布人。最容易做错的是金额字段招投标里的预算和报价会参与比较与合计用 double 存储会出现 0.1 0.2 不等于 0.3 的问题评标环节这是硬伤。CREATE TABLE tender_notice ( id INT PRIMARY KEY AUTO_INCREMENT, notice_no VARCHAR(30) NOT NULL COMMENT 公告编号如ZB-2025-001, title VARCHAR(200) NOT NULL, content TEXT COMMENT 富文本HTML内容, budget DECIMAL(12,2) COMMENT 预算金额, signup_start DATETIME, signup_end DATETIME, bid_end DATETIME COMMENT 投标截止时间, status TINYINT DEFAULT 0 COMMENT 0草稿 1已发布 2报名中 3投标中 4评标中 5已定标 6流标, publisher_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status_end (status, bid_end) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT招标公告表; CREATE TABLE tender_bid ( id INT PRIMARY KEY AUTO_INCREMENT, notice_id INT NOT NULL, company_id INT NOT NULL COMMENT 投标企业用户ID, bid_price DECIMAL(12,2) COMMENT 投标报价, file_path VARCHAR(255) COMMENT 投标文件服务器路径, file_md5 VARCHAR(64) COMMENT 文件摘要查重用, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0有效 1废标 2中标, UNIQUE KEY uk_notice_company (notice_id, company_id), KEY idx_submit (submit_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投标记录表;DECIMAL(12,2) 表示整数部分最多 10 位、小数 2 位覆盖千万级预算没有精度问题。唯一索引 uk_notice_company 从数据库层面杜绝同一家企业对同一个项目重复投标这部分约束必须在 Dao 层做Controller 里判断只是第一道防线。提示status 字段不要用枚举字符串存储TINYINT 数字既能满足查询也方便后续做状态机的位运算和批量流转。3.2 状态机设计投标截止时间一到项目状态能否自动流转网上招投标系统和普通 CRUD 的区别在于状态不是靠人手工改而是靠时间和事件驱动。公告发布后应该进入“报名中”报名截止时间过了要自动变成“投标中”投标截止后要进入“评标中”。如果全让管理员手动点按钮就会出现一个已经过期的项目还在首页置顶的尴尬。状态定义可以单独做一个枚举类public enum TenderStatus { DRAFT(0, 草稿), PUBLISHED(1, 已发布), SIGNING(2, 报名中), BIDDING(3, 投标中), REVIEWING(4, 评标中), AWARDED(5, 已定标), FAILED(6, 流标); private final int code; private final String desc; // 构造方法与 getter 略 }列表查询时status 不能只查数据库里的静态值要有一部分“虚拟状态”。常见做法是在 Service 层做一次状态刷新public void refreshStatus(ListTenderNotice notices, Date now) { for (TenderNotice n : notices) { TenderStatus cur TenderStatus.from(n.getStatus()); if (cur TenderStatus.PUBLISHED n.getSignupEnd().before(now)) { n.setStatus(TenderStatus.SIGNING.getCode()); } if (cur TenderStatus.SIGNING n.getBidEnd().before(now)) { n.setStatus(TenderStatus.BIDDING.getCode()); } } }这段逻辑每次查询公告列表时把当前系统时间和报名截止时间、投标截止时间做比较用现算的“虚拟状态”覆盖数据库里的静态状态。这里不要每次查询都 update 数据库直接在内存里改实体对象返回给页面等用户操作详情或管理员主动终止项目时再持久化。这种设计让公告过期变成自动行为不用额外起定时任务。能在答辩时讲清楚“虚拟状态”和“持久化状态”的区别关于 java 状态对象和 java.util.Date 比较的追问也能应付过去。3.3 MyBatis 分页查询与条件拼接三个参数的边界公告列表页是招投标系统访问最频繁的页面分页加上关键字、状态、时间范围三个条件是最常见查询场景。SSM 里不要手动写 limit 偏移量直接用 PageHelper 分页插件。先在 pom 里引入依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.1.11/version /dependency然后在 spring-mybatis.xml 的 SqlSessionFactoryBean 里配置拦截器bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameplugins array bean classcom.github.pagehelper.PageInterceptor property nameproperties valuehelperDialectmysql/value /property /bean /array /property /beanService 层查询前写一行 PageHelper.startPage(pageNum, pageSize) 即可。这个参数的边界很关键PageHelper 只对紧随其后的第一条查询生效如果 startPage 写在循环里或者查询语句之前又插了别的 SQL分页就会静默失效。pageNum 从 1 开始pageSize 一般给 10总条数通过 PageInfo.getTotal() 拿到。再者是 mapper 里的动态 SQL。公告列表的条件组合这样写select idselectNoticePage resultTypecom.example.tender.entity.TenderNotice SELECT id, notice_no, title, budget, signup_start, signup_end, bid_end, status FROM tender_notice where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR notice_no LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /select这里两次 LIKE 拼接不要写成 LIKE %#{keyword}%预处理语句会把 #{} 当字面值处理。用 CONCAT 拼接是 MyBatis 下最稳妥的模糊查询写法。status 的判断不能写成 status ! 因为数字 0 会被空字符串逻辑误伤Integer 类型只用判不为 null 即可。这些边界条件在改完 SQL 后要用零值和空串各测一次。4. JSP HTML 展示层把公告数据变成能投标的页面4.1 用 EL 和 JSTL 渲染招投标列表页网上招投标系统中页面数量最多的不是 HTML 静态页而是 JSP 动态渲染的数据页。JSP 本质上是在 HTML 里嵌入标签语言通过 EL 表达式从 request 域取数据再配合 JSTL 做循环、判断和格式化。JSP 里尽量不要写大段的 % % Java 代码逻辑放在 Controller页面只负责取值和展示这是整套 SSM 结构能长期维护的原因。招标公告列表页的 JSP 文件开头引入标签库% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table c:forEach items${pageInfo.list} varnotice tr tda href${pageContext.request.contextPath}/notice/detail?id${notice.id}${notice.title}/a/td tdfmt:formatDate value${notice.signupEnd} patternyyyy-MM-dd HH:mm//td td c:choose c:when test${notice.status 2}报名中/c:when c:when test${notice.status 3}投标中/c:when c:otherwise已截止/c:otherwise /c:choose /td td c:if test${sessionScope.loginUser.userType 2} a href${pageContext.request.contextPath}/bid/submit?noticeId${notice.id}我要投标/a /c:if /td /tr /c:forEach /table这段 JSP 值得注意的地方有三个。c:forEach items 指向的是 PageHelper 返回的分页对象 pageInfo 中的 list不能直接遍历 request 里的 pageInfo 本身。fmt:formatDate 把 java.util.Date 转成指定格式字符串如果时间显示和数据库差 8 小时问题百分之百在 JDBC 连接串。最后 c:if 里对 userType 的判断是最简单的行级权限只有投标企业看到“我要投标”入口管理员看到的是“编辑公告”。权限粒度再细一点可以再加一个判断当前时间必须落在 signupStart 和 bidEnd 之间才显示按钮这一步放在后端做更安全。4.2 富文本公告发布与附件上传的两个配置点发布招标公告的正文是富文本 HTML。页面上用开源编辑器把编辑区变成可插入表格、图片的视图后端拿到的是带 HTML 标签的字符串直接存进 tender_notice.content 字段。这里容易被忽视的坑是富文本内容回显时会按 HTML 解析用户如果通过编辑器源码模式插入 script 标签就构成存储型 XSS 漏洞。后端防护其实很朴素输出时做转义而不是修改输入。JSP 里凡是用户可控内容都用 JSTL 的 c:out 转义输出c:out value${notice.title} escapeXmltrue/但正文不能直接用 c:out因为会把富文本里的 b、img 标签一并转义成纯文本。更合适的做法是标题和摘要用 c:out正文在保存时用过滤器把 script、iframe、onerror 等危险属性和标签剔除后再入库。毕设阶段写一个 HTML 白名单过滤器比引入一整套安全框架更实际。附件上传是另一个必配项。投标文件上传在 SSM 里靠 commons-fileupload 实现spring-mvc.xml 里配置解析器bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value52428800/ property namedefaultEncoding valueUTF-8/ /beanmaxUploadSize 单位是字节52428800 就是 50MB公司资质扫描件一般 20MB 以内够用。注意 bean 的 id 必须写成 multipartResolverSpringMVC 才会把它当作上传解析器。defaultEncoding 要和页面表单的 accept-charset 一致否则中文文件名上传后乱码。文件保存路径建议配置到外部目录而不是项目里String dir uploadDir; // 由 properties 配置如 D:/tender_upload/ String fileName UUID.randomUUID().toString().replace(-, ) suffix; String fullPath dir fileName; part.write(fullPath);用 UUID 重命名文件能避开两个问题一是中文文件名在不同 Tomcat 版本下编码不一致二是危险后缀jsp、exe不经过校验直接落盘。数据库里只存相对路径页面通过虚拟路径映射访问。这里强烈建议把上传目录配在 Tomcat 的 webapps 之外因为重新部署项目时 webapp 目录会被整个覆盖上传文件跟着就没了这是毕设项目最常见的丢文件事故。4.3 JSP 里的两个展示细节图片坐标定位和 MP4 视频播放招标公告里常有现场踏勘图、园区图纸这类图片运营人员希望在图上标出“本次施工边界”的可点击区域这就是 jsp 图片如何对坐标定位的典型场景。常见做法不是引入 JS 库而是用 HTML 原生 map 标签img src${pageContext.request.contextPath}/upload/site.jpg usemap#siteMap alt踏勘平面图 map namesiteMap area shaperect coords120,80,320,260 href${pageContext.request.contextPath}/notice/detail?id18 title施工区域A area shapecircle coords640,300,50 href${pageContext.request.contextPath}/notice/detail?id19 title临时用电接入点 /mapcoords 的四个数字对应矩形的左上角 x、y 和右下角 x、y数值要从原图的实际像素尺寸量取而不是按显示尺寸。很多人拿响应式缩放后的截图去量坐标结果热区和图片错位。务实的做法是先把图片设置成固定原图宽度量好 coords 后再给 img 加 max-width:100%浏览器会按比例缩放热区。如果嫌坐标换算麻烦也可以在前端用透明 div 绝对定位模拟点击区域但要监听鼠标事件比 map 方式复杂。招标答疑会的视频回放在 JSP 页面用 HTML5 video 标签实现video idrecordVideo controls preloadmetadata src${pageContext.request.contextPath}/upload/review_2025.mp4 stylewidth:100%;max-height:480px/video最常见的翻车点是浏览器不播 mp4。确认两件事video 源路径在静态资源映射范围内文件编码是 H.264 AAC而不是浏览器不支持的 MPEG-4 Part 2 编码。转码用 FFmpeg 默认参数转一次基本能通。如果需要限制投标人看完视频才能填写确认信息通过 video 的 timeupdate 事件判断 currentTime 大于 duration 的 90% 后启用表单按钮这个逻辑在投标须知页面上很实用。还有一类需求是把公告内容导出成 Word 或 Excel 存档。常见做法是在后端用 Apache POI 生成文件流下载而不要把网页内容直接复制进 WPS 表格——HTML 转表格后样式错乱、合并单元格丢失是必然的POI 能精确控制列宽和单元格合并对评标记录导出尤其有用。5. 网上招投标系统开发避坑现象、原因与解决5.1 启动报 NoSuchMethodErrorMaven 依赖冲突现象Tomcat 启动时控制台抛出 java.lang.NoSuchMethodError: org.springframework.util.ClassUtils.isPresent(...)或者 MyBatis 相关的 AbstractMethodError工程直接起不来。原因依赖树里存在多个版本的 Spring 或 MyBatis。最常见的是某个传递依赖把 spring-core 拉到了 4.x而代码用的是 5.1 的 API另一种是 mybatis 和 mybatis-spring 版本不匹配mybatis-spring 2.0.x 要求 mybatis 3.5pom 里如果写的是 mybatis 3.4.6运行期方法签名就对不上。解决先查依赖树命令如下mvn dependency:tree -Dincludesorg.springframework把输出里所有重复的版本号找出来在 pom 的 dependencyManagement 里统一锁定。mybatis 统一用 3.5.6mybatis-spring 统一用 2.0.6这个组合在 SSM 工程里经过最多验证。改完执行 mvn clean package 重新打包不要只点 IDEA 的刷新按钮。5.2 页面中文全部变成问号现象登录页能打开输入中文昵称保存后数据库里是正常的但列表页面显示 ??或者从数据库查出来的中文在 HTML 里是乱码。原因三层编码不一致。第一层是 JSP 页面没声明 contentType第二层是 Tomcat 接收 GET 请求时 URIEncoding 默认是 ISO-8859-1第三层是 MySQL 连接串没加 characterEncodingutf8导致 JDBC 写入用了 MySQL 默认的 latin1。只改其中一层没有用。解决三处一起设置。JSP 文件头保留 charsetUTF-8Tomcat 的 server.xml 里给 Connector 加 URIEncodingUTF-8JDBC 连接串写成jdbc:mysql://localhost:3306/tender?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai三处一起改是血泪经验别一层一层试。已经乱码的存量数据重新查出来后用 String 的 getBytes(ISO-8859-1) 转回 UTF-8 修复一次新写入的数据不会再乱。5.3 页面链接带着项目名就 404现象首页能打开从列表点详情页 URL 变成 /notice/detail?id1报 404。手动在浏览器输入 http://localhost:8080/tender/notice/detail 反而正常。原因JSP 里的 a 标签 href 写的是 “notice/detail?id18” 这种相对路径。在根路径访问时它相对站点根目录解析成 /notice/detail而部署上下文是 /tenderTomcat 在 /notice 下找不到对应 Servlet。解决所有 JSP 路径都改成绝对上下文路径用 ${pageContext.request.contextPath} 开头即 href${pageContext.request.contextPath}/notice/detail?id18。Controller 的 redirect 返回值如果写 redirect:notice/list也要改成带上下文路径的形式或者直接返回 redirect:/notice/list 让 DispatcherServlet 自己带上上下文。出现 404 时先用浏览器开发者工具看请求 URL凡是少了项目名的请求基本都是这个原因。5.4 改了 JSP 刷新还是旧页面现象在 IDEA 里修改了 JSP 的表格列重启 Tomcat浏览器强制刷新还是显示修改前的页面。原因JSP 不是直接执行的而是先被 Jasper 引擎编译成 Servlet 的 Java 文件再编成 class 并缓存。Tomcat 的 work 目录下存在旧编译产物war exploded 部署没有清理就会一直加载旧 class。另一个隐蔽原因是浏览器缓存了 HTML 响应。解决重启前执行 IDEA 的 Rebuild Project再删掉 Tomcat 的 work/Catalina 目录。如果还是不行把 target 目录和 out 目录全部清理一遍确保没有残留的 class。排查时打开 Tomcat 的 localhost 日志这种问题很多时候不是玄学就是编译缓存没清理干净。5.5 时间显示差 8 小时而且日期格式带个 “T”现象公告列表数据库里存了 2025-04-08 14:30:00页面上显示 2025-04-08T14:30:00或者多出 8 小时变成 22:30。原因分两层。带 T 是 Jackson 序列化 java.util.Date 时默认输出 ISO 格式日期和时间之间用 T 分隔多 8 小时是 MySQL 连接串没配 serverTimezoneJDBC 客户端和服务器之间时区换算错误。解决实体类时间字段加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private Date signupEnd;如果用的是 LocalDateTime还需要引入 jackson-datatype-jsr310 依赖并把 Jackson 的全局格式也配成上面的 pattern。同时检查连接串是否带 serverTimezoneAsia/Shanghai这是 MySQL 8.x 时代最容易漏的参数。调完之后不要在数据库客户端里对比时间数据库显示由会话时区决定以页面输出和接口返回为准。6. 验收前再做一次全链路清单两个扩展点让答辩更有亮点6.1 功能验收清单按下面清单把流程完整走一遍能覆盖 90% 的功能缺漏编号业务场景预期行为1管理员登录跳转后台非管理员访问后台接口被拦截2发布招标公告富文本内容入库列表页可见报名按钮在报名时间范围内可用3投标企业注册注册后状态为待审核审核通过才能提交投标文件4报名截止后刷新列表项目状态自动从报名中变为投标中5同一企业重复投标数据库唯一索引拦截页面给出“已提交过该标段”提示6评标录入价格金额合计用 BigDecimal 计算不出现浮点误差7定标后公示中标结果页只对登录用户开放游客访问跳回登录页8上传超过 50MB 文件返回“文件过大”提示不抛异常堆栈这八条手工在界面上点一遍每条花不了五分钟。招投标系统的短板往往不在主流程而在边界条件截止时间过后用户还能不能点“投标”同一个用户能不能连续提交两次这类判断要在 Controller 和数据库两层都做。6.2 值得动手做深的两块第一个扩展点是评标结果的排序展示。用 Collections.sort 或冒泡排序对投标报价从低到高排列并在页面上标出前三名正好把 java 排序的知识点落进业务场景。第二个扩展点是投标文件查重。tender_bid 表里的 file_md5 字段已经存了文件摘要在管理员端做一个按 md5 分组检测的接口查出同一份文件被不同企业重复提交的情况这是防围标最朴素的一个工业实现也是评委最可能感兴趣的话题。我自己带项目的习惯是验收三件事按顺序来先跑通角色闭环再测异常路径最后才检查页面样式。不要在样式上磨一整天核心流程通了再谈美化。坚持这个顺序毕业设计和真实项目的验收逻辑是一致的希望帮到你。本文还有配套的精品资源点击获取

相关新闻

eNSP中DHCPv6地址自动配置实战:从原理到抓包排错

eNSP中DHCPv6地址自动配置实战:从原理到抓包排错

1. 为什么要在 eNSP 里动手搭 DHCPv6:从"手动配死"到"地址自动下发"1.1 这个实验到底在解决什么问题先说个很多人都有过的经历:刚接触 IPv6 的时候,总觉得它和 IPv4 没什么本质区别——无非就是把 32 位的地址换成了 128…

2026/10/9 3:09:57 阅读更多 →
无法访问 Windows Installer 服务?msiserver 注册表修复与错误 1053 排查

无法访问 Windows Installer 服务?msiserver 注册表修复与错误 1053 排查

前两天有同事拿安装包来找我,说一运行就弹“无法访问 Windows Installer 服务”,装什么都装不上,卸载也卸不掉。这个问题我在 Windows 10、Windows 11 上碰到过不少次,尤其是装 MySQL Installer、Navicat、博途这类带一堆依赖的软…

2026/10/9 3:09:57 阅读更多 →
Java List 全面解析:从 ArrayList 扩容到 LinkedList 选型

Java List 全面解析:从 ArrayList 扩容到 LinkedList 选型

写Java写了快十年&#xff0c;List大概是我用过最多的集合接口&#xff0c;没有之一。但真正让我把这玩意儿彻底想明白的&#xff0c;不是业务代码写了多少遍new ArrayList<>()&#xff0c;而是某次面试候选人时&#xff0c;我顺口问了一句"ArrayList 的扩容机制了解…

2026/10/9 3:09:57 阅读更多 →

最新新闻

Agency-agents:AI智能体编排架构实战指南

Agency-agents:AI智能体编排架构实战指南

1. 项目概述&#xff1a;从“agency-agents”看现代AI开发范式的底层迁移“agency-agents”这个词乍一看像一个拼写错误&#xff0c;或是某个未公开项目的代号&#xff0c;但如果你最近在GitHub Trending、Hacker News热帖或AI工程师的Slack频道里刷到它&#xff0c;大概率不是…

2026/10/9 4:12:36 阅读更多 →
CUDA内存层次结构实战:共享内存与全局内存优化指南

CUDA内存层次结构实战:共享内存与全局内存优化指南

1. 先画一张全景图&#xff1a;为什么内存层次结构决定了GPU性能上限很多刚接触Python CUDA编程的朋友&#xff0c;第一反应往往是“哇&#xff0c;GPU有几千个核心&#xff0c;跑并行一定飞快”。这个直觉没错&#xff0c;但实际写出来的核函数&#xff0c;性能表现经常远低于…

2026/10/9 4:12:36 阅读更多 →
高斯滤波详解:从数学原理到工程调参与代码实现

高斯滤波详解:从数学原理到工程调参与代码实现

做图像处理这些年&#xff0c;高斯滤波大概是我用得最频繁的一个操作&#xff0c;没有之一。工业视觉里的瑕疵检测、医学影像的预处理、手机拍照后的降噪&#xff0c;几乎每条图像处理管线的第一道工序都是它。名字听着学院派&#xff0c;但说白了就是一句话&#xff1a;把图像…

2026/10/9 4:12:36 阅读更多 →
基于Matlab的用户侧储能参与辅助服务优化配置与经济分析

基于Matlab的用户侧储能参与辅助服务优化配置与经济分析

储能项目“能建”不难&#xff0c;难在“怎么建才划算”。这句话放在用户侧储能参与辅助服务的场景里&#xff0c;尤其成立。容量配置小了吧&#xff0c;削峰、调频、备用这些收益来源全都撑不起来&#xff1b;容量配置大了吧&#xff0c;初始投资压力陡增&#xff0c;电池实际…

2026/10/9 4:12:36 阅读更多 →
AngularJS `$digest` 与 `$apply` 深度解析:jstips 精讲双向数据绑定循环机制与性能取舍

AngularJS `$digest` 与 `$apply` 深度解析:jstips 精讲双向数据绑定循环机制与性能取舍

教程 【免费下载链接】jstips This is about useful JS tips! 项目地址&#xff1a; https://gitcode.com/gh_mirrors/js/jstips 点击查看 免费下载 导读 本篇基于 jstips 项目第 01 号技巧&#xff08;关联文档&#xff0c;另有英文原版、简体中文版与西班牙语版&#xff09…

2026/10/9 4:12:36 阅读更多 →
Agent-Reach CLI实战:AI Agent环境搭建与任务编排指南

Agent-Reach CLI实战:AI Agent环境搭建与任务编排指南

1. 从"Agent-Reach"这个名字说起&#xff1a;它到底想解决什么问题第一次看到 Agent-Reach 这个项目名&#xff0c;我的直觉是&#xff1a;这又是一个把 AI Agent 和"触达"绑在一起的工具。事实也确实如此。Agent-Reach 的核心定位&#xff0c;是给 AI Age…

2026/10/9 4:11:35 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题&#xff0c;隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题&#xff0c;排查到最后发现是ZonedDateTime序列化后时区丢了&#xff0c;用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问&#xff1a;办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好&#xff0c;问题是工作场景经常要在几处环境之间来回切换&#xff0c;每次都先登录跳板机再层层代理&#xff0c;实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及&#xff0c;但真正动手搭过一套能跑起来的 Agent 系统的人都知道&#xff0c;从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地&#xff0c;从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →