简介:这份面向Java课程实验与综合设计的档案管理系统源码包,围绕C/S架构下的用户权限、档案入库与检索下载等典型需求展开,适合正在完成Socket通信、多线程与数据库综合练习的学生对照学习。压缩包共47个文件、约1.37MB,包含14个java源文件、15个class文件,以及sql建库脚本、txt说明、jar依赖和docx实验指导等,覆盖客户端、服务器端与数据库连接等模块,便于梳理工程结构与运行思路。已有876人学习,可参考其登录验证、多角色权限管理、文件上传下载、条件查询和用户信息维护的实现方式,并借助实验指导与说明文档理解C/S通信、多线程响应及档案属性入库的关系,用于课程实验复现与功能扩展。
1. 档案管理系统实验设计:从需求到落地的完整方案
课程设计选题里,档案管理系统出现的频率很高,但多数提交上来的东西只能跑个增删改查,答辩时被问一句“档案和普通数据有什么区别”就卡住了。真正要交一份能过关的 Java 实验设计,得先把需求立住:档案数据有保管期限、有密级、有归档状态流转,不是一张表能打发的。这套方案面向的正是要交 Java 课程设计或毕业设计的同学,以及需要一套可讲、可调、可验收的小型管理系统原型的开发者。技术栈锁定在 Java SE 加 JDBC 加 SQL 加 Socket 通信,不依赖重型框架,跑得起来、讲得清楚、改得动,源码结构本身就是实验报告里最硬的那部分证据。
2. 需求拆解与技术选型:为什么不用框架,以及 Socket 到底放在哪
2.1 档案业务的三个硬约束
档案管理系统和图书管理、学生管理最大的区别在于档案本身带有生命周期属性。一份档案从录入开始,要经历待归档、已归档、待销毁、已销毁这几个状态,每个状态之间不是随便跳的,归档要有归档人、归档时间,销毁要有审批依据。课程设计里最常见的翻车点是把档案当成普通记录直接 CRUD,答辩老师一句“你的保管期限到期怎么处理”就没法接。所以这一版设计里,我把档案实体拆成基础信息表加状态流转记录表两张表,基础信息存标题、编号、密级、保管期限、责任人,流转记录存每一次状态变更的操作人、时间、原因。这样查询当前状态走基础表,追溯历史走流转表,逻辑上是干净的。
第二个约束是密级。档案分公开、内部、机密三档,不同密级在查询时的可见范围不同。实验设计里不需要做太复杂的权限模型,但至少要在数据层加一个密级字段,在查询接口里做一次过滤。这比硬塞一个完整的 RBAC 简单得多,又能讲清楚“为什么档案不能所有人看”。
第三个约束是检索。档案检索必须支持按编号精确查、按标题模糊查、按时间段范围查三种方式组合。很多同学只写一个like就交差,结果被问“编号重复了怎么办”答不上来。编号在数据库层加唯一约束,查询接口里把三种条件拼成动态 SQL,这是这一版检索模块的核心设计点。
2.2 技术选型:SE + JDBC + SQL + Socket 的分工
不选 Spring 不是因为框架不好,而是课程设计的答辩场景里,框架封装掉的东西恰恰是老师想看你懂不懂的东西。连接池、事务、DAO 分层,这些用原生 JDBC 手写一遍,实验报告里才有内容可写。Socket 的定位要提前想清楚,它不是用来替代 HTTP 的,而是用来做“档案借阅提醒”这类服务端推送的演示——客户端登录后保持一个长连接,服务端检测到某份档案保管期限临近,主动推一条提醒消息过去。这样 Socket 就是方案里的一个有机模块,而不是为了凑技术点硬加的。
数据库选 MySQL 或 SQLite 都行,教学场景下 SQLite 更省事,不用装服务。JDBC 驱动换成对应的即可,DAO 层接口不变。下面这张表把四个技术点的职责说清楚:
| 技术点 | 在方案中的职责 | 对应模块 |
|---|---|---|
| Java SE | 实体建模、集合操作、多线程处理推送 | 实体层、服务层 |
| JDBC | 连接管理、预编译语句、事务控制 | DAO 层 |
| SQL | 建表约束、动态条件查询、状态统计 | 数据层 |
| Socket | 服务端提醒推送、客户端消息接收 | 通信模块 |
2.3 目录结构先定下来
动手写代码之前先把包结构定死,后面改起来才不会乱。这一版采用典型的分层结构,每个包只干一件事:
src/ ├── entity/ # 档案、用户、流转记录实体 ├── dao/ # 数据库访问接口与实现 ├── service/ # 业务逻辑:归档、销毁、检索 ├── socket/ # 服务端推送与客户端接收 ├── ui/ # 控制台菜单或 Swing 界面 └── util/ # 数据库连接、日期处理工具分层的好处是答辩时能指着结构图讲“我的业务逻辑不碰 SQL,DAO 不碰界面”,这是课程设计里最容易拿分的表达方式。实体类只放字段和 getter/setter,DAO 接口定义方法签名,实现类里写 JDBC,Service 层做状态校验和事务边界,UI 层只负责输入输出。
3. 数据层设计:表结构、约束与 DAO 实现
3.1 三张核心表与约束设计
档案管理的数据模型不需要铺太大,三张表足够撑起完整业务:档案主表、流转记录表、用户表。主表里编号加唯一索引,密级和状态用枚举值存字符串,保管期限存到期日期而不是存年限,这样查询“即将到期”时直接比较日期,不用做运算。流转表用外键关联主表,记录每一次状态变更。用户表只存账号、密码哈希、角色三个字段,角色粗分成管理员和普通用户两档。
-- 档案主表:编号唯一,状态与密级用受限字符串 CREATE TABLE archive ( id INTEGER PRIMARY KEY AUTOINCREMENT, archive_no VARCHAR(32) NOT NULL UNIQUE, title VARCHAR(128) NOT NULL, security VARCHAR(8) NOT NULL DEFAULT '内部', status VARCHAR(8) NOT NULL DEFAULT '待归档', keeper VARCHAR(32), expire_date DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CHECK (security IN ('公开','内部','机密')), CHECK (status IN ('待归档','已归档','待销毁','已销毁')) ); -- 流转记录表:每次状态变更留痕 CREATE TABLE archive_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, archive_id INTEGER NOT NULL, from_status VARCHAR(8), to_status VARCHAR(8) NOT NULL, operator VARCHAR(32) NOT NULL, reason VARCHAR(256), changed_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (archive_id) REFERENCES archive(id) );建表时把 CHECK 约束写进去,是这一版设计里最容易被忽略但答辩时很加分的细节。状态和密级的合法值由数据库兜底,Service 层再校验一次,双保险。expire_date 存到期日而不是“保管 5 年”这种描述,是因为到期提醒的 Socket 推送需要按日期扫描,存年限每次都要算,存日期直接WHERE expire_date <= ?就出来了。
3.2 DAO 层:预编译语句与动态条件拼装
DAO 层的关键不是把 SQL 写出来,而是把参数化查询和动态条件处理好。检索模块要支持编号、标题、时间段、密级四个条件的任意组合,用字符串拼接 SQL 是最常见的翻车写法,既容易被注入,组合逻辑也容易写漏。正确的做法是用 StringBuilder 拼条件片段,参数统一走 PreparedStatement 的占位符。
public List<Archive> search(String no, String title, String security, LocalDate from, LocalDate to) throws SQLException { StringBuilder sql = new StringBuilder( "SELECT * FROM archive WHERE 1=1"); List<Object> params = new ArrayList<>(); // 条件按需追加,占位符与参数顺序严格对应 if (no != null && !no.isBlank()) { sql.append(" AND archive_no = ?"); params.add(no.trim()); } if (title != null && !title.isBlank()) { sql.append(" AND title LIKE ?"); params.add("%" + title.trim() + "%"); } if (security != null && !security.isBlank()) { sql.append(" AND security = ?"); params.add(security); } if (from != null) { sql.append(" AND expire_date >= ?"); params.add(java.sql.Date.valueOf(from)); } if (to != null) { sql.append(" AND expire_date <= ?"); params.add(java.sql.Date.valueOf(to)); } try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } // 结果集映射交给独立方法,保持查询与映射分离 return mapRows(ps.executeQuery()); } }这段代码有三个设计点值得在实验报告里展开。第一,WHERE 1=1是动态 SQL 的惯用写法,保证后面每个条件都能以 AND 开头追加,不用判断“是不是第一个条件”。第二,参数全部收集到 List 里再按序绑定,占位符数量和参数数量由同一个循环保证一致,这是避免“参数越界”异常最稳的写法。第三,查询和结果集映射拆成两个方法,mapRows 里做ResultSet到实体的转换,后面加字段时只改一处。日期参数用java.sql.Date.valueOf转换,注意实体里存的是LocalDate,JDBC 直接 setObject 传 LocalDate 在部分驱动版本上会报错,显式转成 sql.Date 是踩过坑之后的稳妥做法。
3.3 事务边界放在 Service 层
归档和销毁这类操作要同时改主表状态和插流转记录,两条 SQL 必须在一个事务里。DAO 层不负责开事务,每个方法只管执行自己的语句,事务由 Service 层用setAutoCommit(false)框住:
public void archive(int archiveId, String operator) throws SQLException { try (Connection conn = DbUtil.getConnection()) { conn.setAutoCommit(false); // 关闭自动提交,开启事务 try { Archive a = archiveDao.findById(conn, archiveId); if (!"待归档".equals(a.getStatus())) { throw new IllegalStateException("当前状态不允许归档"); } archiveDao.updateStatus(conn, archiveId, "已归档"); logDao.insert(conn, archiveId, a.getStatus(), "已归档", operator, "正常归档"); conn.commit(); // 两条语句都成功才提交 } catch (Exception e) { conn.rollback(); // 任一失败整体回滚 throw e; } finally { conn.setAutoCommit(true); // 连接归还前恢复默认 } } }这里有个容易被忽略的细节:DAO 的方法签名要接收 Connection 参数,而不是每个方法自己开连接。只有把同一个 Connection 传进多个 DAO 方法,事务才能覆盖到它们。很多同学在 DAO 里各自getConnection(),结果 rollback 只回滚了其中一条,数据就脏了。Service 层做状态前置校验也很重要,待归档不能直接跳已销毁,这类流转规则写在 Service 里,DAO 保持纯粹的数据访问职责。
4. 业务层与通信模块:状态机、Socket 提醒与界面串联
4.1 档案状态机与业务规则落地
档案状态流转不是随便改字段,而是一个有向状态机:待归档只能到已归档,已归档可以到待销毁,待销毁可以到已销毁,反向流转一律拒绝。把这个规则写成一个校验方法放在 Service 层,所有状态变更入口统一调用,比在每个业务方法里写 if-else 可靠得多。
private static final Map<String, Set<String>> FLOW = Map.of( "待归档", Set.of("已归档"), "已归档", Set.of("待销毁"), "待销毁", Set.of("已销毁"), "已销毁", Set.of() // 终态,不允许再流转 ); public void changeStatus(int id, String target, String operator, String reason) throws SQLException { Archive a = archiveDao.findById(id); Set<String> allowed = FLOW.getOrDefault(a.getStatus(), Set.of()); if (!allowed.contains(target)) { throw new IllegalStateException( "非法流转:" + a.getStatus() + " -> " + target); } // 销毁前必须有原因,归档不强制 if ("已销毁".equals(target) && (reason == null || reason.isBlank())) { throw new IllegalArgumentException("销毁必须填写原因"); } // 事务部分与 3.3 一致,此处省略 }用 Map 描述状态机的好处是规则集中、可读性强,答辩时指着这段讲“我的业务约束没有散落在各处”比讲十个 CRUD 方法都有说服力。销毁必须填原因这个约束,也是档案业务区别于普通数据的点,写在代码里就是证据。
4.2 Socket 提醒模块:服务端扫描与客户端接收
Socket 在这个方案里承担的是到期提醒推送。服务端起一个监听线程接受客户端连接,同时起一个定时任务扫描临近到期的档案,把提醒消息推给在线客户端。这个模块的代码量不大,但能讲清楚“长连接”和“服务端主动推送”两个概念,对课程设计来说技术点就够了。
// 服务端:接受连接后保持输出流,供定时任务推送 public class RemindServer { private final List<PrintWriter> clients = Collections.synchronizedList(new ArrayList<>()); public void start(int port) throws IOException { ServerSocket server = new ServerSocket(port); new Thread(() -> { // 接受连接线程 while (true) { Socket s = server.accept(); PrintWriter out = new PrintWriter(s.getOutputStream(), true); clients.add(out); } }).start(); startScanTask(); // 启动到期扫描 } private void startScanTask() { ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor(); pool.scheduleAtFixedRate(() -> { // 查询 7 天内到期且未销毁的档案 List<Archive> soon = archiveDao.findExpiring(7); String msg = soon.isEmpty() ? null : "有 " + soon.size() + " 份档案临近保管期限"; if (msg != null) { synchronized (clients) { clients.forEach(c -> c.println(msg)); } } }, 0, 1, TimeUnit.HOURS); } }服务端设计上有两个注意点。clients 列表用Collections.synchronizedList包一层,因为接受连接线程和扫描线程会并发访问它,不包的话偶发ConcurrentModificationException,这类问题答辩演示时不一定复现,但代码审查时一眼就能看出来。扫描任务用scheduleAtFixedRate按小时跑,实际演示时可以把周期改成分钟级方便展示效果,参数改一下就行。
客户端这边就是一个循环读取:
// 客户端:登录后保持连接,持续接收提醒 public class RemindClient { public void connect(String host, int port) throws IOException { Socket socket = new Socket(host, port); BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); String line; while ((line = in.readLine()) != null) { // 阻塞等待推送 System.out.println("[提醒] " + line); } } }客户端用独立线程跑这个循环,主线程继续处理菜单操作,否则界面会被阻塞。这个“一个线程收消息、一个线程处理输入”的结构,是 Socket 模块里最值得在报告里画图说明的部分。
4.3 界面层:控制台菜单与 Service 的对接
课程设计不要求界面多漂亮,但要能完整走一遍业务流程。控制台菜单按角色分两套:管理员有归档、销毁、用户管理,普通用户只有检索和查看。菜单层只做输入解析和结果打印,所有判断都调 Service,不在菜单里写业务逻辑。这样界面换成 Swing 时,Service 和 DAO 一行不用改,这也是分层设计在答辩时最能自圆其说的地方。
检索菜单要把 3.2 的四个条件都暴露出来,允许用户只填其中几项,空着就传 null。查询结果打印时把密级过滤加上:普通用户查不到机密档案,管理员查全部。这个过滤放在 Service 的检索方法里,根据当前登录用户角色决定是否追加密级条件,不要放到界面层,否则换个界面就漏了。
5. 避坑与排查:实验设计里最容易翻车的五个点
5.1 日期类型在实体和 JDBC 之间来回转换报错
现象是插入带到期日的档案时抛类型转换异常,或者查出来的日期少一天。原因是实体用LocalDate,直接ps.setObject传进去,部分 JDBC 驱动不认,或者时区处理不一致。解决办法是统一在 DAO 边界做转换:写入时java.sql.Date.valueOf(localDate),读取时rs.getDate("expire_date").toLocalDate()。转换只在这两处出现,实体和 Service 层不碰 sql 包,边界清晰就不会乱。
5.2 事务回滚了但数据还是脏的
现象是归档操作中途失败,状态改了但流转记录没插进去,或者反过来。原因基本都是 DAO 方法各自开了连接,Service 层的 rollback 管不到另一个连接上的语句。排查时先看 DAO 方法签名有没有接收 Connection 参数,再看 Service 里是不是所有相关 DAO 调用都传了同一个 conn。改法就是 3.3 那套:连接由 Service 开,往下传,事务范围跟着连接走。
5.3 Socket 客户端收不到推送
现象是服务端日志显示消息发出去了,客户端界面没反应。常见原因有三个:客户端读取循环被放在主线程导致界面卡死、PrintWriter 没加 autoflush 导致消息留在缓冲区、客户端连接后没有保持引用被 GC 回收。排查顺序是先确认客户端是不是在独立线程里 readLine,再看服务端new PrintWriter(out, true)第二个参数有没有传 true,最后检查 Socket 对象有没有被局部变量持有后丢弃。
5.4 检索条件组合时参数错位
现象是只填标题查出来是空的,或者填了编号却按标题匹配了。原因是拼 SQL 时条件顺序和参数 add 的顺序不一致,或者某个条件判断写漏了导致占位符比参数多。改法是严格按 3.2 的写法,条件和参数在同一个 if 块里成对出现,拼完 SQL 后可以打印一下占位符个数和 params.size() 对比,不相等就是有分支写漏了。
5.5 状态约束只写在界面上
现象是界面上按钮灰了不能点,但直接调 Service 还能改状态,答辩时被现场用代码验证就尴尬了。原因是校验逻辑写在了菜单层。改法是把 4.1 的状态机校验放进 Service 的 changeStatus 里,界面层的按钮禁用只是体验优化,真正的约束必须在 Service 层,这样无论从哪个入口调都拦得住。
6. 验收与答辩:怎么用一份检查清单把方案讲圆
验收这件事,我一般建议在写代码之前就把检查清单列出来,写完对着勾,比写完再想“还差什么”效率高得多。这份方案对应的验收项可以分成三档:能跑通、能讲清、能扩展。能跑通是最低要求,登录、录入、归档、检索、销毁五条主流程走一遍不报错;能讲清是答辩要求,每个模块的设计理由要能一句话说明白;能扩展是加分项,比如密级过滤能不能换成更细的权限模型、Socket 提醒能不能换成邮件。
下面这张清单可以直接拿去对着自查:
| 验收项 | 检查方法 | 合格标准 |
|---|---|---|
| 表结构约束 | 直接插非法状态值 | 数据库拒绝写入 |
| 动态检索 | 四个条件分别单填、组合填 | 结果与预期一致,无异常 |
| 事务完整性 | 归档时手动制造异常 | 状态与记录同时回滚 |
| 状态机 | 直接调 Service 跨状态流转 | 抛出明确异常 |
| Socket 推送 | 造一份 7 天内到期档案 | 客户端 1 分钟内收到提醒 |
| 密级过滤 | 普通用户检索机密档案 | 结果为空,管理员可见 |
答辩时最容易被追问的三个问题,提前把答案准备好:为什么不用框架,答案回到 2.2 的分工表,讲清楚手写 JDBC 是为了展示对连接、事务、分层的理解;档案和普通数据的区别,答案回到 2.1 的三个约束,状态机、密级、检索组合是核心论据;Socket 是不是硬凑的,答案回到 4.2,讲清楚它是为到期提醒服务的,有明确的业务触发点。
有个血泪经验值得说一句:演示前一定要用一份全新的数据库跑一遍完整流程,不要拿开发时满是测试数据的库去答辩。测试数据里状态乱七八糟,演示归档时撞上一个已经是已归档的记录,当场抛异常,后面讲得再好也救不回来。从那以后我每次交付这类系统,都会在验收清单最上面加一条“清库重跑主流程”,这等于给自己留一颗后悔药。希望这套结构和清单能帮到你,把实验设计做成一份能讲、能跑、能改的方案,而不是一堆只能演示一次的代码。
本文还有配套的精品资源,点击获取