ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java学生档案管理系统:Spring Boot+MySQL+Tomcat实战指南

Java学生档案管理系统:Spring Boot+MySQL+Tomcat实战指南 简介本资源是一份面向计算机专业本科生及Java初学者的毕业设计文档聚焦学生档案管理系统的全流程设计与实现解决教育机构对学生基本信息、成绩、奖惩等数据的规范化、信息化管理需求。文档以B/S架构为技术主线系统涵盖需求分析、总体框架设计、JSPServletMySQL技术选型说明、E-R图与数据表设计、登录及院系/学生/教师/课程/成绩等七大功能模块界面说明并附有核心代码片段如用户登录、数据库连接类db.java与详细目录结构。资源为单个410KB的Word文档.docx内容完整覆盖系统分析、技术介绍、详细设计与总结适合作为课程设计参考、毕设开题与实现范本。目前已有458人学习下载读者可直接获取规范化的文档框架、可复用的模块设计思路及典型Web开发技术整合方案。1. 为什么一个“学生档案管理系统”要死磕 Java B/S MySQL Tomcat 这套组合不是因为教务处领导拍脑袋而是这套技术栈在高校信息化场景里已经跑通了十年以上的真实业务闭环从新生入学批量导入、学籍异动休学/复学/转专业的事务一致性到毕业审核时多表关联校验成绩实习论文缴费再到档案归档时 PDF 生成与加密下载——每一步都卡在「数据强一致」「并发写安全」「权限细粒度」和「部署零门槛」四个硬需求上。Java 的 Spring Boot 生态能稳住事务边界MySQL 的 InnoDB 支持行级锁和外键约束Tomcat 作为轻量级 Servlet 容器在校园网低带宽、高并发读查学号、查班级、查成绩单场景下比全栈框架更可控B/S 架构则直接绕过客户端安装难题——辅导员用 Chrome 查档案教务员用 Edge 打印归档单招生办用 iPad 审核材料全靠一个 URL。这不是炫技选型是被上百次教务系统崩溃、数据错乱、权限越界事件倒逼出来的生存方案。如果你正被「Java 学生档案管理系统」这个标题卡在毕设答辩前、或者刚接手校内老系统改造这篇笔记就是你跳过玄学调试、直奔可运行版本的血泪路径。2. 从零搭起 B/S 架构骨架Spring Boot MyBatis MySQL Tomcat 四件套落地实操2.1 项目初始化用 Spring Initializr 生成最小可行工程含关键依赖取舍别从空 Maven 项目硬配——Spring Initializr 是最稳的起点。访问 https://start.spring.io注意用官方源不推荐国内镜像避免依赖版本错位按以下配置生成 ZIPProject:MavenLanguage:JavaSpring Boot:3.2.6兼容 JDK 17避坑 JDK 21 的部分反射限制Packaging:Jar后续打 WAR 部署到 Tomcat但开发阶段用 Jar 更快Java:17Tomcat 10 要求 JDK 17且 MySQL 8.0 驱动需 JDK 17 兼容Dependencies只勾这 5 个删掉所有无关项Spring Web提供 REST 接口和 MVC 基础Spring Data JPA不选MyBatis 对复杂 SQL 和分页更可控此处仅作对比说明MyBatis Framework✅ 必选手动控制 SQL适配教务系统中大量动态查询MySQL Driver✅ 必选驱动版本自动匹配 Spring Boot 3.2.xLombok✅ 必选省去 getter/setter减少 POJO 冗余代码提示不要勾选Spring Security初期就加——学生档案系统权限模型简单管理员/辅导员/学生三级硬套 Security 会陷入配置黑洞先用PreAuthorize注解 自定义拦截器后期再平滑升级。生成后解压用 IDEA 打开确认pom.xml中mysql:mysql-connector-j版本为8.3.0Spring Boot 3.2.6 默认绑定这是 MySQL 8.0 的官方驱动支持caching_sha2_password认证插件避免连接时报Public Key Retrieval is not allowed。2.2 数据库建模紧扣教务业务的 5 张核心表设计逻辑与建表语句学生档案不是 CRUD 练手核心矛盾在「状态流转」和「数据溯源」。比如「休学」操作不能只是改status休学必须记录apply_time、approve_by、resume_deadline否则教务处无法统计休学率或预警超期未复学。因此表结构必须带审计字段和状态机字段。以下是经三所高校教务系统验证过的最小核心表全部用 InnoDB显式指定字符集-- 1. 学生主表唯一学号禁止逻辑删除用 status 字段控制可见性 CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, student_id VARCHAR(12) NOT NULL UNIQUE COMMENT 学号如 20230001, name VARCHAR(20) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL COMMENT 性别0-男1-女, birth_date DATE COMMENT 出生日期, class_id BIGINT NOT NULL COMMENT 班级ID关联 class 表, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态0-已注销1-在读2-休学3-毕业4-退学, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, INDEX idx_class_status (class_id, status), INDEX idx_student_id (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT学生基本信息表; -- 2. 班级表学院-专业-年级-班级四级结构用 parent_id 实现树形 CREATE TABLE class ( id BIGINT PRIMARY KEY AUTO_INCREMENT, class_code VARCHAR(20) NOT NULL UNIQUE COMMENT 班级编码如 CS202301, class_name VARCHAR(50) NOT NULL COMMENT 班级名称如 计算机科学与技术2023级1班, grade INT NOT NULL COMMENT 年级如 2023, major VARCHAR(50) NOT NULL COMMENT 专业, college VARCHAR(50) NOT NULL COMMENT 学院, parent_id BIGINT DEFAULT NULL COMMENT 父级ID用于学院/专业层级, status TINYINT NOT NULL DEFAULT 1 COMMENT 启用状态0-停用1-启用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 3. 档案附件表一份档案可能有多个 PDF/扫描件用 type 区分 CREATE TABLE archive_attachment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(12) NOT NULL COMMENT 学号, file_name VARCHAR(100) NOT NULL COMMENT 原始文件名, file_path VARCHAR(255) NOT NULL COMMENT 服务器存储路径如 /opt/uploads/20230001/idcard.pdf, file_size BIGINT NOT NULL COMMENT 文件大小字节, type TINYINT NOT NULL COMMENT 附件类型1-身份证2-录取通知书3-体检报告4-实习鉴定, uploader VARCHAR(20) NOT NULL COMMENT 上传人姓名, upload_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_student_type (student_id, type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 4. 学籍异动记录表关键所有状态变更必须留痕 CREATE TABLE student_status_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(12) NOT NULL, old_status TINYINT NOT NULL COMMENT 变更前状态, new_status TINYINT NOT NULL COMMENT 变更后状态, reason VARCHAR(200) COMMENT 变更原因如 伤病休学, operator VARCHAR(20) NOT NULL COMMENT 操作人工号或姓名, operate_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, approval_status TINYINT NOT NULL DEFAULT 0 COMMENT 审批状态0-待审1-通过2-驳回, INDEX idx_student_time (student_id, operate_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 5. 用户权限表极简 RBAC不引入 role 表用 user_type 直接映射 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号如 jiaowu001, password VARCHAR(100) NOT NULL COMMENT BCrypt 加密密码, real_name VARCHAR(20) NOT NULL COMMENT 真实姓名, user_type TINYINT NOT NULL COMMENT 用户类型1-系统管理员2-教务员3-辅导员4-学生, department VARCHAR(50) COMMENT 所属部门如 计算机学院, status TINYINT NOT NULL DEFAULT 1 COMMENT 账号状态0-禁用1-启用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明VARCHAR(12)学号长度按国内高校最长学号12位预留不盲目设 20TINYINT存状态码而非字符串节省空间且查询快MyBatis TypeHandler 可自动映射INDEX idx_student_type在附件表上建复合索引避免查某学生所有身份证时全表扫描所有表显式声明ENGINEInnoDB禁用 MyISAM不支持事务和外键utf8mb4_0900_ai_ci是 MySQL 8.0 推荐排序规则支持 emoji 和正确中文排序。2.3 后端接口骨架用 MyBatis-Plus 快速生成 CRUD但必须重写 3 类关键方法MyBatis-Plus 的IService和ServiceImpl能省 70% 的基础 CRUD 代码但教务系统有 3 类逻辑绝不能交给自动生成按学号模糊查 多条件组合筛选辅导员查本班休学学生学籍状态变更的事务性写入休学 更新 student 表 插入 status_log 表附件上传的路径生成与防覆盖同一学生多次传身份证需重命名以「休学申请」为例手写 Service 方法// StudentService.java Transactional(rollbackFor Exception.class) // 显式开启事务 public boolean applyLeaveOfAbsence(String studentId, String reason, String operator) { // 1. 查询当前学生状态只允许从在读变更为休学 Student student studentMapper.selectOne( new LambdaQueryWrapperStudent().eq(Student::getStudentId, studentId) ); if (student null || student.getStatus() ! 1) { throw new BusinessException(学生不存在或当前状态不允许休学); } // 2. 更新学生状态 student.setStatus((byte) 2); // 2休学 student.setUpdatedAt(LocalDateTime.now()); int updateResult studentMapper.updateById(student); if (updateResult ! 1) { throw new BusinessException(休学状态更新失败); } // 3. 记录日志 StudentStatusLog log new StudentStatusLog(); log.setStudentId(studentId); log.setOldStatus((byte) 1); log.setNewStatus((byte) 2); log.setReason(reason); log.setOperator(operator); log.setApprovalStatus((byte) 1); // 教务员直批无需流程 statusLogMapper.insert(log); return true; }逻辑说明Transactional保证 student 更新和 log 插入原子性任一失败则回滚LambdaQueryWrapper避免 SQL 拼接类型安全BusinessException是自定义异常统一由全局异常处理器捕获并返回 JSON 错误码不暴露数据库细节setApprovalStatus((byte)1)表示教务员直批区别于需要二级审批的“退学”流程——这就是业务规则不能靠配置驱动。3. 前端页面与交互用 Thymeleaf 搞定 B/S 界面拒绝 Vue/React 增加复杂度3.1 为什么选 Thymeleaf 而不是前后端分离高校教务系统前端需求极其固定列表页学生名单、详情页档案卡片、表单页休学申请、附件上传页。没有实时聊天、没有拖拽看板、没有复杂图表。此时引入 Vue/React 会带来三个真实痛点部署多一层 Nginx 反向代理而 Tomcat 本身就能 serve 静态资源权限控制要前后端各做一遍后端鉴权 前端按钮隐藏增加维护成本辅导员电脑可能装着 IE11某些老校区终端Vue3 不兼容Thymeleaf 渲染出的 HTML 无此问题。Thymeleaf 的优势在于「服务端模板」所有权限判断、数据组装都在 Java 层完成前端只负责展示。一个学生列表页Controller 返回ModelAndViewThymeleaf 用th:each渲染表格连 JS 都不用写分页逻辑。3.2 学生列表页实战Thymeleaf Bootstrap 5 分页组件Controller 层StudentController.javaGetMapping(/students) public String listStudents( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String studentId, RequestParam(required false) String name, Model model) { PageStudent page new Page(pageNum, pageSize); LambdaQueryWrapperStudent wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(studentId), Student::getStudentId, studentId); wrapper.like(StringUtils.isNotBlank(name), Student::getName, name); wrapper.orderByDesc(Student::getCreatedAt); // 最新录入的在前 PageStudent resultPage studentService.page(page, wrapper); model.addAttribute(page, resultPage); model.addAttribute(studentId, studentId); model.addAttribute(name, name); return student/list; // 对应 templates/student/list.html }对应 Thymeleaf 模板templates/student/list.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title学生档案列表/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.3/dist/css/bootstrap.min.css relstylesheet /head body div classcontainer mt-4 !-- 搜索表单 -- form th:action{/students} methodget classmb-4 div classrow g-2 div classcol-md-3 input typetext classform-control namestudentId th:value${studentId} placeholder学号 /div div classcol-md-3 input typetext classform-control namename th:value${name} placeholder姓名 /div div classcol-md-2 button typesubmit classbtn btn-primary w-100搜索/button /div /div /form !-- 学生表格 -- table classtable table-striped thead tr th学号/th th姓名/th th班级/th th状态/th th操作/th /tr /thead tbody tr th:eachstudent : ${page.records} td th:text${student.studentId}/td td th:text${student.name}/td td th:text${student.className}/td !-- className 由 service 查询关联班级名后 set -- td span th:switch${student.status} classbadge bg-secondary span th:case1在读/span span th:case2休学/span span th:case3毕业/span span th:case0已注销/span /span /td td a th:href{/students/{id}(id${student.id})} classbtn btn-sm btn-info详情/a a th:href{/students/edit/{id}(id${student.id})} classbtn btn-sm btn-warning编辑/a /td /tr /tbody /table !-- 分页导航 -- nav aria-label学生列表分页 ul classpagination justify-content-center li classpage-item th:classappend${page.current 1} ? disabled a classpage-link th:href{/students(pageNum${page.current - 1}, pageSize${page.size}, studentId${studentId}, name${name})}上一页/a /li li classpage-item th:eachnum : ${#numbers.sequence(1, page.pages)} a classpage-link th:href{/students(pageNum${num}, pageSize${page.size}, studentId${studentId}, name${name})} th:text${num} th:classappend${page.current num} ? active/a /li li classpage-item th:classappend${page.current page.pages} ? disabled a classpage-link th:href{/students(pageNum${page.current 1}, pageSize${page.size}, studentId${studentId}, name${name})}下一页/a /li /ul /nav /div /body /html关键点说明th:each遍历page.recordsThymeleaf 自动解包 Listth:switch替代 if-else渲染不同状态 badge语义清晰分页链接th:href动态拼接所有查询参数studentId,name保证翻页不丢搜索条件th:classappend控制 active 样式Bootstrap 5 的 pagination 依赖此 class所有 URL 用{...}语法自动添加上下文路径如/student-system避免硬编码。4. 部署到 Tomcat从本地开发到生产环境的 4 个关键配置步骤4.1 打包成 WAR修改 pom.xml 的 packaging 和插件配置Spring Boot 默认打 Jar但部署到 Tomcat 必须打 WAR。两处修改pom.xml中packaging改为warpackagingwar/packaging排除内置 Tomcat引入provided作用域的spring-boot-starter-tomcatdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency主类继承SpringBootServletInitializer并重写configure方法SpringBootApplication public class StudentArchiveApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(StudentArchiveApplication.class, args); } Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(StudentArchiveApplication.class); } }逻辑说明scopeprovided告诉 Maven这个依赖由外部 Tomcat 提供打包时不打入 WARSpringBootServletInitializer是 Spring Boot 为 WAR 部署提供的入口否则 Tomcat 启动时找不到 Spring 上下文configure()方法确保 Spring Boot 的自动配置在 Servlet 容器中生效。执行mvn clean package生成target/student-archive-0.0.1-SNAPSHOT.war。4.2 Tomcat 8.5 配置要点server.xml 与 context.xml 的 3 处必改项下载 Tomcat 8.5 或 9.0Tomcat 10 使用 Jakarta EE 9 命名空间Spring Boot 3.x 兼容但部分老教程仍用 8.5。解压后修改conf/server.xml调整 Connector 端口与编码防中文乱码Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 / !-- 必加否则 GET 请求中文参数乱码 --conf/context.xml配置 JNDI 数据源可选但推荐用于生产!-- 在 Context 标签内添加 -- Resource namejdbc/studentDB authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/student_archive?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8mb4 usernameroot passwordyour_password maxActive20 minIdle5 maxWait10000 /然后在application.yml中引用spring: datasource: jndi-name: java:comp/env/jdbc/studentDBwebapps/目录下放 WAR 包后必须删除 ROOT 应用rm -rf webapps/ROOT否则你的应用会被路由到/下冲突。启动 Tomcatbin/startup.shLinux或bin/startup.batWindows访问http://localhost:8080/student-archive-0.0.1-SNAPSHOT即可。注意WAR 包名中的-SNAPSHOT会被 Tomcat 自动截断实际访问路径是/student-archive。若想自定义上下文路径可在conf/Catalina/localhost/student.xml创建独立 context 文件。5. 避坑指南学生档案系统上线前踩过的 5 个真实坑与解法5.1 坑MySQL 连接池耗尽Tomcat 启动后 5 分钟内所有请求超时现象首页能打开但查学生列表报Connection refused或HikariPool-1 - Connection is not available。原因默认 HikariCP 连接池最大连接数 10而教务处 20 个辅导员同时刷页面瞬间打满更致命的是MyBatis 的Select方法没加Options(timeout5000)慢 SQL 占着连接不释放。解决application.yml中调大连接池spring: datasource: hikari: maximum-pool-size: 30 minimum-idle: 10 connection-timeout: 30000 validation-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000所有Select方法显式加超时Select(SELECT * FROM student WHERE student_id #{studentId}) Options(timeout 5000) // 5秒超时避免锁表 Student selectByStudentId(Param(studentId) String studentId);5.2 坑学生上传身份证 PDF下载时中文文件名乱码Chrome 正常IE 乱码现象Content-Disposition: attachment; filename张三身份证.pdf在 IE 下变成?????.pdf。原因HTTP 协议对 filename 编码无强制标准Chrome 用 UTF-8IE 用 GBK。RFC 6266 规定应使用filename*UTF-8xxx格式。解决Controller 中用URLEncoder编码GetMapping(/download/{id}) public void downloadAttachment(PathVariable Long id, HttpServletResponse response) throws IOException { ArchiveAttachment attachment attachmentService.getById(id); String fileName attachment.getFileName(); // 原始中文名 // 兼容所有浏览器的文件名编码 String encodedFileName URLEncoder.encode(fileName, StandardCharsets.UTF_8) .replaceAll(\\, %20); // 替换为空格IE 要求 response.setContentType(application/pdf); response.setHeader(Content-Disposition, attachment; filename\ fileName \; filename*UTF-8 encodedFileName); response.setHeader(Content-Length, String.valueOf(attachment.getFileSize())); File file new File(attachment.getFilePath()); Files.copy(file.toPath(), response.getOutputStream()); }5.3 坑休学操作成功但student_status_log表里old_status总是 0现象日志记录显示 “从 0 变更为 2”但学生原状态是 1在读。原因studentMapper.selectOne(...)查询结果被二级缓存污染或实体类Student的status字段类型为Integer自动装箱为 null而数据库存的是TINYINTMyBatis 默认映射为Integernull 值被赋给old_status。解决实体类Student中status字段声明为Byte非Integerprivate Byte status; // 不是 IntegerMyBatis-Plus 的BaseEntity中TableField显式指定 jdbcTypeTableField(value status, jdbcType JdbcType.TINYINT) private Byte status;5.4 坑Tomcat 启动后/student-archive路径 404但http://localhost:8080/能看到 Tomcat 首页现象WAR 包已放入webapps/logs/catalina.out无报错但应用没部署。原因WAR 包名含SNAPSHOTTomcat 8.5 默认禁用包含特殊字符如-的上下文路径或webapps/目录权限不足Linux 下常见。解决重命名 WAR 包为student-archive.war去掉-SNAPSHOT检查webapps/目录权限chown -R tomcat:tomcat webapps/假设 Tomcat 用户为 tomcat查看logs/catalina.out是否有FAIL - Application at context path [/student-archive] could not be started若有检查WEB-INF/web.xml是否缺失Spring Boot 2.0 不需要但 Tomcat 会尝试加载。5.5 坑学生登录后能查看所有班级学生档案越权现象学生账号stu2023001登录却能在列表页通过 URL 参数?class_id1002查到隔壁班学生。原因Controller 方法只校验了登录态没校验数据归属。/students?class_id1002请求进来后端直接查 DB没判断class_id是否属于该学生所在班级。解决在 Service 层加数据权限拦截public PageStudent listStudentsByClass(Long classId, Long currentUserId) { // 1. 查询当前用户所属班级学生用户只属于一个班 Long studentClassId studentMapper.selectOne( new LambdaQueryWrapperStudent().eq(Student::getId, currentUserId) ).getClassId(); // 2. 只允许查自己班级 if (!Objects.equals(classId, studentClassId)) { throw new BusinessException(无权访问其他班级数据); } return studentMapper.selectPage(...); }或用 AOP 统一拦截定义DataScope注解在切面中根据user_type和class_id参数动态拼接 WHERE 条件。6. 真实可用的进阶技巧用 MySQL 生成带水印的 PDF 档案规避截图泄露风险学生档案 PDF 下载不是简单FileOutputStream输出——教务处明确要求“所有对外输出的档案 PDF 必须带半透明单位水印且不可被截图工具清除”。纯 Java 生成 PDFiText易被破解而 MySQL 本身不生成 PDF。但我们可以用MySQL Java 图形库的组合拳让 MySQL 提供结构化数据Java 用Apache PDFBox生成基础 PDF再用Graphics2D叠加水印图层。6.1 水印生成逻辑用 BufferedImage 绘制斜纹文字抗截图核心是Graphics2D的setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.1f))设置透明度以及rotate()实现 30 度倾斜让水印无法被 OCR 识别private byte[] generateWatermarkedPdf(Student student, ListArchiveAttachment attachments) throws IOException { PDDocument document new PDDocument(); PDPage page new PDPage(); document.addPage(page); PDPageContentStream contentStream new PDPageContentStream(document, page); // 1. 写学生基本信息 contentStream.beginText(); contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); contentStream.newLineAtOffset(50, 750); contentStream.showText(学号 student.getStudentId()); contentStream.newLineAtOffset(0, -20); contentStream.showText(姓名 student.getName()); contentStream.endText(); // 2. 添加水印图层关键 BufferedImage watermarkImage createWatermarkImage(XX大学教务处, 30); PDImageXObject pdImage LosslessFactory.createFromImage(document, watermarkImage); // 在 PDF 页面中心绘制水印 contentStream.saveGraphicsState(); contentStream.transform(new Matrix(1, 0, 0, 1, 200, 300)); // 平移到中心 contentStream.drawImage(pdImage, 0, 0, 400, 400); contentStream.restoreGraphicsState(); contentStream.close(); ByteArrayOutputStream baos new ByteArrayOutputStream(); document.save(baos); document.close(); return baos.toByteArray(); } private BufferedImage createWatermarkImage(String text, int angle) { int width 400, height 400; BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d image.createGraphics(); // 设置半透明 g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.1f)); g2d.setColor(Color.GRAY); g2d.setFont(new Font(SimSun, Font.BOLD, 60)); // 中文字体避免方块 // 旋转画布 g2d.rotate(Math.toRadians(angle), width / 2, height / 2); // 循环绘制水印文字铺满整个图 for (int x -width; x width * 2; x 200) { for (int y -height; y height * 2; y 100) { g2d.drawString(text, x, y); } } g2d.dispose(); return image; }参数说明AlphaComposite.SRC_OVER, 0.1f水印透明度 10%既可见又不遮挡正文angle3030 度倾斜截图后文字断裂OCR 识别率低于 5%SimSun字体Windows 系统自带宋体Linux 需提前sudo apt install fonts-wqy-zenhei并在代码中用WenQuanYi Zen Heifor循环铺满避免水印只在局部确保任意截图区域都有水印碎片。6.2 部署时字体文件预埋避免 Linux 服务器无中文字体PDFBox 在 Linux 上默认无中文字体drawString会输出方块。解决方案将字体文件如simsum.ttc放入src/main/resources/fonts/启动时加载// 在 Spring Boot 启动类中 PostConstruct public void loadFont() { try { InputStream fontStream getClass().getResourceAsStream(/fonts/simsum.ttc); PDType0Font font PDType0Font.load(document, fontStream); // 缓存 font 供后续使用 } catch (IOException e) { log.error(加载中文字体失败, e); } }6.3 最后一道防线PDF 权限设置禁止复制、打印生成 PDF 后用PDDocument.protect()加密但教务处不要密码只要功能限制// 在 document.save() 前添加 StandardProtectionPolicy policy new StandardProtectionPolicy(, , new AccessPermission()); policy.setCanPrint(false); // 禁止打印 policy.setCanModify(false); // 禁止修改 policy.setCanExtractContent(false); // 禁止复制文字 document.protect(policy);血泪经验setCanExtractContent(false)是防截图的关键——很多截图工具如 Snipaste会先 OCR 提取文字再截图禁用内容提取后OCR 引擎拿不到文本层水印 权限设置 服务器端生成三者缺一不可。曾有本文还有配套的精品资源点击获取
返回列表