Java高校兼职管理平台实战:从表设计到并发控制 📅 发布时间:2026/9/17 18:23:02 👁 浏览次数: 简介一份面向计算机科学及相关专业高年级学生、Java学习者的高校兼职管理平台完整项目实例旨在通过信息化管理、智能匹配等设计思路解决传统兼职管理中的信息分散、匹配效率低等问题覆盖需求分析、架构设计、数据库规划、功能开发、系统调试与优化等完整流程。压缩包内共1个docx文档容量89KB可作为课程作业、实训项目或个人研究课题的参考范本。已有184人学习浏览。文档内容详实包含数据库表设计及SQL代码、各功能模块的Java实现、精美GUI界面分阶段创建过程、后端逻辑与界面互动编码、调试优化技巧以及目录结构设计和部署注意事项能帮助读者系统掌握Java EE、MySQL及前端界面设计等实际技能。项目未来还可扩展移动端、AI精准匹配、信用评价体系等方向为深入学习提供切入点。1. 高校兼职管理平台先解决冲突再谈功能高校兼职管理平台在一众 Java 项目实例里很常见但多数示例只把目光停在“发布兼职—学生报名—管理员审核”这条直线流程上。真正常被忽略的是两个工程问题一个学生在同一时段报了多个岗位系统要不要拦管理员把“已录用”的岗位结算完状态还能不能退回“报名中”这两个问题决定了项目是能演示还是能跑完一个学期的真实数据。下面从表结构说起把这两条主线的完整落地路径走一遍主从表承载岗位与报名的关系状态机约束兼职生命周期Swing 构建可操作的桌面 GUI再把连接池、事务锁、线程边界这些写过真实项目才会遇到的细节一起补上。适合刚学完 Java 基础、打算用一个完整实例把 JDBC、Swing 和多线程串起来的读者也适合把它当作毕业设计前的一次全流程演练。2. 项目实例起步兼职平台核心表结构与权限模型2.1 为什么是三张核心表而不是一张大宽表高校兼职管理平台的第一版设计我一般建议先立住三张核心表用户表、岗位表、报名表。用户表负责登录和权限区分岗位表保存兼职发布信息报名表记录学生和岗位之间的每一次申请动作。很多新手会把岗位信息直接嵌入报名表做成一张包含岗位名、岗位描述、学生姓名、学号、申请时间的大宽表这样做小 demo 没感觉但只要一个岗位有十个学生报名岗位描述就要重复存十遍改一次岗位内容要么全部更新、要么出现同一岗位在不同报名记录里内容不一致。正确的做法是让岗位表与报名表形成一对多的主从关系。岗位是一次发布报名是围绕这次发布产生的独立事件。报名记录有自己独立的状态流转待审核、已录用、已拒绝、已结算、已取消这些状态和岗位本身的状态不能混在一个字段里。岗位状态只能描述“这个兼职还在不在招人”而报名状态描述的是“这个学生和这个岗位之间的关系到了哪一步”两类状态服务的目标完全不同。用户表方面平台上存在学生、管理员、教师或辅导员三种角色。不需要建三张用户表登录凭证统一放在一张表里加一个 role 字段区分权限。原因是登录逻辑完全一样账号密码校验通过后根据 role 跳转到不同的功能面板而不是走三套认证流程。角色表另建一张也行但对这种规模的平台属于过度设计一个 int 字段加一个枚举足够。2.2 岗位表与报名表主从关系让“冲突”可查岗位表需要保存兼职的时间范围这是整个平台冲突检测的数据基础。student_id 字段在报名表里也要建索引因为时间冲突检测每次查询都要按学生维度过滤。岗位表里还要记录总名额与已报名人数这两个字段组合起来配合状态字段才能承受并发报名时的约束校验。外键我建议只保留逻辑外键不建物理外键。物理外键在批量导入、按需分表、定时清理时会变成阻碍真实项目普遍把外键约束放在应用层自己校验。表之间的引用关系用字段名规范表达job_id 对应岗位表主键student_id 对应用户表主键从命名上就能看出关联意图。报名表的 union key 也很重要。student_id 和 job_id 联合唯一防止同一个学生对同一个岗位提交两次申请。这个唯一的实现不能放在应用层做判断应该直接在表上建唯一索引把重复提交的拦截交给数据库原因很简单应用层在并发环境下先查后插必然有竞态窗口数据库唯一索引才是最终防线。2.3 建表 SQL 与 status 字段的状态机雏形下面是三张核心表的基础 DDL字段按最小可用集设计后续章节的代码全部基于这个结构运行。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account VARCHAR(32) NOT NULL UNIQUE COMMENT 登录账号, password_hash VARCHAR(64) NOT NULL COMMENT BCrypt哈希不存明文, real_name VARCHAR(16) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT 1-学生 2-管理员 3-教师, phone VARCHAR(16), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE job ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(64) NOT NULL COMMENT 岗位名称, description TEXT, category VARCHAR(32) COMMENT 岗位分类如图书整理、实验室助理, headcount INT NOT NULL DEFAULT 1 COMMENT 总招聘人数, headcount_taken INT NOT NULL DEFAULT 0 COMMENT 已录取人数, start_time DATETIME NOT NULL COMMENT 兼职开始时间, end_time DATETIME NOT NULL COMMENT 兼职结束时间, hourly_wage DECIMAL(8,2) COMMENT 时薪或总报酬, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-招募中 2-已满 3-已结束, created_by BIGINT NOT NULL COMMENT 发布人关联sys_user.id, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_status_start (status, start_time), KEY idx_created_by (created_by) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE application ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_id BIGINT NOT NULL, student_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待审核 1-已录用 2-已拒绝 3-已结算 4-已取消, apply_note VARCHAR(255) COMMENT 学生申请备注, review_note VARCHAR(255) COMMENT 管理员审核备注, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_student_job (student_id, job_id), KEY idx_job_status (job_id, status), KEY idx_student_status (student_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计的核心在 status 的取值语义上。job 表的 status 只有三个值招募中、已满、已结束。招募中可以报名已满不能再报名已结束后所有报名动作禁止。application 表的 status 是五个值的状态机其中 3 表示已结算一旦流转到已结算就不允许再取消或退回这笔学生的劳动报酬已归档。两个时间字段 start_time 和 end_time 是冲突检测的判定基础。idx_status_start 索引覆盖了状态和开始时间的联合查询条件这在后续刷新岗位列表时很关键Where 子句同时带 status 和 start_time 排序就不需要额外文件排序。如果将来要经常查某个教师名下的岗位created_by 的索引也能直接命中。3. 数据访问层实现HikariCP 连接池参数与登录鉴权代码详解3.1 连接池参数HikariCP 的 5 个必调项Swing 客户端的每个按钮都可能触发数据库操作如果每个操作都通过 DriverManager.getConnection 新建连接等于每次请求都把 TCP 握手、MySQL 认证、连接创建的完整链路走一遍。桌面程序不像 Web 服务那样有长期存活的请求线程池操作密集时会在短时间内反复创建连接数据库端 quickly 就会堆积 TIME_WAIT 连接界面会表现为越用越卡。解决方式是统一走连接池。Java 桌面项目里 HikariCP 最常见它的关键参数与适用场景对应关系如下。参数名推荐值作用与调整方式maximumPoolSize10连接池最大连接数桌面程序并发有限10 足够调大不会更快反而增加数据库压力minimumIdle5池中保持的最小空闲连接Swing 程序操作有间歇性设 5 保证低延迟又不过度占用connectionTimeout3000获取连接的超时时间单位毫秒设置过大会让失败操作长时间挂住界面idleTimeout600000空闲连接回收时间默认 10 分钟桌面程序长时间不操作时回收空闲连接maxLifetime1800000连接最大存活时间要略小于数据库 wait_timeout防止拿到已被数据库关闭的失效连接配置文件放在 src/main/resources/db.properties 里用 Properties 类加载避免把数据库地址和密码硬编码在 Java 源码中。jdbcUrljdbc:mysql://localhost:3306/parttime?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot passwordyour_password driverClasscom.mysql.cj.jdbc.Driver maximumPoolSize10 minimumIdle5 connectionTimeout30003.2 JdbcUtils 与用户登录查询的代码详解连接池的创建逻辑抽成工具类整个项目只初始化一次。用静态代码块加载配置然后创建 HikariDataSource 实例并持有。public class JdbcUtils { private static HikariDataSource dataSource; static { try (InputStream in JdbcUtils.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); HikariConfig config new HikariConfig(); config.setJdbcUrl(props.getProperty(jdbcUrl)); config.setUsername(props.getProperty(username)); config.setPassword(props.getProperty(password)); config.setDriverClassName(props.getProperty(driverClass)); config.setMaximumPoolSize(Integer.parseInt(props.getProperty(maximumPoolSize))); config.setMinimumIdle(Integer.parseInt(props.getProperty(minimumIdle))); config.setConnectionTimeout(Long.parseLong(props.getProperty(connectionTimeout))); dataSource new HikariDataSource(config); } catch (IOException e) { throw new ExceptionInInitializerError(db.properties 加载失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }getConnection 拿到的连接来自池中复用不再需要手动加载驱动类HikariCP 在创建数据源时已经完成了 Driver 注册。Swing 里每个业务操作都应该在后台线程拿连接不要在事件分发线程中执行网络请求这一点后续 GUI 章节会专门展开。用户登录查询使用 PreparedStatement 完成参数化查询这是 Java 面试里反复出现的 SQL 注入拦截点。所谓注入本质是拼接字符串把用户输入变成了 SQL 结构的一部分PreparedStatement 将参数与 SQL 模板分离数据库端按参数值处理结构永远不变。public UserDTO login(String account, String password) { String sql SELECT id, account, password_hash, real_name, role FROM sys_user WHERE account ?; try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, account); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { String hash rs.getString(password_hash); if (BCrypt.checkpw(password, hash)) { return new UserDTO( rs.getLong(id), rs.getString(account), rs.getString(real_name), rs.getInt(role) ); } } return null; } } catch (SQLException e) { e.printStackTrace(); return null; } }这里的查询只取了登录必需的五个字段没有用 select *。原因是减少网络传输、降低结果集解析开销更重要的是让代码语义暴露得很清楚登录只需要身份信息岗位列表、申请记录等操作都在各自的方法里单独取。setString 第一个参数是占位符序号从 1 开始与 SQL 中问号出现的位置一一对应。try-with-resources 确保 Connection、PreparedStatement、ResultSet 三层资源全部关上连接归还连接池而不是物理断开。3.3 密码不能存明文BCrypt 校验的落地写法早期课设常见做法是把密码直接存成字符串登录时拿用户输入的密码和库里的字符串做 equals。这种做法在单机 demo 里看不出问题但学生信息属于敏感数据一旦数据库泄露所有明文密码直接暴露。即使转成 MD5也挡不住撞库和彩虹表攻击。MD5 是快速哈希同一密码永远生成同一摘要攻击者可以预计算海量常见密码的摘要直接反查。正确做法是使用 BCrypt 这类自适应加盐哈希。BCrypt 内部自动生成随机盐并把这个盐一并编码进最终结果中每次哈希相同密码得到不同结果却能通过 checkpw 正确校验。注册用户时对原始密码做哈希登录时把库中哈希读出来传给 checkpw 验证。import org.mindrot.jbcrypt.BCrypt; // 注册时生成哈希 String hashed BCrypt.hashpw(password, BCrypt.gensalt(12)); // 登录时校验 boolean matched BCrypt.checkpw(inputPassword, storedHash);gensalt 的参数是工作因子范围 4 到 31数值越大计算越慢。桌面程序按 12 设置单次校验大约几十毫秒安全性和响应速度平衡。不要为了性能降到 10 以下也不要超过 14否则低配机器上登录操作会明显卡顿。工作因子的调优原则是在可接受的响应时间内尽量增大BCrypt 的慢哈希特性本身就是对抗暴力破解的第一道防线。4. GUI 设计与实现Swing 主窗口的线程边界与动态刷新4.1 先定 Swing 的线程边界为什么界面会“卡死”Swing 是单线程 GUI 框架这个单线程指的是事件分发线程 EDT。所有对组件状态的修改、重绘、布局计算都必须在 EDT 上完成后台线程直接触碰组件不会立刻报错但会随机出现界面更新延迟、闪烁、甚至完全无响应。很多基于 Java 的桌面管理系统最终被评价为“卡得要命”多半不是业务代码慢而是把数据库查询直接写在了按钮的 actionPerformed 里。点击按钮后actionPerformed 本身就在 EDT 上执行如果查询耗时三秒这三秒内 EDT 被阻塞整个窗口无法拖动、无法最小化、点击任何按钮都没反应。操作系统层面还在运行但对用户来说已经等于死掉了。正确的分工方式是EDT 只管发起任务和更新界面耗时操作交给后台线程完成后通过 SwingUtilities.invokeLater 切回 EDT 再改界面。4.2 主窗口骨架CardLayout 完成登录页到功能页的切换主界面用 CardLayout 做面板切换。这个布局管理器的核心价值是让多个面板共用一个显示区域通过 show 方法按名字切换。平台界面整体拆成两张卡片登录面板和一个包含功能菜单的主面板。登录成功后隐藏登录卡切到主卡退出登录时反向切换不重新创建窗口。public class MainFrame extends JFrame { private CardLayout cardLayout new CardLayout(); private JPanel rootPanel new JPanel(cardLayout); private LoginPanel loginPanel; private HomePanel homePanel; public MainFrame() { setTitle(高校兼职管理平台); setSize(1024, 700); setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); loginPanel new LoginPanel(this::onLoginSuccess); homePanel new HomePanel(); rootPanel.add(loginPanel, login); rootPanel.add(homePanel, home); setContentPane(rootPanel); } private void onLoginSuccess(UserDTO user) { homePanel.setCurrentUser(user); cardLayout.show(rootPanel, home); } }构造方法传入方法引用 this::onLoginSuccess而不是让 LoginPanel 直接依赖 MainFrame这是为了拆解耦合。LoginPanel 只关心“登录成功后把用户对象传出去”具体切到哪张卡片由 MainFrame 决定。homePanel.setCurrentUser 把登录用户信息带进主面板后续所有业务操作都要用 user.getId() 作为当前操作人标识。窗口初始大小根据三种角色的操作密度取舍。管理员要查看列表和审核记录1024 宽度是底线过窄会导致表格列需要横向拖动。登录按钮回调中使用 SwingUtilities.invokeLater 确保 UI 更新发生在 EDT 上从回调方法返回后再调用 cardLayout.show 是安全的。4.3 JTable 数据绑定与后台刷新岗位列表怎么动起来岗位列表是平台的核心展示组件数据来自 job 表的多条件查询。用 JTable 配合 DefaultTableModel 是最直接的做法但这里有一个新手几乎必踩的坑直接向 model 添加行后表格不会自动刷新。必须在添加完数据后调用 fireTableDataChanged 通知触发重绘否则界面停在一开始加载的状态。后台加载部分用 SwingWorker 处理。SwingWorker 泛型第一个参数是后台任务的返回类型第二个参数是增量发布的数据类型。doInBackground 在后台线程执行done 方法会切回 EDT 自动执行这是最干净的线程模型。SwingWorkerListJobVO, Void worker new SwingWorker() { Override protected ListJobVO doInBackground() { return jobDao.listAvailableJobs(); } Override protected void done() { try { ListJobVO jobs get(); DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); for (JobVO job : jobs) { model.addRow(new Object[]{ job.getId(), job.getTitle(), job.getStartTime(), job.getEndTime(), job.getHeadcountTaken() / job.getHeadcount(), job.getHourlyWage() }); } model.fireTableDataChanged(); } catch (Exception e) { JOptionPane.showMessageDialog(rootPanel, 岗位加载失败: e.getMessage(), 错误, JOptionPane.ERROR_MESSAGE); } } }; worker.execute();setRowCount(0) 的作用是清空旧数据避免每次刷新都不断追加行。如果不先清空再填充刷新操作会累积出大量重复行。addRow 传入的 Object 数组顺序必须与表列的创建顺序一致Swing 的 TableModel 不做列名匹配只按数组下标对齐。get() 在 done 中调用会抛出后台任务产生的异常这种包装方式让错误处理集中在一个位置不会因为后台线程抛错而静默失败。SwingWorker 只能执行一次不能复用。每次刷新创建一个新实例调用 execute 即可execute 是异步的不会阻塞当前线程。Swing 的默认字体在 Windows 下是宋体在 Linux 下可能是完全不支持中文的字体导致中文显示成方块。统一在启动入口设置全局字体是更稳的做法创建任何组件之前设置 UIManager 的默认字体。这也是在 Windows 和 WSL 两种环境切换开发代码时最常让人误以为程序写错了的一个细节。Font font new Font(Dialog, Font.PLAIN, 14); java.util.EnumerationObject keys UIManager.getDefaults().keys(); while (keys.hasMoreElements()) { Object key keys.nextElement(); Object value UIManager.get(key); if (value instanceof Font) { UIManager.put(key, font); } }Dialog 是逻辑字体名Swing 会自动映射到当前系统中最合适的中文字体避免硬编码“微软雅黑”导致 Linux 上找不到。这段代码必须在创建任何 JFrame 之前执行一旦组件已经创建再改 UIManager 对已有组件不生效。5. 核心业务落地报名并发控制、时间冲突检测与结算状态机5.1 并发抢岗报名事务为什么必须配合行级锁报名操作涉及三个动作检查岗位状态与剩余名额、扣减已录取人数、插入报名记录。这三个动作必须在一个事务里完成任何一个步骤失败都要整体回滚。最直接的写法是先 select 查询剩余名额再 update 扣减最后 insert 报名记录。这个思路在单用户下没问题但换个连 GUI 桌面程序也是同一个数据库两个学生同时报名最后一个名额时两个 select 都查到剩 1 人两个都执行了 update最终名额变成负数。解决方案是把检查和扣减合并到一条 update 语句里用 update 影响行数判断是否成功。public boolean applyJob(Long jobId, Long studentId) { String lockSql SELECT id FROM job WHERE id ? AND status 1 FOR UPDATE; String updateSql UPDATE job SET headcount_taken headcount_taken 1 WHERE id ? AND status 1 AND headcount_taken headcount; String insertSql INSERT INTO application (job_id, student_id) VALUES (?, ?); try (Connection conn JdbcUtils.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(lockSql)) { ps.setLong(1, jobId); ps.executeQuery(); } try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setLong(1, jobId); int rows ps.executeUpdate(); if (rows 0) { conn.rollback(); return false; } } try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setLong(1, jobId); ps.setLong(2, studentId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); return false; } }select for update 先把岗位行锁住后续事务在读取同一行时必须等待。这样的事务才真正变成串行执行第二个学生的事务会停留在第一步直到第一个事务提交后才读到锁释放后的最新状态。这里不能只依赖 update 的条件判断锁的时序保证的是事务之间的可见性条件保证的是业务约束两者配合才是完整方案。5.2 时间冲突检测一段 SQL 拦下重叠兼职时间冲突检测需要在报名之前完成。检测条件和常见直觉正好相反不能只查“新兼职开始时间大于已有兼职结束时间”或“新兼职结束时间早于已有兼职开始时间”完整的不重叠判断必须双向覆盖。两个时间段不重叠的条件是已有兼职的结束时间不晚于新兼职开始时间或者已有兼职的开始时间不早于新兼职结束时间。SELECT COUNT(*) FROM application a JOIN job j ON a.job_id j.id WHERE a.student_id ? AND a.status IN (0, 1, 3) AND j.status 3 AND NOT (j.end_time ? OR j.start_time ?);三个问号依次是学生 ID、新岗位开始时间、新岗位结束时间。状态条件里包含 0 待审核、1 已录用、3 已结算排除 2 已拒绝和 4 已取消因为拒绝和取消的记录不占用学生时间。j.status 3 表示只看还没有结束的兼职岗位。拆解这个 SQL 的逻辑内层两个条件分别判断“已有兼职在自己开始前就结束”和“已有兼职在自己结束后才开工”只要满足任一两个时间段就不重叠。取反 NOT 后查询命中的就是所有时间重叠的记录。Count 结果大于零就直接拒绝这次报名。这段判断语句可以复用在一个 select 语句中与 5.1 的事务配合使用时放在 for update 锁的后面。锁住岗位行之后再做冲突检测检测结果才不会因其他事务提交新报名而失效。5.3 结算状态机已结算的数据不允许改状态application 表的状态流转必须集中控制不可以在 GUI 的每个按钮里随意 update status。最实际的原因是状态之间不是全连通图被拒绝的报名不能再次变成已录用已结算的报名不能退回已待审核否则结算金额、岗位名额、学生工时这些数据会对不上账。管理员界面上所有下拉框、按钮的状态入口都调用同一个校验方法。public boolean canTransit(int current, int target) { switch (current) { case 0: return target 1 || target 2 || target 4; case 1: return target 3 || target 4; case 2: return false; case 3: return false; case 4: return false; default: return false; } }许可路径是待审核可以录用、拒绝或取消已录用只能结算或取消已拒绝、已结算、已取消都是终态。终态的判断依据是业务是否还需要人工干预——已拒绝不需要再操作已结算涉及报酬归档已取消说明学生主动退出。这里特别说明一下为什么把 4 已取消放在已录用之后而不是从任意状态都允许取消。若让学生在待审核阶段随意取消并发场景下会出现学员取消后重新申请同一岗位而管理员正在审核时状态错乱。限制取消只发生在已录用之后是为了避免报名环节出现两个方向并发修改同一行数据的可能性。状态机的每次变更应该放在事务里执行查询当前状态、校验 canTransit、更新新状态三个步骤不要分开防止两个管理员同时操作同一条报名记录产生状态覆盖。状态机的校验逻辑虽然简单但在 Java 面试里经常被追问如何设计可以扩展的状态流程。一个小型项目用 switch 分支足够清晰如果状态数增长到十个以上就要考虑用状态模式把每个状态的流转逻辑拆成独立类。现在能明确说出“为什么已结算不能退回”的人比只会写增删改查的更接近岗位要求。6. 发布与排错JAR 打包、中文字体与并发验证6.1 打包与启动JAR 里的资源路径和字符集GUI 程序交付时打成可运行 JAR双击即可执行。打包的关键不在压缩本身而在资源路径。程序运行时用 getResourceAsStream(db.properties) 读取配置文件打成 JAR 后这个文件在压缩包内部用 File 方式读取会直接失败。代码中保持 classpath 相对路径读取方式即可。带 GUI 的程序还需要显式引入 Swing 依赖和数据库驱动。使用 Maven 时引入 maven-assembly-plugin 或 maven-shade-plugin 打出带依赖的 fat JAR。启动命令中 -Dfile.encodingUTF-8 必须放在 -jar 之前否则 JVM 按平台默认字符集初始化Windows 控制台下数据库连接串中的中文参数可能变成乱码。java -Dfile.encodingUTF-8 -jar parttime-platform.jarLinux 下检查中文字体渲染时先输入 fc-list | grep -i cjk 确认系统中装了中文字体。服务器或最小安装的 Ubuntu 默认没有中文字体包Swing 的 Dialog 字体找不到中文映射就会显示方块。6.2 高频异常对照表与并发控制验证异常信息排查方向ClassNotFoundException: com.mysql.cj.jdbc.Driver数据库驱动没有打包进 JAR检查 final jar 是否包含 mysql-connectorLock wait timeout exceeded事务未提交或未回滚导致锁未释放检查代码路径是否遗漏 rollbackDuplicate entry for key uk_student_job连接表缺少唯一索引或在应用层先判断后插入仍发生并发竞态Communications link failure数据库服务未启动或连接串的主机名、端口配置错误中文显示成方块JVM 字符集或操作系统缺少中文字体在启动参数补 UTF-8验证事务和锁是否真正生效有一个直接办法同一台机器上同时启动两个平台实例登录两个不同学生账号对同一个只剩一个名额的岗位同时点击报名。正确实现下两个客户端打印的执行结果只有一个为 true另一个返回 false。如果两个都返回 true检查 update 影响行数的判断是否遗漏或者事务隔离级别是否被你改成了 READ_UNCOMMITTED。打印 update 返回值是定位并发问题的第一手线索。本文还有配套的精品资源点击获取