简介:面向高职院校计算机专业毕业生的一份原创毕业设计论文,主题是基于JavaWeb的高职二级院系任务积分管理系统。论文在引言部分交代了教务管理数字化转型背景,提出以积分量化学生任务完成情况,进而实现客观公正的评价。随后围绕任务发布、提交、审核、积分计算与查询统计等核心业务流程,明确教师、学生、管理员三类用户角色,并依次完成需求分析、系统设计、技术选型、环境搭建、系统实现与测试优化。技术方案以JavaWeb为基础,选用Spring Boot简化开发、MyBatis处理数据访问、MySQL完成数据存储,同时在安全性与权限控制方面做了必要设计。文档共1个docx文件,压缩包仅28KB,目录涵盖引言、系统分析与设计、技术选型与环境搭建、系统实现、系统测试与优化及总结与展望,适合作为专科或本科毕业设计论文的整体框架参考。目前已有87人学习下载,尤其适合需要快速把握JavaWeb管理类系统研发全流程、并撰写毕业论文的应届毕业生。
1. 高职二级院系的任务积分管理:Excel 和微信群已经算不明白了
每到学期末,高职二级院系的教学秘书就要面对一堆散落在微信群接龙、Excel 填报、纸质签到表里的“临时任务记录”。竞赛指导算 6 分、校内讲座算 2 分、值班一次算 1 分,规则倒是不复杂,但几百条记录要按人、按类型、按月份汇总,再跟评优评先挂钩时,Excel 公式被复制错一位,整个部门的积分就对不上账。这个标题(基于 JavaWeb 的高职二级院系任务积分管理系统)说白了就是解决这件事:把任务发布、报名、完成确认、积分核算这几步从微信群和表格里搬到 Web 系统里,让每一条加分都有据可查。系统实际是小型内部工具,用户量不大但流程要严谨,适合二级院系的教务管理老师、辅导员,也适合需要完成 JavaWeb 课程设计的在校生照着做。
2. JavaWeb 技术栈选型:为什么不是 Spring Boot 全家桶也能落地
2.1 任务积分管理的业务特征:低频、小并发、强规则,恰好是 JavaWeb 的舒适区
先把业务看清楚:高职二级院系的日常任务积分,通常是学期初发布任务、教师或学生报名、完成后由教学秘书或教研室主任确认、期末汇总。一个系可能只有几十到两百个用户,每天任务量不超过几十条,峰值集中在开学初和期末收尾。这和电商秒杀、公共门户那种高并发场景完全不同,对吞吐量没有硬性要求,但对数据准确性极为敏感。
正因为这种“低频、小并发、强规则”的特征,传统 JavaWeb 方案反而比微服务、前后端分离更合适。它足够简单:浏览器请求打到 Servlet,Servlet 调 Service,Service 调 DAO,DAO 操作 MySQL,最后把数据渲染到 JSP 页面。整个链路短,排查问题容易。如果硬套 Spring Cloud 或 Vue + RESTful API,需要维护两套工程、处理跨域、写接口文档,对校内小团队和日常维护人员来说负担反而更重。
我在做这类系统时,通常会把标题里的“JavaWeb”理解为两大类:一是传统 Servlet + JSP + MySQL + Tomcat,二是 Spring Boot 包装过的 JavaWeb 工程。两者底层都是 JavaWeb 技术体系,业务设计完全一样,只是工程管理方式不同。如果是课程设计或毕业设计,传统 Servlet/JSP 更容易展示自己对请求和响应的理解;如果是想长期交给系部使用、后续有人维护,Spring Boot 更顺手。本章后面的设计对这两种实现方式都适用,差异主要在依赖管理和部署方式上。
2.2 技术选型清单:Servlet/JSP 为主,MyBatis 辅助
下面给出一份我在规划这类系统时常用的选型清单,也对应设计文档里“技术架构”章节应写的内容。
| 组件 | 建议选择 | 理由 |
|---|---|---|
| 表现层 | JSP + JSTL | 服务端渲染,配合 Servlet 控制跳转,权限检查代码集中在 Filter 中,不引入前端构建工具 |
| 控制层 | Servlet 3.0+ | 使用 @WebServlet 注解或 web.xml 映射,简单直观 |
| 业务层 | Service 类 + 接口 | 积分审核、汇总算法放在 Service 层,便于写单元测试 |
| 持久层 | JDBC 或 MyBatis | 任务积分表关系固定,MyBatis 能减少结果集映射代码;若不想引框架,用 JDBC 封装一个 DBUtil 也行 |
| 数据库 | MySQL 5.7 或 8.0 | 高职机房和服务器基本都支持,备份工具成熟 |
| 容器 | Tomcat 9 | 与 Servlet 规范兼容,IDEA/Eclipse 都自带集成 |
| 连接池 | HikariCP 或 Druid | 避免每次请求创建数据库连接,否则并发稍高就报连接超时 |
选择 Servlet/JSP 而不做前后端分离,还有一个现实原因:这类系统往往没有专职前端。JSP 可以配合 JSTL 直接在页面里写条件判断和循环,熟悉 Java 的人上手快,临时改一个显示字段不需要启动两个服务。至于 Spring Boot,很多同学会把它当成“JavaWeb 的替代品”,实际它是 JavaWeb 生态中的一个框架,既能写 REST 接口,也能用 Thymeleaf 做服务端模板。如果最终选择 Spring Boot,项目结构从 src/main/webapp 改成 src/main/resources/templates,但数据库表设计、业务状态机、积分核算逻辑仍然可以复用下面几章的方案。
2.3 设计文档里该画哪几张图:用例、ER、状态流转
一个完整的基于 JavaWeb 的高职二级院系任务积分管理系统设计,不能只写代码,设计文档至少要覆盖三张图。
第一张是用例图。参与者包括系统管理员、教学秘书、教师、学生(如果是学生积分)或教研室主任。核心用例有:登录认证、维护个人资料、发布任务、查看任务、报名任务、确认任务完成、审核积分、查询个人积分、按月/学期汇总导出。这张图的价值是划清角色边界,避免开发时把权限全写成管理员一个人操作。
第二张是 ER 图。实体包括用户、角色、任务类型、任务、任务报名/参与记录、积分流水。实体之间关系要明确:一个用户可创建多个任务,一个任务可被多个用户报名,一个用户报名一个任务得到一条参与记录,一条参与记录产生一到多条积分流水。如果不想画得很复杂,可以用 MySQL Workbench 直接反向导入表结构生成。
第三张是任务积分状态流转图。我一般按“已发布 → 报名中 → 进行中 → 待确认 → 已结束”设计任务状态。参与记录状态则分成“已报名 → 已通过 → 已完成 → 已计分”。只有进入“已计分”状态的记录才允许生成积分流水,且流水一旦生成不允许修改,只能由管理员做冲正(加一条负数记录)。这张状态流转图直接决定后面程序里的 if/switch 条件和 SQL 更新语句,比任何文字描述都管用。
3. 数据库与积分模型设计:把“任务积分”拆成可计算的关系表
3.1 先回答三个问题:谁、做了什么任务、得多少分
任务积分系统最核心的需求不是好看,而是积分可追溯。任何一条加分记录,都要能回答:谁(用户 ID)、做了什么任务(任务 ID)、得了多少分(积分流水)。围绕这三个问题,数据库表可以拆成用户、角色、任务类型、任务、任务报名、积分流水这几类。
为什么不能只做一张“积分表”?因为积分是任务执行结果的衍生数据。如果直接在任务表里加一个“获得积分”字段,那一个人参加多个任务、一个任务由多人协作的模型就没法表达。常见的反例是:任务表里存一个owner_id,完成任务后给这个用户加 2 分。一旦出现“带队教师 + 参与学生”的组合任务,就需要额外加并列字段或逗号分隔 ID,后续统计只能写字符串处理代码,完全失去 SQL 聚合能力。
正确做法是把“任务”和“记录”分离:任务表描述“有什么任务”,任务报名/参与记录表描述“谁参加了”,积分流水表描述“谁因此获得了多少积分”。这样教师和学生可以报名同一个任务,各自按角色获得不同积分,汇总时一条 SQL 就能算出来。
3.2 建表 SQL:核心表结构与唯一约束
下面这组 DDL 是我在类似项目里常用的结构,以 MySQL 为例。可根据实际院系和组织架构调整字段名,但几个核心约束建议保留。
CREATE TABLE `sys_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(30) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'BCrypt或SHA-256密文', `real_name` varchar(30) NOT NULL COMMENT '姓名', `dept_id` int(11) DEFAULT NULL COMMENT '所属院系/教研室', `role_type` varchar(20) NOT NULL COMMENT 'admin/secretary/teacher/student', `phone` varchar(20) DEFAULT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `task_type` ( `id` int(11) NOT NULL AUTO_INCREMENT, `type_name` varchar(50) NOT NULL COMMENT '任务类型:值班、竞赛指导、讲座等', `base_score` decimal(10,2) NOT NULL COMMENT '基础积分', `score_unit` varchar(20) DEFAULT '分/次', `description` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `task` ( `id` int(11) NOT NULL AUTO_INCREMENT, `task_type_id` int(11) NOT NULL, `title` varchar(100) NOT NULL, `content` text, `place` varchar(100) DEFAULT NULL, `task_date` date NOT NULL, `start_time` time DEFAULT NULL, `end_time` time DEFAULT NULL, `need_count` int(11) DEFAULT '1', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1发布 2报名中 3进行中 4待确认 5已结束', `publisher_id` int(11) NOT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_task_date` (`task_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `task_apply` ( `id` int(11) NOT NULL AUTO_INCREMENT, `task_id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `apply_role_type` varchar(20) NOT NULL COMMENT '报名时的身份,决定按哪个角色计分', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1已报名 2已通过 3已完成 4已计分', `apply_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `confirm_time` datetime DEFAULT NULL, `confirm_user_id` int(11) DEFAULT NULL, `remark` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_task_user` (`task_id`,`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `score_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `task_apply_id` int(11) NOT NULL, `task_type_id` int(11) NOT NULL, `change_type` varchar(20) NOT NULL COMMENT 'add/revoke', `score` decimal(10,2) NOT NULL, `reason` varchar(255) DEFAULT NULL, `operator_id` int(11) NOT NULL COMMENT '操作人,通常是审核员', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_apply` (`task_apply_id`,`change_type`), KEY `idx_user_time` (`user_id`,`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:sys_user.role_type区分管理员、教学秘书、教师、学生,不建单独的角色关联表,是因为一个二级院系用户角色基本固定,一张系统用户表足够;如果后续需要一人多角色,再拆出sys_role和sys_user_role。task_type.base_score是任务类型的基础积分,比如“竞赛指导”6 分,“校内讲座”2 分。task_apply里不直接存得分,只存参与状态和报名身份,加分统一由score_log记录。这样设计后,统计某个教师的学期积分就是SUM(score_log.score),统计某类任务总积分就是按task_type_id分组。
参数说明:score使用decimal(10,2)而不用float/double,避免浮点误差;uk_task_user唯一索引确保同一用户对同一任务不能重复报名,是防重复加分的数据库层保障;task_apply.status用 tinyint 而不是字符串,比较快,但需要在代码里定义常量映射。字符集统一utf8mb4,否则后面插入特殊符号时容易出现索引超限或乱码。uk_apply唯一索引表示一条参与记录只能有一条 add 类型的流水,重复执行审核 SQL 时会直接报错,不会产生两条加分。
3.3 积分计算公式:角色系数、任务类型积分、审核状态机
积分计算不复杂,但要统一规则。常见的公式是:个人积分 = 任务类型基础积分 × 角色系数 × 完成度系数。高职二级院系里,教师和学生参加同一场讲座,教师可能是组织者得 3 分,学生是听讲者得 1 分。这个差异用task_apply.apply_role_type与task_type表联动实现。
计算逻辑用 Java 描述如下:
public void confirmApply(int applyId, int operatorId) { // 1. 查报名记录,状态必须为“已完成” TaskApply apply = applyDao.findById(applyId); if (apply == null || apply.getStatus() != TaskApply.STATUS_COMPLETED) { throw new BusinessException("当前记录不允许审核计分"); } // 2. 获取任务类型基础积分 TaskType type = taskTypeDao.findById(apply.getTaskTypeId()); // 3. 根据报名身份计算系数 double ratio = roleRatio(apply.getApplyRoleType()); BigDecimal score = type.getBaseScore() .multiply(BigDecimal.valueOf(ratio)) .setScale(2, RoundingMode.HALF_UP); // 4. 写入积分流水 ScoreLog log = new ScoreLog(); log.setUserId(apply.getUserId()); log.setTaskApplyId(apply.getId()); log.setTaskTypeId(type.getId()); log.setChangeType("add"); log.setScore(score); log.setOperatorId(operatorId); log.setCreateTime(new Date()); scoreLogDao.insert(log); // 5. 更新报名状态为已计分 applyDao.updateStatus(applyId, TaskApply.STATUS_SCORED); }逻辑说明:第一步查报名记录后必须检查状态,防止同一个applyId被重复提交审核。第二步和第三步把基础积分和角色系数分离,后续调积分规则只改roleRatio方法或数据库配置,不用重编译代码。第四步插入积分流水,依赖数据库唯一索引防止重复。最后一步更新状态,形成闭环。
参数说明:roleRatio通常采用常量映射,比如teacher → 1.0、student → 0.5、secretary → 1.2;不建议把这些系数硬编码在 Servlet 里,而是放在一个ScoreRule类或task_type表新增字段维护。RoundingMode.HALF_UP是为了处理除以 3 之类除不尽的情况,保留两位小数必须指定舍入模式,否则 BigDecial 会抛异常。审核操作建议在同一个事务里完成“插入流水 + 更新状态”,否则两个操作中间出现异常,可能出现流水已写但状态未改,用户反复看到“待计分”。
4. 用 IDEA 跑通这个 JavaWeb 项目:从导入到部署的关键配置
4.1 Maven 依赖与 IDEA 中的 Tomcat 配置
拿到一个 JavaWeb 任务积分系统项目,第一步不是读代码,而是先把工程跑起来。用 IDEA 操作时,我通常建议用 Maven 管理依赖,而不是直接往 WEB-INF/lib 里扔 jar。这样版本冲突少,换机器拉依赖也方便。
pom.xml里的核心依赖如下:
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> </dependencies>逻辑说明:javax.servlet-api的 scope 是provided,表示编译时需要但 Tomcat 容器已经自带,避免把 servlet-api 打包进 war 导致冲突。JSTL 用于在 JSP 中写简化的循环和判断,配合c:forEach显示任务列表。MySQL 驱动和 HikariCP 配套使用,HikariCP 是连接池,比直接DriverManager.getConnection可靠得多。
参数说明:如果服务器 MySQL 是 5.7,可以把驱动换成5.1.49,否则mysql-connector-java 8.x连接 5.7 时需要在 JDBC URL 里加useSSL=false&allowPublicKeyRetrieval=true。IDEA 配置 Tomcat 时,在运行配置里选择 “Tomcat Server -> Local”,指定 Tomcat 安装目录,并在 Deployment 标签页里添加 “exploded war”。Application context 一般设为/ScoreSystem,这样访问地址就是http://localhost:8080/ScoreSystem/。如果直接用 Maven 打 war 包,IDEA 菜单右侧 Maven 面板执行package,再把 target 下的 war 扔进 Tomcat 的 webapps 目录也能运行。
4.2 从 Servlet 到 JSP:登录接口的最小实现
跑通项目最快要验证的就是登录。以下是登录 Servlet 的关键逻辑,使用传统 Servlet 方式。
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); if (username == null || password == null || username.trim().isEmpty()) { resp.sendRedirect(req.getContextPath() + "/login.jsp?error=1"); return; } User user = userService.login(username, password); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp?error=1"); return; } HttpSession session = req.getSession(); session.setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } }逻辑说明:@WebServlet("/login")是 Servlet 3.0 注解方式,省去web.xml里的<servlet>配置。req.setCharacterEncoding("UTF-8")必须在读取getParameter之前调用,否则 POST 提交的中文参数会乱码。登录成功后把 User 对象放进 Session,后续页面上显示姓名和角色都从session.getAttribute("loginUser")取。所有/login之外的受保护 Servlet 需要在 Filter 里检查 Session 是否存在。
参数说明:req.getContextPath()是应用上下文路径,在部署到非根路径时必须带上,否则页面里写死href="/index.jsp"在 Application context 不是根时全部 404。密码存储不要用明文,至少用SHA-256加盐或BCrypt做哈希再入库;这样userService.login里比对的是摘要而不是明文。
4.3 积分审核的事务边界:避免半成功状态
登录跑通后,重点在审核积分这个核心流程。很多人在 Servlet 里直接写业务代码,一个方法里既查库又写库,没有事务控制,一旦第二步失败,第一步的积分流水已经写入,结果用户积分比实际多了一笔。
我一般会抽一个ScoreService,把数据源操作统一放在 Service 方法里,用连接池拿到 Connection 后手动控制事务。示例写法如下:
public void confirmApplyWithTx(int applyId, int operatorId) { DataSource ds = DataSourceManager.getDataSource(); Connection conn = null; try { conn = ds.getConnection(); conn.setAutoCommit(false); TaskApplyDao applyDao = new TaskApplyDao(conn); ScoreLogDao scoreLogDao = new ScoreLogDao(conn); TaskApply apply = applyDao.findById(applyId); if (apply == null || apply.getStatus() != 3) { throw new BusinessException("状态不允许计分"); } TaskType type = new TaskTypeDao(conn).findById(apply.getTaskTypeId()); BigDecimal score = ScoreRule.calculate(type.getBaseScore(), apply.getApplyRoleType()); scoreLogDao.insert(apply.getUserId(), apply.getId(), type.getId(), "add", score, operatorId); applyDao.updateStatus(applyId, 4); conn.commit(); } catch (Exception e) { try { if (conn != null) conn.rollback(); } catch (Exception ignored) {} throw new BusinessException("积分确认失败:" + e.getMessage(), e); } finally { try { if (conn != null) conn.close(); } catch (Exception ignored) {} } }逻辑说明:conn.setAutoCommit(false)开启事务,后续 SQL 全部提交或全部回滚,保证“插入流水”和“更新状态”要么都成功要么都失败。DAO 类都从同一个 Connection 创建,而不是在 DAO 内部再去连接池拿新连接,这是事务控制能否生效的关键。最后finally里关闭的是连接,连接归还到池中供下次使用。
参数说明:如果使用 MyBatis,可以配置@Transactional注解,效果与此相同。这里手动写事务是为了让新手理解边界:Service 方法是一个事务单元,Servlet 只负责接收参数和跳转,不能让 Servlet 直接操作两个 DAO 而失去事务。若服务器使用 Spring Boot,上面流程可以简化为@Service+@Transactional(rollbackFor = Exception.class),但状态判断和积分计算逻辑不变。
5. 避坑指南:任务积分系统上线前最容易翻车的 5 个地方
5.1 JSP 页面中文乱码,根因是三层编码不一致
现象:页面上显示“报了名参加讲座”,提交到 MySQL 里变成“????”,或者从数据库读出来再显示变成乱码。
原因:JSP 文件头没设置pageEncoding="UTF-8",请求参数编码没有统一,数据库连接 URL 没带characterEncoding。这三层只要有一层不对,中文就乱。很多翻车现场是页面文件本身是 UTF-8,但浏览器提交时按 GBK 解析,导致后端拿到乱码。
解决:JSP 页面第一行写<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>;在web.xml里配置一个字符编码过滤器,强制请求和响应都用 UTF-8;JDBC URL 写成jdbc:mysql://localhost:3306/score?useUnicode=true&characterEncoding=utf8&useSSL=false。注意 XML 里&要写成&,否则 Tomcat 启动解析 web.xml 报错。字符编码过滤器用 Spring 的CharacterEncodingFilter或手写一个 Filter 都可以,关键是setEncoding("UTF-8")之后直接调用filterChain.doFilter(request, response)。
5.2 积分被重复计算:唯一索引和幂等校验少了一个
现象:老师点了一次“确认计分”,页面卡顿后又点了一次,积分流水里出现两条相同的加分记录;或者同一个学生反复报名同一个讲座,报名表里有两条记录。
原因:按钮没有提交前禁用,后端confirmApply方法没有做状态判断,数据库也没有唯一约束。这个系统并发量不高,不是因为同时有很多请求,而是因为同一个用户的重复点击和浏览器刷新。
解决:首先在task_apply表加上UNIQUE KEY uk_task_user(task_id, user_id),从数据库层面挡住重复报名;然后在score_log表给task_apply_id和change_type加唯一索引,这样同一审核操作执行两次时,第二次插入直接报Duplicate entry。最后在 Service 方法开头检查apply.getStatus() != 3再抛异常。三层都做,基本不会再重复加分。
5.3 数据库连接池耗尽:每访问一次页面就新建一个连接
现象:系统刚启动时正常,运行一两个小时后,页面开始报 “Connection is not available, request timed out after 30000ms”,Tomcat 日志里出现大量connection leak警告。
原因:代码里每次操作数据库都通过Class.forName+DriverManager.getConnection新建连接,用完不调用close();或者用了连接池但finally块里没有归还连接,连接被请求线程占用,池被耗尽。
解决:统一使用连接池,并在finally中关闭连接。使用 HikariCP 时,在一个静态方法里创建HikariDataSource,所有 DAO 通过dataSource.getConnection()获取连接,执行完 SQL 后conn.close()归还。连接池参数一般配maximumPoolSize=10、minimumIdle=2、connectionTimeout=30000,对高职院系几十个用户足够了。如果发现某个 DAO 方法内自己new Connection,一律改成从 DataSource 获取。连接池配置好后,可以用SELECT 1做心跳测试,Tomcat 启动时能看到连接池成功初始化的日志。
5.4 积分汇总对不上账:积分散落在多个表里
现象:期末统计某教师积分时,页面汇总数是 78.5,教学秘书用 Excel 手工统计是 81,差了 2.5 分,但找不到差在哪里。
原因:系统某段代码在task_apply表直接存了score字段,另一段代码又从score_log表 SUM,两边数据不一致。或者有人直接在数据库里 UPDATE 了sys_user表加了一个总积分字段,业务代码也维护这个字段,结果某次更新失败,总积分和明细对不上。
解决:统一以score_log表作为唯一积分事实来源,所有统计用SELECT user_id, SUM(score) FROM score_log WHERE change_type='add' GROUP BY user_id。不要在task_apply和sys_user表里冗余总积分。如果为了列表性能确实要冗余,必须建一个定时对账任务,每天比较冗余字段与 SUM 明细是否一致,不一致时发告警。核销积分时插入change_type='revoke'的负数记录,而不是删除原来的加分流水,这样保留历史,期末对账时可追溯到每一笔得分。
5.5 静态资源和 Servlet 路径 404:根路径和部署路径冲突
现象:JSP 页面打开后没有样式,点击“任务列表”按钮地址栏变成http://localhost:8080/task/list,但 IDEA 里配置的 Application context 是/ScoreSystem,结果 404。
原因:JSP 里写死了绝对路径,如href="/js/common.js"。当应用部署在http://localhost:8080/ScoreSystem时,这个路径被解析成从服务器根往下找js目录,而资源实际在ScoreSystem/js下。类似地,前端链接直接写/login,没带上下文,Servlet 映射自然匹配不到。
解决:JSP 页面顶部用 JSTL 获取根路径,比如<c:set var="ctx" value="${pageContext.request.contextPath}" />,然后所有链接写成href="${ctx}/js/common.js"、${ctx}/login。Servlet@WebServlet("/login")保持不变,但前端提交表单时 action 写${ctx}/login。部署时无论是根路径还是带应用名,都能自动适配。另外,IDEA 的 Deployment 页面里 Artifact 名称和 Application context 要保持一致,否则打完 war 包扔到 Tomcat 后访问路径和本地不一样,出现本地正常、线上 404 的诡异情况。这种问题我遇到过多次,后来索性每次部署都先在浏览器里看一次静态资源请求的 URL,确认响应码是 200 再往下走。
6. 用一份验收脚本证明系统能用,再决定要不要落地
到这一步,项目已经从设计文档变成了可运行的系统。如果要确保它真能投入使用,我习惯准备一个多角色验收脚本,而不是随机点几下页面。脚本要覆盖一条完整任务流:用管理员账号创建“校园讲座”任务类型,教学秘书发布一场讲座,教师报名,学生也报名,教学秘书把两人状态都改为已结束,然后分别审核计分,最后在个人积分查询页里核对两人的分数。接着用同一个客户端连开两个浏览器,同时点“确认计分”,验证第二次操作会被唯一索引挡住。再把score_log表里的分数手工改掉一分,重新执行汇总 SQL,观察总分是否与该 SQL 输出一致。这套脚本跑完,系统有没有隐藏的重复加分、状态跳跃、权限越权问题基本暴露出来。
如果后续还想扩展,可以在task_apply表加一个complete_time字段,用来统计任务完成耗时;也可以写一个 Quartz 定时任务,每月末自动生成积分汇总报表并导出 Excel。导出功能不用做得很复杂,用Apache POI或直接在 JSP 里用 CSV 输出都能满足二级院系的期末需求。我个人的习惯是先做对账 SQL,再做导出,最后才考虑图表。因为积分系统最重要的不是一个漂亮的折线图,而是任何时候都能说清楚教师积分的每一分来源。希望这份基于 JavaWeb 的任务积分管理系统的设计思路和踩坑经验,能帮你把 Excel 里那点“算不清的账”真正交到系统手里。
本文还有配套的精品资源,点击获取