先说个可能不太讨喜的判断:像“jsp大连东软人才培训中心oa系统tc617”这类带机构名和编号的实训项目,在很多人眼里是“培训班项目”“没什么技术含量”,但如果你是真的想走Java Web这条路,它反而是难得能把JSP、Servlet、JDBC、数据库设计、权限模型、流程状态这些知识点完整串起来的样本。
这类项目一般由培训机构或学校实训环节给出,目的是让人独立完成一个能跑、能演示、能扩展的中小型管理系统。它的文件名虽然写得很“交付物风格”,但其实角色非常清晰:程序给的是工程源码,数据库给的是初始化脚本,调试部署用来验证整套环境是否可用。我接手过不少类似的OA、CRM、学生管理系统,最大的体会是:不要被“OA系统”四个字吓到,也不要因为它看起来土就跳过。真正值得研究的是,从一张登录页到一张业务流程表,中间到底发生了什么。
如果你手里也有一份类似的JSP OA项目源码,或者正打算把一个OA实训项目从零跑起来,这篇文章按我实际的排查和开发顺序来写,可以当成一份操作笔记来用。
1. 这类OA系统的业务全貌:拆开标题,先搞清楚对方做了什么
1.1 培训机构里常见的OA系统,业务范围到底有多广
“OA”这个词对新手特别不友好。一提OA,很多人第一反应是钉钉、飞书、泛微那一类复杂产品,于是觉得项目里必须要有工作流引擎、消息中间件、多租户组织架构。但打开绝大多数实训项目源码后你会发现,它做的其实就是一个企业内部的轻量办公平台。
常见的功能模块是这样分布的:
- 系统管理:登录、注销、管理员密码修改、用户状态启用禁用。
- 组织架构:部门信息维护、员工账号维护,一些项目会做简单的部门树。
- 公告通知:管理端发公告,普通员工在首页或公告栏看到最新通知。
- 日程与个人办公:记录日程安排、待办事项,有些项目会和个人待办列表合并。
- 审批流程:最核心的部分,通常围绕请假、出差、加班、报销申请展开。你会填一张单子,选择类型,填写事由和时间,提交给上级审批,上级看到后点通过或驳回。
这里的“培训中心OA”如果把业务映射到实际操作中,大概率也是“基础管理 + 简单审批”的组合。看到标题里的tc617不用纠结,它更像项目编号或在线课堂的任务号,不代表系统里有617个功能,也不代表第617个版本。它的存在是对项目做归档标识,方便教学环节分配任务。
1.2 从页面反推业务线,比直接看代码高效得多
我拿到陌生项目后第一步不是打开IDE,而是先用归档工具看看里面有哪些目录和文件,尝试判断它的业务边界。你可以按“三类页面”来划分:
前端页面往往分成三个层面:登录页、主框架页、业务功能页。登录页通常一眼就能认出来;主框架页多为左右结构,左边是菜单,右边是iframe嵌入的功能页面;业务功能页则按照“列表页、表单页、详情页”的套路重复出现。
看功能和数据库关系时,可以用一张表快速对应:
| 功能页面 | 核心动作 | 对应数据主体 |
|---|---|---|
| 登录页 | 身份认证 | 用户表 |
| 员工管理 | 增删改查、重置密码 | 员工账号表、部门表 |
| 部门管理 | 维护组织层级 | 部门表 |
| 公告管理 | 发布/查看公告 | 公告表 |
| 请假申请 | 填写、提交 | 审批申请单、审批记录 |
| 待办审批 | 通过/驳回 | 审批状态字段更新 |
看清楚这些,再去翻源码目录结构,你会发现很多项目都按“实体类、DAO类、Servlet、JSP页面、工具类”分包。这不是巧合,而是老牌Java Web项目最通用的组织方式,也是后来学SSM、Spring Boot时分层思想的雏形。
1.3 这类项目真正的难点,在“状态”而不在“增删改查”
大多数学习者看OA系统时很容易抱着一个想法:这不就是几个CRUD吗?数据库里加一条记录、页面上查一个列表,有什么难的?
实际上,CRUD只是地基。OA系统和“学生信息管理”这类纯数据维护系统最大的差别在于业务状态流转。
以请假申请为例,一条数据的生命周期大概是这样:
- 员工填写申请,此时状态可能是“草稿”或“未提交”。
- 点击提交按钮后,状态变为“待审批”。
- 审批人打开列表,看到属于自己的待办事项。
- 点击同意,状态变成“已通过”;点击驳回,状态变成“已驳回”,并需要填写驳回理由。
- 员工重新编辑被驳回的单据,再次提交,状态又回到“待审批”。
这就是一个最简单的状态机。如果项目里还区分部门经理审批、总经理审批,那么状态还得再加几个层级。后面改代码时,最常见的逻辑问题就是:状态字段更新错了,或者提交人误把待审批的单子再次提交,又或者不同角色看到了不该看到的数据。
所以,拿到OA系统源码后,你最应该先找的不是“登录代码”,而是“审批状态更新那段逻辑”。看懂一个状态字段如何从0变成1,再从1变成2或3,比看懂十个列表查询都更有收获。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与源码阅读顺序,为什么纯JSP反而适合打底子
2.1 为什么要用JSP、Servlet而不是Spring生态
短视频平台上经常有人吐槽:“2024年了还在教JSP?”这话有失偏颇。培训机构或实训单位选JSP,不是因为技术老旧,而是因为它最容易把Web开发的基础流程讲清楚。
在一个纯JSP项目中,没有Spring管理Bean,没有MyBatis自动映射,没有Spring MVC的注解驱动,所有行为都很直接:
- 用户浏览器发出HTTP请求到Tomcat。
- Tomcat根据URL映射规则把请求交给对应的Servlet。
- Servlet里手动解析参数、手动调用业务逻辑或DAO。
- DAO通过JDBC连接数据库,执行SQL,拿到结果集。
- Servlet把数据塞进request或session,通过转发或重定向跳转到JSP。
- JSP负责把数据输出为HTML,交回浏览器显示。
这条链路里,每一步都没有“魔法”。你可以在源码里直接看到自己写的Connection从哪里创建,PreparedStatement如何执行,ResultSet怎么被遍历。等你以后再去学Spring Boot,会发现大部分思想跟这条路完全一致,只是框架帮你省掉了重复的样板代码。
所以我的建议是:不要觉得JSP项目过时就草草跑起来截图交差。相反,纯JSP源码恰好能让你补上“底层Web运行逻辑”这块最容易被框架屏蔽的知识。
2.2 登录请求的完整路径:从点击按钮到进入主页面
为了把阅读顺序讲明白,我用一个最典型的功能来走查:登录。
假设页面表单里有用户名输入框和密码输入框,form的action可能是loginServlet或userServlet?action=login。你顺着请求,会看到Servlet里的处理逻辑一般是这样的:
java复制@WebServlet("/loginServlet")
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.findUserByUsernameAndPassword(username, password);
if (user != null) {
HttpSession session = request.getSession();
session.setAttribute("loginUser", user);
response.sendRedirect("main.jsp");
} else {
request.setAttribute("errorMsg", "用户名或密码错误");
request.getRequestDispatcher("login.jsp").forward(request, response);
}
}
}
这一段代码几乎能回答所有“Servlet如何工作”的问题:参数在request里,业务逻辑在DAO中,登录成功后的用户身份放进session。你可以清晰地看到,用户数据在登录成功后会被保存到会话对象中,后续页面判断是否登录,通常就是检查这个session。
再往下走,你会看到DAO中大致是这样连接数据库的:
java复制Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/oa_db?characterEncoding=utf8";
Connection conn = DriverManager.getConnection(url, "root", "123456");
PreparedStatement ps = conn.prepareStatement("select * from t_user where username=? and password=?");
ps.setString(1, username);
ps.setString(2, MD5Util.md5(password));
ResultSet rs = ps.executeQuery();
请注意,查询用户时用的是?占位符而不是字符串拼接。这是个很重要的分界线。很多老OA项目为了省事,习惯写"select * from t_user where username='" + username + "'",这种写法简单但隐患极大。看到这种代码,你可以顺手在二次开发时改造成PreparedStatement。
2.3 JSP页面上的Java片段:热词背后是老项目的通病
你在一些技术热词里能看到“如果在jsp上写java代码的风险”“jsp脚本片段”这类搜索词,说明很多人在实操时都遇到过同样困惑:看起来JSP页面里可以直接写Java代码,为什么前辈又说尽量别写?实训项目HTML页面上那些<%标签不就是吗?
先解释一下JSP运行机制。JSP文件第一次被访问时,Tomcat会把它翻译成一个Java文件,再编译成Class文件。你写在<% %>里的Java片段会原封不动进入生成的Servlet类中。也就是说,JSP本质是一个“能嵌入Java代码的模板页面”,而不是纯粹的HTML。
在实训项目里,你经常能在页面顶部看到这样的写法:
jsp复制<%
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) {
response.sendRedirect("login.jsp");
return;
}
%>
这段代码用于登录校验,看起来也没毛病。但问题在于,一旦项目变大,所有人都在JSP里到处写<% %>,页面会变得极难维护。更麻烦的是,如果页面直接用out.println()输出某个变量的值,而这个值来自用户输入,且没有做HTML转义,就可能出现安全隐患。
我建议你处理陌生JSP项目时,把下面的原则记下来:
- 页面显示数据优先用EL表达式
${...}和JSTL标签<c:forEach>。 - 真正必须用Java片段做逻辑判断的地方,尽量收敛到少量公共JSP或Servlet里。
- 服务器收到的用户输入,经过处理后再输出到页面,一定要做转义,通常用
<c:out value="${content}" />或工具类处理。
另外要注意:即便JSP里只有一行<%@ include file="header.jsp" %>,也要先搞清楚这个公共文件会不会在其他页面里造成重复的变量声明。实训项目的公共页面经常把连接数据库的代码也写进去,导致每个页面都开启一个数据库连接,这会拖垮整体性能,也是代码坏味道的重灾区。
3. 这类OA的表结构和初始化数据为什么容易卡壳
3.1 最小可用表集合:没有哪张表是多余的
虽然不同的OA系统表名不一样,有的叫t_employee,有的叫sys_user,有的叫leave_info,有的叫oa_apply,但核心的表一定跑不出这些:
| 表业务含义 | 建议字段 | 说明 |
|---|---|---|
| 部门表 | 部门编号、部门名称、上级部门、负责人、联系电话 | 体现组织层级 |
| 员工/账号表 | 账号、密码、姓名、部门、职位、角色、状态、手机号 | 通常与登录用户共用一张表 |
| 公告表 | 标题、内容、发布人、发布时间 | 员工端只读 |
| 审批表 | 申请人、类型、开始时间、结束时间、事由、当前状态、当前审批人 | 状态字段是关键 |
| 操作日志表 | 操作人、操作类型、操作时间、操作描述 | 非必需,但加了更完整 |
| 日程/待办表 | 所属人、标题、内容、提醒时间、状态 | 部分项目有 |
我看到过很多把登录账号和员工详细资料分成两张表的实训项目,比如一张t_user专门存账号密码,另一张t_employee存员工姓名部门。这样做虽然在概念上更规范,但给新手带来了大量关联查询。如果是同一个系统里的注册用户就是本公司员工,那么合并成一张表反而是更务实的选择。二次开发时,你在新增“部门”下拉框时,一个JOIN就能出来部门名称,而不需要三层嵌套查询。
3.2 审批状态字段:用一张表理解整条业务线
很多项目的核心表结构类似于:
sql复制CREATE TABLE t_leave (
id INT PRIMARY KEY AUTO_INCREMENT,
applicant_id INT COMMENT '申请人id',
leave_type VARCHAR(20) COMMENT '事假/病假/年假等',
start_date DATE,
end_date DATE,
days DOUBLE,
reason VARCHAR(500),
status INT DEFAULT 0 COMMENT '0草稿 1待审批 2通过 3驳回',
approver_id INT COMMENT '当前审批人',
audit_time DATETIME,
audit_comment VARCHAR(500)
);
这里的status是整个审批模块的引擎。源码里的所有查询,本质上都在围绕这个字段转:
- 普通员工进入“我的申请”,查的是
applicant_id = 当前用户id。 - 审批人进入“待办审批”,查的是
approver_id = 当前用户id AND status = 1。 - 管理员进入“历史审批”,查的是所有
status != 1的数据。
审批通过时执行的更新语句也很直白:
sql复制UPDATE t_leave
SET status = 2, approver_id = ?, audit_time = NOW(), audit_comment = ?
WHERE id = ? AND status = 1;
注意这个WHERE里除了主键,还带了status = 1。这是一个容易被忽略的细节:如果用户在前端重复点了两次“通过”按钮,第二次提交时状态已经不是1了,数据库更新会返回影响行数为0,业务逻辑就可以据此避免重复审批。这种“乐观锁”的思路,不用引入任何复杂框架,在一张表里就能体会到。
3.3 初始化数据的老问题:唯一索引和已存在的冲突数据
标题的热词里有一句很绕的话,叫“mysql设置唯一已经有重复数据库”。我猜很多人是这么踩坑的:项目原先的表里已经存在重复数据,比如员工表里已经有两条手机号相同的记录,此时想给手机号字段加唯一索引,数据库直接报错,字段设不上去。
其实这个问题的本质是:唯一索引要求表内现有数据本身就满足唯一性,不可能在脏数据存在的情况下一键加约束。遇到这种情况,你在调试老系统时不要硬来,正确顺序应该是:
- 先查出重复记录:
sql复制SELECT phone, COUNT(*) FROM t_user GROUP BY phone HAVING COUNT(*) > 1;
- 确认哪些记录是废弃数据,需要合并或删除。
- 清理完重复内容后,再执行:
sql复制ALTER TABLE t_user ADD UNIQUE KEY uk_phone (phone);
如果你是重新导入实训项目里的SQL脚本,而不是在一套旧库上做增量开发,那更简单:直接删除旧库重建。初始化脚本里的表结构定义一般不会有冲突,真正让你头疼的是库里已经有旧表、旧数据,导致CREATE TABLE失败,或主键自增ID对不上。
导入数据库时,我建议用下面的固定套路:
bash复制mysql -uroot -p
CREATE DATABASE IF NOT EXISTS oa_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE oa_db;
SOURCE D:/projects/oa/sql/oa.sql;
导入后再执行一条确认语句:
sql复制SHOW TABLES;
如果你看到几十张业务表,并且里面能查到admin账号,说明脚本导入成功。别急着登录,先去表里看一下密码字段是什么形式。如果密码是32位十六进制字符串,那它肯定是MD5加密后的结果;如果是明文,说明系统没有做密码加密,二次开发时至少要补一个MD5工具类。
4. 一个干净机器的标准部署顺序和典型报错清单
4.1 版本配对是第一步,也是最容易出错的地方
我曾经接过一个项目,开发者环境是JDK 7 + Tomcat 7 + MySQL 5.5,到我手里变成JDK 17 + Tomcat 10 + MySQL 8,结果编译报错一堆,核心原因是Tomcat 10把javax.servlet升级成了jakarta.servlet,老代码里所有import javax.servlet全部无法识别。这个坑非常典型。
如果你要调试老JSP项目,请尽量把环境控制在下面这个组合,不要盲目追求新版本:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 对老代码兼容性最好 |
| Tomcat | 8.5.x | 支持Servlet 3.1,老代码无需改动 |
| MySQL | 5.7 或 8.0 | 注意驱动版本,8.0需用cj驱动 |
| IDE | Eclipse或IDEA | 按自身习惯选 |
如果你手头只有Tomcat 10或JDK 17,也不一定跑不起来,但要做好心理准备:你可能需要把源码里的javax.servlet批量替换成jakarta.servlet,并且升级依赖的Servlet API jar包。这会耗费额外精力。所以,我建议先在一台装了JDK 8和Tomcat 8.5的机器上把项目跑起来。这不是偷懒,而是用最确定的组合去验证项目本身有没有问题,否则项目问题和环境问题混在一起,排查起来极度痛苦。
4.2 数据库连接配置修改:别只盯着文件名看
源码里找数据库配置,不要只找db.properties或jdbc.properties,许多老项目把连接信息直接写死在Java类的静态代码块里。你可以用IDE的全局搜索功能,搜下面几个关键词:
jdbc:mysqlClass.forNameDriverManager.getConnectiondriverNameusernamepassword
有时你会在DAO公共父类里看到这样的代码:
java复制static {
try {
Class.forName("com.mysql.jdbc.Driver");
URL = "jdbc:mysql://localhost:3306/oa_db?useUnicode=true&characterEncoding=utf8";
USER = "root";
PASSWORD = "123456";
} catch (Exception e) {
e.printStackTrace();
}
}
如果是MySQL 8或新版驱动,com.mysql.jdbc.Driver这个类虽然还会有过时提示,但保险起见建议改成com.mysql.cj.jdbc.Driver,连接串后面最好加上时区参数:
java复制jdbc:mysql://localhost:3306/oa_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
不然MySQL 8下容易出现关于serverTimezone的报错。配置数据库连接最关键的一步是确保用户名、密码和实际数据库一致,账号对应的权限至少要有查询、插入、更新、删除。如果root密码记不清,也不要上来就改项目源码去猜,先用命令行工具确认能连上数据库。
4.3 源码导入与部署:直接运行还是先导war包
老式JSP项目的目录结构通常有两种:一种是Eclipse标准的Web项目,目录里有WebRoot或WebContent;另一种是IDEA普通Java项目,但业务代码和JSP都放在某个文件夹下。
不管哪一种,你都要确保最终部署到Tomcat的是一个“Web应用目录”,这个目录下要有WEB-INF文件夹,并且其中必须有web.xml(如果项目没有显式web.xml,也要有Servlet 3.0规范的元数据)。如果某个文件夹没有WEB-INF,那它大概率不是部署根目录。
用IDEA时,我建议配置Tomcat后把Artifact选择为war exploded模式,它会直接把源码目录同步到Tomcat。改完JSP后刷新页面就能看到效果,更适合调试。而如果项目是给你一个war包,那更方便,扔到Tomcat的webapps目录下,启动Tomcat会自动解压。启动成功后,访问URL一般是:
text复制http://localhost:8080/项目名/login.jsp
项目名最好用英文,不要使用中文名,避免Tomcat路径解码时出问题。另外,如果你的项目上下文名本身比较长,访问时也容易写错。稳妥做法是打开Tomcat的manager页面或直接看解压目录名来确定。
4.4 启动后的典型报错与排查顺序
把项目部署完,打开浏览器,你会遇到四类典型问题,按出现频率从高到低分别是:404、500、乱码、无法连接数据库。
404的意思是你访问的资源不存在。先看URL路径对不对,再检查构建产物里是否真的生成了对应的JSP或Servlet。如果你写的是:8080/login.jsp但工程名是oa,实际应该访问:8080/oa/login.jsp。
500错误一般是服务端抛异常。第一时间要看Tomcat控制台或日志,高频原因是没找到MySQL驱动,也就是ClassNotFoundException: com.mysql.jdbc.Driver。解决办法是把mysql-connector-java的jar包放到WEB-INF/lib目录下,并重启Tomcat。很多实训项目源码根目录里自带lib文件夹,但导入Eclipse时没有自动加入构建路径,也会导致这种错误。
乱码问题涉及请求和响应两端。最常见页面上已经设置了pageEncoding="UTF-8",但Servlet里用来接收参数时没有写request.setCharacterEncoding("UTF-8"),于是中文参数变乱码。而数据库里中文变问号,则要检查建库时的字符集是否写成utf8mb4,连接串里有没有characterEncoding=utf8。
页面白屏且没有任何输出,通常是Servlet转发到了错误的JSP路径,或者JSP本身在翻译阶段发生异常。你可以远程访问jsp直接看错误代码,但更好的做法是把Tomcat日志完整打印出来,顺着栈信息找第一个自己写的类。
5. 业务层二次开发的常见改造方案:审批流和附件是核心
5.1 从浏览器逆推到源码:接到需求后的正确找码方式
很多人拿到陌生OA项目后,想加一个功能或改一个字段,第一反应是用IDE的目录树一点点翻,效率很低。我的习惯是“从浏览器反推源码”。
比如需求是:把审批列表里的“通过”按钮改成“同意并转交下一人”。你不需要先看所有类,只需要:
- 浏览器打开该功能页面,按F12打开开发者工具。
- 找到那个按钮对应的HTML元素。
- 查看它所在的表单或JS事件,拿到提交的URL,类似
applyServlet?action=pass。 - 回到IDE里全文搜索
action=pass或applyServlet。 - 命中后,就会看到对应的Servlet、Service或DAO,再往下追,就能找到执行状态更新的SQL。
这套方法看似笨,但在没有文档的情况下,比顺着包名漫无目的地读要快得多。它本质上是在帮你快速建立“页面元素 -> 请求地址 -> Servlet入口 -> DAO方法 -> SQL语句”的映射关系。
5.2 审批流前端到底怎么搭:jQuery发请求是个实用套路
实训项目里很少用复杂的Vue全家桶,更多是JSP页面用jQuery完成交互。你控制台里可能要实现的“前端使用js+jquery设置审批流”,落到代码上其实很清晰。
比如一个待办列表,每一行都有两个按钮:通过、驳回。页面初始化时用JSTL循环输出列表:
jsp复制<c:forEach items="${pendingList}" var="apply">
<tr>
<td>${apply.applicantName}</td>
<td>${apply.leaveType}</td>
<td>${apply.startDate} 至 ${apply.endDate}</td>
<td>
<button class="passBtn" data-id="${apply.id}">通过</button>
<button class="rejectBtn" data-id="${apply.id}">驳回</button>
<input type="text" id="comment_${apply.id}" placeholder="审批意见" />
</td>
</tr>
</c:forEach>
然后在页面底部写jQuery:
javascript复制$(function () {
$(".passBtn").on("click", function () {
var id = $(this).data("id");
var comment = $("#comment_" + id).val();
if (!confirm("确定通过这条申请吗?")) return;
$.post("applyServlet?action=pass",
{ applyId: id, comment: comment },
function (result) {
if (result === "success") {
alert("审批完成");
location.reload();
} else {
alert(result);
}
});
});
$(".rejectBtn").on("click", function () {
var id = $(this).data("id");
var comment = $("#comment_" + id).val();
if (!comment) {
alert("驳回时必须填写意见");
return;
}
$.post("applyServlet?action=reject",
{ applyId: id, comment: comment },
function (result) {
if (result === "success") {
alert("已驳回");
location.reload();
} else {
alert(result);
}
});
});
});
对应的Servlet代码只需要在原来“通过”“驳回”两个分支里增加参数读取和状态更新。这样做的好处是页面不需要整页刷新,用户操作体验更接近现代应用。实际开发时还要注意给按钮加“处理中禁用”的状态,防止网络慢时用户重复点击,反复提交。这也是为什么后端更新语句里要做status=1的判断,双重保险。
5.3 谷歌浏览器里的“保存文件路径”:OA附件上传的隐藏坑
热词里还有一条搜索叫“jsp代码谷歌浏览器获取保存文件路径”。很多刚做OA附件功能的人会想:如果表单里有个文件上传控件,我想在用户选择完文件后,把文件的本地完整路径一起提交给后台,这样系统就知道文件在哪了。
但在谷歌浏览器里,这个思路从一开始就是错的。出于安全限制,浏览器不会向网页暴露用户本地文件的完整路径。你在input框里选择文件后,用JavaScript读取value,只会得到类似C:\fakepath\xxx.docx的结果。这是故意设计的安全行为,不是代码写错了。
真正合适的做法是:
- 用户通过
<input type="file">选择文件。 - 页面把文件上传到服务器的一个专门接口。
- 后端把文件保存到Tomcat某个上传目录或独立文件存储目录,并给文件重新命名,避免中文乱码和重名。
- 数据库里只存服务器端生成的相对URL或文件名,比如
/upload/202406/20240601_163000_001.docx。 - 页面展示附件时,用
<a href="${ctx}/upload/...">去下载,而不是去读本地路径。
如果老项目里已经有人尝试在JSP中保存本地路径,代码里可能会残留类似C:\Users\xxx\Desktop\test.xlsx这样的字符串。这种写法换个电脑或浏览器就失效。所以接手后要做的事情是:把文件上传逻辑改成“保存到服务器目录,数据库只存访问路径”,这也更贴近真实企业系统的做法。
5.4 给老系统加权限判断:公共页面的Filter写法
很多纯JSP项目只在每个页面的顶部判断session是否为空,但这样会有一个问题:如果你没登录,直接访问某个功能Servlet的URL,那Servlet本身并没有做session校验,数据可能被直接查出来。
最省事且通用的改造是增加一个Filter:
java复制@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
HttpSession session = req.getSession(false);
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
String path = uri.substring(contextPath.length());
// 放行登录页、登录请求和公共静态资源
if (path.equals("/login.jsp") || path.equals("/loginServlet")
|| path.startsWith("/css") || path.startsWith("/js") || path.startsWith("/images")) {
chain.doFilter(request, response);
return;
}
Object loginUser = session == null ? null : session.getAttribute("loginUser");
if (loginUser == null) {
resp.sendRedirect(contextPath + "/login.jsp");
return;
}
chain.doFilter(request, response);
}
}
如果你想把“普通员工不能访问管理员Servlet”的逻辑也做进去,可以再细化Filter里的判断:管理员操作路径以admin开头,而当前登录用户角色不是管理员时,直接返回403或跳转到无权限页面。这个属于锦上添花,但面试或答辩时聊到“权限控制”,你会明显比只说“session判断”的人有底气。
6. 按这套代码做交付物时的经验性总结
6.1 一个能独立复现的项目,才算真正交付
“程序+源码+数据库+调试部署+开发环境”这几个字样出现得很频繁,它其实就是交付物完整性的模板。你去看任何商业软件项目,除了线上运行代码,背后一定还有数据库脚本、部署文档、环境要求、FAQ。实训项目也一样,最好的验证方式是在一台空机器上从零复现:
- 安装JDK 8和Tomcat 8.5。
- 安装MySQL并导入SQL脚本。
- clone或解压源码。
- 修改数据库连接配置。
- 启动Tomcat,完整走一遍员工登录、发起申请、审批、查看公告的流程。
任何一个环节依赖了你机器上的私有配置,比如某个jar包只放在本地仓库、数据库账号密码硬写在代码里且只适用于你的库,那么这套东西拿给别人就会立刻卡住。所以每次调试完,我建议顺手把遇到的问题和解决方式写进文档。不要让“下一个接手者”再把时间浪费在同样的事情上。
6.2 用“OA系统”作为面试或继续学习跳板时的加分点
如果这篇博文是给准备面试
