JSP科研成果申报管理系统:从数据库设计到部署避坑实战
简介这是一份科研成果申报管理系统的设计与实现分析文档采用JSPJavaBean架构并结合SQL Server 2000数据库适合高校计算机专业毕业设计及科研管理平台开发者参考。文档从系统开发背景与设计目标入手依次介绍了技术选型、系统建模、可行性分析和需求分析划分出申报管理、审批管理、用户管理等核心功能模块数据库设计部分给出了数据流图、实体关系图与数据表结构定义保障数据的一致性和完整性。系统实现部分分析了JavaBean关键组件以及主界面、科研申报、登录、课题申报、个人信息修改、审批、查询等模块的运行效果和核心源代码。资源为单个doc文件大小516KB已有62人学习可作为毕业设计论文参考和系统开发蓝本。1. 为什么到现在还有人用 JSP 做科研成果申报系统先把业务账算清楚JSPJava Server Pages做科研成果申报管理系统听起来像是上一个 Web 时代的组合但在高校和科研院所的毕设选题池里它依然高频出现。这个标题背后是一个很实际的诉求用 JSP 把申报—审核—统计这一整条业务闭环落地从数据库设计到页面渲染每一步怎么选、怎么实现、坑在哪。适合两类人准备拿它做毕设或课设的学生以及想快速给单位搭一个内网管理系统的初级开发。读完你会得到一套最小可运行的 JSP 项目骨架以及比骨架更值钱的部署细节、并发审核边界和典型翻车点。2. 科研成果申报管理系统到底在管什么从业务流程到数据模型做系统之前先把业务账算清楚很多毕设翻车不是代码写不出来是表设计不对、流程没理顺。科研成果申报这个场景放在任何一所高校或科研院所流程都惊人地相似教师填报成果科研秘书初审分管领导终审年底再按成果类型做统计。想清楚这三段流程数据表和页面结构就都跟着出来了。2.1 申报、审核、统计的三段式流程状态值怎么设计整个系统的核心不是页面做得多好看而是一条状态主线。每个申报单从创建到入库要经过这么几个状态0草稿教师保存了还没提交本人可见1已提交待初审科研秘书进入审核列表2初审通过待终审分管领导进入终审列表3终审通过入库可以参与年度统计-1初审驳回教师修改后可再次提交-2终审驳回教师修改后可再次提交状态用数字而不是字符串是因为数字在数据库里占 1 个字节、索引效率高代码里用常量类统一管理不会出现已通过审核通过 approved 这种同一含义三种写法的问题。状态只走这一条主线不建议再加撤回转交之类分支动作——流程每多一个分支前端菜单、权限判断、SQL 条件都会跟着复杂一截对毕设来说性价比太低。如果导师坚持要撤回功能也简单撤回本质是把状态从 1 改回 0前提是审核人还没点过通过按钮SQL 里带上 status 1 的 WHERE 条件就行这个细节在第 4 章审核流里会再讲。统计这一段依赖的是状态为 3 的数据。注意统计口径要和状态绑定不能看 create_time而要看 submit_time因为有的教师会把一份申报单当草稿存两个月提交时间才是真正的申报年度。2.2 数据表设计申报表、成果表、审核记录的字段取舍四张表能覆盖所有需求用户表、申报主表、成果明细表、审核日志表。下面是可直接执行的建表 SQL字符集统一用 utf8mb4避免以后存入生僻字或 Emoji 时出现乱码。CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT 1-教师 2-科研秘书 3-分管领导 4-管理员, dept VARCHAR(100) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_declaration ( decl_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, category VARCHAR(50) NOT NULL COMMENT 论文/专利/获奖/课题, abstract_txt TEXT, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-草稿 1-已提交 2-初审通过 3-终审通过 -1初审驳回 -2终审驳回, submit_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status (status), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_achievement ( ach_id INT PRIMARY KEY AUTO_INCREMENT, decl_id INT NOT NULL, journal_name VARCHAR(200), publish_date DATE, authors VARCHAR(255), attachment_path VARCHAR(255), score DECIMAL(5,2) DEFAULT 0 COMMENT 用于年度工作量结算, KEY idx_decl (decl_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_audit_log ( log_id INT PRIMARY KEY AUTO_INCREMENT, decl_id INT NOT NULL, auditor_id INT NOT NULL, action VARCHAR(20) NOT NULL COMMENT submit/approve/reject, comment_text VARCHAR(500), audit_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_decl (decl_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段取舍上最容易出问题的是三处。第一t_declaration 的 status 为什么不用字符串 varchar上面已经解释了数字状态配合代码里的 StatusConstant 类最省心。第二t_audit_log 为什么要单独开表而不是在申报主表里放一个 audit_comment 和一个 audit_time因为一份申报单可能被驳回、修改、再提交、再审核主表只放一个意见字段会被下一次审核覆盖掉历史记录全丢单独一张日志表才能追溯谁在什么时候做了什么决定答辩时这是很好的加分点。第三附件不直接存数据库数据库只存 attachment_path 路径文件落本地磁盘或对象存储否则数据库体积会迅速膨胀备份和迁移都痛苦。2.3 角色权限教师、科研秘书、分管领导各自看到什么权限模型不搞复杂的 RBAC 框架三种角色加一个管理员足够承载全部需求。权限矩阵如下功能教师科研秘书分管领导管理员新建/修改申报单能不能不能视同教师提交申报单能不能不能不能初审状态 1-2 或 -1不能能不能不能终审状态 2-3 或 -2不能不能能不能查看统计报表只能看自己的全院/全所全院/全所全量用户管理不能不能不能能实现上不引入 Spring Security 这类重武器用 Servlet Filter 按 URL 前缀过滤就够了。约定 /decl/* 开头需要登录/audit/* 开头只允许角色 2 和 3/admin/* 只允许角色 4。页面菜单用 JSTL 判断当前登录用户的 role 字段渲染教师登录后根本看不到审核列表这个入口。这种写法的好处是过滤器一眼能看懂坏处是 URL 约定一旦被绕过就会越权所以过滤器里要同时校验 session 中的 user 对象是否为空、角色是否匹配两个条件缺一不可。3. 搭建一个能跑的最小 JSP 项目环境选型与 IDEA 创建参数这一章聊的是 jsp 入门的关键一步环境怎么选、项目怎么建。很多新手在 IDEA 里新建项目时纠结选 Jakarta EE 还是 Maven其实方向错了。你现在要的不是最新最炫的框架而是一个能稳定跑完答辩、代码在网上搜得到参考的经典组合。3.1 环境选型JDK 8 Tomcat 8.5 Servlet 3.1 的兼容性理由先说版本结论这是血泪经验换来的JDK 8 Tomcat 8.5 IntelliJ IDEA 2021 或更早的版本是跑传统 JSP 项目最稳的组合。原因有三层。第一JDK 8 直到今天仍然是大量高校机房的默认版本你写代码用的语法特性Lambda、Stream在 JDK 8 里都支持但到了 JDK 17 或 21 反而要适配模块化纯属给自己加戏。第二Tomcat 8.5 支持 Servlet 3.1请求默认就用 UTF-8 解码相比 Tomcat 7 少踩一个 GET 请求中文乱码的坑这对管理系统来说是刚需。第三Servlet 3.1 支持 WebServlet 注解不需要在 web.xml 里给每个接口写一大段servlet-mapping代码整洁程度高一个档次。不建议用 Tomcat 10 的原因更实际它把包名从 javax.servlet 迁移到了 jakarta.servlet而你网上搜到的绝大多数 JSP 教程、CSDN 示例、学长学姐留下的项目代码全是 javax 包名。照着抄会直接编译报错对毕设来说这是纯时间成本。等做完了核心功能想升级再升级不要一开始就挑战生态不兼容的版本。3.2 在 IDEA 里新建 JSP 项目Maven 骨架与 pom.xml 关键依赖IDEA 新建 jsp 项目我推荐走 Maven Archetype 而不是 New Project 里的 Jakarta EE 模板原因是 Maven 骨架自带 pom.xml 和打包插件后续传统 JSP 项目打包 war 直接mvn package就完事。操作步骤是File → New → Project → Maven Archetype选择 maven-archetype-webapp填好 groupId 和 artifactId然后手动补出 src/main/java 和 src/main/resources 两个目录。pom.xml 的关键依赖如下直接抄这份配置逐条解释在后面。project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdscientific-research-decl/artifactId version1.0-SNAPSHOT/version packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version1.4/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency /dependencies build finalNamescientific-research-decl/finalName /build /project逻辑说明packaging必须是 war这样构建产物才能被 Tomcat 直接识别。javax.servlet-api的 scope 设置成provided很关键因为 Tomcat 自带了 Servlet 容器实现如果打成 jar 包塞进 WEB-INF/lib运行时反而会出现类冲突或方法签名不一致。jstl1.2 一个 jar 就同时包含 API 和实现不需要再引 standard.jar。mysql-connector-java用 5.1.49 是因为它兼容 MySQL 5.7 和 8.0写 JDBC URL 时不需要额外加时区参数。finalName的值决定最终 WAR 文件名也就是部署后的上下文路径起一个有意义的名字后面排查 404 时能少点头疼。web.xml 在 src/main/webapp/WEB-INF 下最小配置只要声明一个 welcome-file 列表让访问根路径时自动跳转到首页。前提是目录结构完整标准 Maven Web 项目结构是这样的src/main/java # Java 源码 src/main/resources # 配置文件 src/main/webapp # JSP、静态资源、WEB-INF3.3 JSP Servlet JavaBean 三层写法从登录接口看请求链路JSP 项目的经典三层分工是JSP 只负责渲染页面Servlet 只做请求控制JavaBean/DAO 只碰数据库。最常见的反模式是在 JSP 里直接写% %连数据库查数据那种代码当时能跑加一个页面就乱成一坨答辩老师看到也会皱眉。用一个登录接口串起请求链路。先写 JavaBean只存放数据不写业务逻辑public class User { private int userId; private String username; private String password; private String realName; private int role; private String dept; public int getRole() { return role; } public void setRole(int role) { this.role role; } // 其余字段的 getter/setter 省略IDEA 自动生成即可 }再写 LoginServlet注意第一行代码必须是设置请求字符集这一行放错位置乱码就出现了WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, password); if (user ! null) { request.getSession().setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /index.jsp); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }逻辑说明登录成功后用sendRedirect返回 302 让浏览器重新发起请求然后由 index.jsp 渲染首页登录失败用forward在服务端内部跳转回登录页同时把错误信息放进 request 作用域。这两个跳转方式的区别值得在答辩时讲清楚sendRedirect 会丢失 request 属性所以错误信息必须用 forward 传而 index.jsp 要从 session 里取用户信息所以登录成功后把 user 放进 session。最后是 login.jsp 的表单主体form 的 action 指向 Servlet 的映射路径method 必须为 postform action${pageContext.request.contextPath}/login methodpost input typetext nameusername placeholder用户名/ input typepassword namepassword placeholder密码/ button typesubmit登录/button /form c:if test${not empty errorMsg} p stylecolor:red${errorMsg}/p /c:if这里的${pageContext.request.contextPath}是 JSP EL 表达式用来动态获取项目上下文路径这样无论部署后叫 scientific-research-decl 还是 ROOT表单都不需要改。整套流程走下来用户点击登录 → Tomcat 解析参数 → Servlet 调 DAO → 查库 → 回写 session → 页面跳转这条链路就完整了。4. 核心模块设计实现申报表单、审核事务与统计报表系统能不能用就看申报、审核、统计这三个模块。前一章把骨架搭好这一章把肉填上。4.1 申报表单文件上传与 multipart 表单解析科研成果申报里论文的 PDF、专利的扫描件都要作为附件上传。JSP 页面里表单只要加了enctypemultipart/form-data普通字段的获取方式就完全变了这是新手最容易困惑的点。form action${pageContext.request.contextPath}/decl/submitDecl methodpost enctypemultipart/form-data input typetext nametitle placeholder成果标题/ input typetext namecategory placeholder成果类别/ input typefile nameattachment/ button typesubmit提交申报/button /form对应 Servlet 里的解析代码WebServlet(/decl/submitDecl) public class SubmitDeclServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); if (!ServletFileUpload.isMultipartContent(request)) { response.sendError(400, 请使用 multipart/form-data 编码提交); return; } DiskFileItemFactory factory new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 超过 1MB 写入临时文件 ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(10 * 1024 * 1024); // 单文件最大 10MB try { ListFileItem items upload.parseRequest(request); String title null; String category null; FileItem attachItem null; for (FileItem item : items) { if (item.isFormField()) { if (title.equals(item.getFieldName())) { title item.getString(UTF-8); } else if (category.equals(item.getFieldName())) { category item.getString(UTF-8); } } else { attachItem item; } } String saveDir /data/decl_attachments; File dir new File(saveDir); if (!dir.exists()) dir.mkdirs(); String fileName UUID.randomUUID().toString() _ attachItem.getName(); attachItem.write(new File(dir, fileName)); // 组装 Declaration 对象并插入数据库此处省略 DAO 调用 } catch (Exception e) { throw new ServletException(文件上传解析失败, e); } } }逻辑说明表单一旦声明了 multipartrequest.getParameter(title)就一定返回 null因为 multipart 的请求体是二进制流格式Tomcat 不会主动把字段解析进 parameterMap。必须通过upload.parseRequest(request)拿回 FileItem 列表再逐个判断isFormField()区分普通字段和文件字段。item.getString(UTF-8)解决的是 multipart 表单里中文字段的乱码问题文件名前拼 UUID 是为了防止两个教师传了同名文件互相覆盖setSizeThreshold和setFileSizeMax是文件上传必须调的两个参数前者控制内存阈值后者控制文件总大小上限生产环境里还要把临时目录单独配置。4.2 审核流用状态字段加事务边界防并发翻车审核动作本质上就是一句话把状态从当前值改成下一个值同时记录一条日志。听着简单但并发环境下很容易翻车。两个审核员同时打开同一份申报单一个点通过一个点驳回没有控制的话最终状态完全取决于谁后提交这就出事故了。解决办法是条件更新加事务。下面是审核方法的核心骨架public boolean audit(int declId, int auditorId, String action, String comment) { Connection conn null; PreparedStatement psUpdate null; PreparedStatement psLog null; try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); // 条件更新只允许从 expectedStatus 变到 nextStatus int expectedStatus approve.equals(action) ? 1 : 2; // 初审通过 1-2 int nextStatus approve.equals(action) ? 2 : -1; String updateSql UPDATE t_declaration SET status?, audit_comment?, audit_timeNOW() WHERE decl_id? AND status?; psUpdate conn.prepareStatement(updateSql); psUpdate.setInt(1, nextStatus); psUpdate.setString(2, comment); psUpdate.setInt(3, declId); psUpdate.setInt(4, expectedStatus); int rows psUpdate.executeUpdate(); if (rows 0) { conn.rollback(); return false; // 说明状态已被别人改过本次审核作废 } // 插入审核日志 String logSql INSERT INTO t_audit_log(decl_id, auditor_id, action, comment_text) VALUES(?,?,?,?); psLog conn.prepareStatement(logSql); psLog.setInt(1, declId); psLog.setInt(2, auditorId); psLog.setString(3, action); psLog.setString(4, comment); psLog.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(审核失败, e); } finally { JdbcUtil.close(psUpdate, psLog, conn); } }逻辑说明UPDATE ... WHERE decl_id? AND status?这行是关键它保证只有当前状态等于预期状态时才会更新executeUpdate()返回值是 0 说明已有其他人改过状态直接回滚并返回失败。conn.setAutoCommit(false)解决的是主表状态更新和日志插入的一致性问题——如果只更新状态不插日志一旦出问题无法追溯如果先插日志后更新状态、更新又失败日志就成了谎话。commit 之前发生任何异常都 rollback两条 SQL 要么都成功要么都失败这就是事务边界的意义。JdbcUtil.close的核心是把 Connection、Statement、ResultSet 按逆序全部关闭具体写法在第五章避坑里再展开。4.3 统计报表把 SQL 聚合结果交给 JSTL 渲染统计报表的价值在于让领导一眼看到今年全院发了几篇论文、几项专利总工作量多少。报表数据尽量在 SQL 层算好不要在 JSP 里写循环累加那样页面卡顿且代码难看。给一个按年度和类别统计的查询SELECT d.category AS 成果类别, COUNT(*) AS 申报数量, SUM(CASE WHEN d.status 3 THEN 1 ELSE 0 END) AS 通过数量, SUM(CASE WHEN d.status 3 THEN a.score ELSE 0 END) AS 总工作量 FROM t_declaration d LEFT JOIN t_achievement a ON d.decl_id a.decl_id WHERE YEAR(d.submit_time) ? GROUP BY d.category;统计 Servlet 里用 PreparedStatement 传入年份参数把查询结果封装成 ListMapString, Object存进 request 后转发到 report.jsp。JSP 页面的渲染代码很短核心就是 JSTL 的 forEachtable border1 tr th成果类别/th th申报数量/th th通过数量/th th总工作量/th /tr c:forEach items${reportList} varrow tr td${row[成果类别]}/td td${row[申报数量]}/td td${row[通过数量]}/td td${row[总工作量]}/td /tr /c:forEach /table这里用 LEFT JOIN 而不是 INNER JOIN 是有讲究的有的申报单状态是已提交还没走到成果明细录入如果用 INNER JOIN这些申报单会直接从统计表里消失数量就对不上。WHERE YEAR(submit_time) ?让统计维度跟着提交年度走不是跟着创建时间走这个细节和第二章的状态设计是呼应的。5. 常见问题与避坑JSP 项目从开发到部署的 5 个典型坑这一章写的是从开发到上线的过程中我实际踩过的坑每一条都是现象 → 原因 → 解决的完整链路直接照着排查能省一半时间。5.1 中文乱码POST 与 GET 的两套解法现象表单里输入张三数据库存进去变成寮犱笁页面上显示??? 这几乎是 JSP 入门必遇的鬼故事。原因HTTP 请求携带的参数是字节流Tomcat 用什么字符集解码决定了你能不能还原出中文。POST 请求和 GET 请求的解码路径不同所以解法分两套。POST 请求要靠业务代码在读取参数前设置字符集GET 请求的参数在 URL 里由 Tomcat 的 connector 统一解码。解决POST 请求在 Servlet 的 doPost 第一行写上request.setCharacterEncoding(UTF-8)并且保证这行代码在第一次调用getParameter()之前执行否则已经解析过的参数不会再重新解码。GET 请求在 Tomcat 8.5 里默认就是 UTF-8基本不用管如果你用的 Tomcat 7 或更早打开 conf/server.xml在Connector标签里加上URIEncodingUTF-8。响应端别忘了response.setContentType(text/html;charsetUTF-8)否则浏览器不知道页面是什么编码。三处都设对中文才能全程无恙。5.2 文件上传时 request.getParameter() 返回 null现象表单加了enctypemultipart/form-data后原来的request.getParameter(title)突然全是 null代码没动过玄学问题。原因不是玄学。multipart 是一种复合请求体把普通字段和文件二进制混在一个流里Tomcat 默认不会解析这种格式所以 request 的 parameterMap 为空。这是 Servlet 规范的既定行为不是 bug。解决按 4.1 节的代码走ServletFileUpload.parseRequest()从 FileItem 列表里取字段。注意item.getString(UTF-8)是必要的否则取出来的中文标题照样乱码。另外不要自己去拼接解析 multipart 边界字符串那是个大坑边界值boundary是随机生成的手写解析器会把文件内容截错直接用 commons-fileupload 这套成熟方案。5.3 数据库连接用完不关导致的连接耗尽现象系统跑一两个小时日志里出现Cannot get connection: wait millis 60000, active 20, maxActive 20数据库连接数被占满系统假死。原因代码里获取了 Connection 但没在 finally 里关闭。有的同学只关了 ConnectionPreparedStatement 和 ResultSet 没关同样造成连接泄漏。Druid 连接池的 maxActive 是 20连接被借走不还池子就空了。解决写一个统一的 JDBC 工具类关闭操作按逆序执行并在 finally 里调用。public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明ResultSet 依赖 StatementStatement 依赖 Connection关闭顺序必须从内到外否则先关 Connection 再关 Statement内部的游标可能未被释放。另外 Druid 的初始化参数里setInitialSize(5)、setMinIdle(2)、setMaxActive(20)这三个值要配套maxActive 不要一下子设成 200内网管理系统 20 足够设太大反而拖垮数据库。5.4 IDEA 里跑得好好的部署到 Tomcat 就 404现象IDEA 里配置了本地 Tomcat点运行按钮打开浏览器能正常访问首页把项目打成 WAR 包丢到 Tomcat 的 webapps 目录访问却 404。原因两种运行方式的环境不一样。IDEA 运行时用的是它自己创建的 Artifact部署到了 Tomcat 的conf/Catalina/localhost下一个临时路径上下文名是 IDEA 给你起的直接丢 WAR 到 webappsTomcat 解压后生成的目录名是 WAR 文件名如果你访问的路径是/scientific-research-decl/而 WAR 名叫demo.war自然匹配不上。另一个常见原因是 WAR 包结构不对缺 WEB-INF/classes 下的编译产物或者缺 WEB-INF/lib 下的依赖 jar。解决第一步确认访问路径和 WAR 文件名一致最简单方法是在 webapps 下看解压出来的目录名。第二步解压 WAR 看一眼结构jar tf scientific-research-decl.war输出里必须有 WEB-INF/classes 下的 .class 文件以及 WEB-INF/lib 下的 mysql、druid、jstl 等 jar 包。如果 classes 为空说明 Maven 编译输出目录没配好如果 lib 为空说明依赖 scope 或打包插件配置错了。排查完这两点90% 的 404 都能解决剩下 10% 去看 Tomcat 的日志文件logs/catalina.out或logs/localhost.2025-xx-xx.log那里有真正的报错堆栈。5.5 传统 JSP 项目打包 WAR构建配置与 nginx 转发 Tomcat 的注意点现象执行mvn clean package成功后部署到 Tomcat 报ClassNotFoundException: com.mysql.jdbc.Driver。原因Maven 默认的 war 打包不会把 provided scope 的依赖打进去这本身是对的问题往往出在你把 mysql-connector-java、druid、jstl 这些运行时依赖写成了 provided或者根本没引入。传统 JSP 项目打包 war 时只有 javax.servlet-api 需要 provided其他依赖都必须以 compile 或 runtime scope 进入 WEB-INF/lib。解决回到 pom.xml 确认依赖 scope。servlet-api 保持 providedmysql-connector-java、jstl、commons-fileupload、druid 全部不要写 scope默认 compile 即可。然后重新打包检查 WEB-INF/lib。部署上线后还有一个高频问题项目跑在 8080 端口但领导只想通过 80 端口访问这时候就会想到nginx 支持 jsp 吗。答案是 nginx 本身不解析 JSP它只处理静态文件和转发请求必须把动态请求转到 Tomcat 去执行。最小配置如下server { listen 80; server_name decl.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里的proxy_pass就是把请求原样转交给监听 8080 的 TomcatJSP 文件最终是由 Tomcat 的 JSP 引擎编译执行的。对内网管理系统整个/全转发就够了如果以后要拆静态资源再单独配置/static/路径的 alias 指向本地目录。注意修改 nginx 配置后要执行nginx -s reload而不是 restartreload 是无损重载。6. 上线前的验证状态迁移自检、接口测试与部署后排查系统写完不叫完事状态机跑得通、数据能对上才算。我习惯在上线前做一轮最朴素的验证不用自动化测试框架就用 curl 和 SQL十分钟能把核心链路全过一遍。6.1 用 curl 走一遍核心接口流程先启动 Tomcat然后用 curl 带 cookie jar 模拟登录会话# 第一次访问首页预期收到 200 和登录页 HTML curl -i http://localhost:8080/scientific-research-decl/ # 登录保存 cookie 到文件预期收到 302 跳转 curl -i -c cookies.txt -d usernameteacher1password123456 \ http://localhost:8080/scientific-research-decl/login # 带着 cookie 访问教师申报列表页预期 200 curl -b cookies.txt http://localhost:8080/scientific-research-decl/decl/declList.jsp-c cookies.txt是把服务端返回的 Set-Cookie 存下来-b cookies.txt是让后续请求带上这个 cookie。看到 302 说明登录跳转正常看到 200 说明会话认证生效。如果返回 401 或 403,先检查 Filter 里的 session 取值和角色判断逻辑。6.2 数据库侧的状态迁移自检与日志排查接口通了只能说明请求链路正常状态迁移对不对要看数据库。用两条 SQL 就能完成自检-- 查某份申报单的完整状态轨迹 SELECT decl_id, status, submit_time FROM t_declaration WHERE decl_id 1; -- 查这张单子的所有审核记录 SELECT action, comment_text, audit_time FROM t_audit_log WHERE decl_id 1 ORDER BY audit_time;正确的结果是申报单状态从 0 变 1、再变 2、再变 3同时 t_audit_log 里有三条对应记录——submit、approve、approve。如果状态是 3 但日志只有两条说明事务没包住赶紧回 4.2 节检查 setAutoCommit 和 try-catch-finally。我个人的习惯是每次改完状态机代码都手动把草稿 → 提交 → 初审通过 → 终审通过 → 驳回 → 重新提交完整跑一遍每一步都查数据库因为状态字段改起来太容易了日志漏了才难看。这套流程在答辩演示时也很有说服力——直接给评委看数据变化比空口讲设计好得多。希望今天的这些细节能让你少走几段弯路祝你的系统一次跑通。本文还有配套的精品资源点击获取

相关新闻

嵌入式驱动从能跑到量产:工程化思维与实战避坑指南

嵌入式驱动从能跑到量产:工程化思维与实战避坑指南

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

2026/9/30 2:46:00 阅读更多 →
C语言只有值传递:指针传参本质是地址值拷贝

C语言只有值传递:指针传参本质是地址值拷贝

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

2026/9/30 2:46:00 阅读更多 →
不同机型保修期不一样,售后系统怎么配置才不会人工判断出错?

不同机型保修期不一样,售后系统怎么配置才不会人工判断出错?

不同机型保修期不一样,售后系统怎么配置才不会人工判断出错? 关键词: 售后系统|不同机型保修期|质保规则配置|SN码|电子保修卡|保内保外判断 太长不看版 • 不同机型保修期不一样时&…

2026/9/30 2:44:59 阅读更多 →

最新新闻

RTX 4060 Laptop GPU模型优化实战:量化、剪枝与蒸馏系统工程

RTX 4060 Laptop GPU模型优化实战:量化、剪枝与蒸馏系统工程

1. “Model-Optimizer”不是软件名,而是工程能力的代号很多人第一次看到“Model-Optimizer”这个词,会下意识去GitHub搜仓库、去PyPI查包、甚至在NVIDIA官网翻文档——结果一无所获。我当年也这么干过,花了整整两天,最后发现&…

2026/9/30 3:42:34 阅读更多 →
分布式数据库代理:分库分表后的路由、连接池与读写分离实战

分布式数据库代理:分库分表后的路由、连接池与读写分离实战

接手过一个日订单量百万级的数据平台之后,你多半会被一个问题反复折磨:单库的连接数已经打满,读写全挤在同一个实例上,事务越来越慢,扩容一次要停机半小时。这时候大多数人会想到分库分表,可真把表拆完&…

2026/9/30 3:42:34 阅读更多 →
医学图像分割必学:nnU-Net自动配置、预处理与训练策略全解析

医学图像分割必学:nnU-Net自动配置、预处理与训练策略全解析

做医学图像分割这几年,我见过太多人一上来就追各种花哨的新网络,折腾半个月,最后效果还不如老老实实跑一个经典模型。如果你也在这个领域,我想先给你一个建议:别的可以先放放,nnU-Net 一定要先学会。它不是…

2026/9/30 3:42:34 阅读更多 →
Python商品数据分析与随机森林销量预测:Django可视化实战

Python商品数据分析与随机森林销量预测:Django可视化实战

说实话,刚看到"Python商品数据分析 随机森林销量预测 Django可视化"这个组合,我就觉得它在毕业设计选题库里属于"标准答案"级别。技术链路完整、演示效果好、答辩时能讲的东西多,而且整套技术栈都非常主流,…

2026/9/30 3:42:34 阅读更多 →
用 AkShare + backtrader 做 量化策略:ETF 双均线回测(含滑点/印花税完整代码)

用 AkShare + backtrader 做 量化策略:ETF 双均线回测(含滑点/印花税完整代码)

## 前言很多刚接触量化的朋友第一个策略就是"双均线"——短期均线上穿长期均线买入,下穿卖出。 但大部分人跑出来的回测结果都是"假的":没算手续费、没考虑 T1、没处理涨跌停。本文用 AkShare 获取数据 backtrader 做回测&#xff…

2026/9/30 3:42:34 阅读更多 →
Kubernetes 核心概念实战:Namespace 与 Pod 的运维避坑指南

Kubernetes 核心概念实战:Namespace 与 Pod 的运维避坑指南

这是一篇关于 Kubernetes 核心概念中 Namespace 与 Pod 的实战经验分享,从基础原理到生产落地,包含了我在实际集群运维中的踩坑记录。无论你是刚接触 K8s 的开发者,还是正在准备面试的运维工程师,这篇文章都能帮你把这两个基础但关…

2026/9/30 3:41:34 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 8:16:59 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/29 16:41:41 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/29 8:24:48 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/29 3:55:56 阅读更多 →