ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C#档案管理系统开发:数据模型、三层架构与借阅归还实现

2026/10/8 13:48:50 拓冰建站 浏览量
C#档案管理系统开发:数据模型、三层架构与借阅归还实现 简介基于C#的单位档案信息管理系统源码包面向计算机相关专业毕业设计、课程设计学习者以及需要档案信息化方案的企事业单位开发人员。系统基于C#与.NET Framework采用Windows Forms构建界面通过ADO.NET操作SQL Server数据库划分数据访问层、业务逻辑层与用户界面层覆盖档案增删改查、权限管理、异常处理、数据加密等模块展示较完整的C/S架构项目实现思路。资源共460个文件大小6.2MB包含33个aspx页面、292个png及55个gif界面素材、12个css、25个js、2个mdf/ldf数据库文件等既有可运行代码也有过程截图便于按目录结构阅读源码和还原环境。内容预览中出现dangan.aspx、manageadd.aspx、zhigong.aspx等对应档案、职工、管理员等典型业务模块。已有177人学习下载适合毕设选题参考与功能扩展可快速掌握分层架构、数据库访问、权限控制等关键技能。1. 先搞清楚“档案管理”到底管什么再决定怎么写这套 C# 系统单位档案信息管理系统算得上是 C# 里最常见、也最容易做成一堆废代码的题目。单位里合同、资质、人员信息散落在 Excel 和纸质文件里想找一份档案靠记忆想统计到期情况靠手动标记花钱买现成的业务系统又改不动字段。用 C# 写一套桌面端的档案管理系统能把“登记、查找、借阅、归还、到期提醒”这几件事在 Windows 上跑起来。适合谁适合要给单位内部做信息化但没有专职开发团队的人也适合用源码快速改造成自己毕业设计或内部项目的开发者。先说反直觉结论这类系统做得好不好跟 C# 语法水平关系不大跟数据模型的定义和边界关系最大。2. 地基是数据模型为单位档案系统先设计五张能跑十年的表2.1 先定表结构再写代码五张基础表与字段边界如果你习惯从教程里复制代码再改字段我劝你先把表定下来。档案系统后期最痛苦的不是界面丑而是想加一个“保管期限”字段时发现所有报表都要跟着改。常见做法是一开始就把五张基础表建好单位表、档案分类表、档案记录表、用户表、操作日志表它们之间的关系用外键而不是字符串名称做连接。表名用途关键字段org_info单位基本信息org_id, org_name, org_code, contact_person, contact_phone, address, remarkarchive_category档案分类category_id, category_name, parent_idarchive_record档案记录archive_id, org_id, category_id, archive_no, title, storage_location, security_level, borrow_status, file_path, created_by, created_time, update_timesys_user系统用户user_id, username, password_hash, real_name, role_type, is_activesys_log操作日志log_id, user_id, action_type, action_desc, ip_address, log_time为什么要把单位拆成单独一张表而不是在档案表里直接存单位名称因为一个单位会挂多条档案后期统计“每个单位的档案数量”时外键 join 比字符串相等靠谱太多。分类表留 parent_id是为了支持“二级类目”这种最常见的层级不建议一开始就做成无限树除非你确实有需要。下面是实际的建表 SQLCREATE DATABASE UnitArchiveDB COLLATE Chinese_PRC_CI_AS; GO USE UnitArchiveDB; GO CREATE TABLE org_info ( org_id INT IDENTITY(1,1) PRIMARY KEY, org_name NVARCHAR(200) NOT NULL, org_code NVARCHAR(50) NOT NULL UNIQUE, contact_person NVARCHAR(50) NULL, contact_phone NVARCHAR(20) NULL, address NVARCHAR(300) NULL, remark NVARCHAR(500) NULL, created_time DATETIME DEFAULT GETDATE() ); CREATE TABLE archive_category ( category_id INT IDENTITY(1,1) PRIMARY KEY, category_name NVARCHAR(100) NOT NULL, parent_id INT NULL DEFAULT 0 ); CREATE TABLE sys_user ( user_id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, -- password_hash 保存 SHA256 加盐后的结果不是明文 password_hash NVARCHAR(64) NOT NULL, real_name NVARCHAR(50) NULL, role_type TINYINT DEFAULT 1, is_active BIT DEFAULT 1 ); CREATE TABLE archive_record ( archive_id INT IDENTITY(1,1) PRIMARY KEY, org_id INT NOT NULL REFERENCES org_info(org_id), category_id INT NULL REFERENCES archive_category(category_id), archive_no NVARCHAR(50) NOT NULL UNIQUE, title NVARCHAR(200) NOT NULL, storage_location NVARCHAR(200) NULL, security_level TINYINT DEFAULT 0, borrow_status TINYINT DEFAULT 0, borrow_user INT NULL, borrow_time DATETIME NULL, file_path NVARCHAR(300) NULL, created_by INT NULL, created_time DATETIME DEFAULT GETDATE(), update_time DATETIME DEFAULT GETDATE() ); CREATE TABLE sys_log ( log_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NULL, action_type NVARCHAR(50) NOT NULL, action_desc NVARCHAR(500) NULL, ip_address NVARCHAR(50) NULL, log_time DATETIME DEFAULT GETDATE() ); CREATE INDEX IX_archive_org ON archive_record(org_id); CREATE INDEX IX_archive_category ON archive_record(category_id); CREATE INDEX IX_archive_created ON archive_record(created_time);建表逻辑说明如下org_code 是业务编号而不是自增主键比如“91430100MA4LXXXX”必须加上 UNIQUE 约束否则人工录入会重复password_hash 存的是哈希串而不是明文密码这是档案系统最基本的安全底线所有名称字段用 NVARCHAR 而不是 VARCHAR是为了避免中文在不同地区 Windows 上编码不一致而乱码。archive_record 的 org_id、category_id、created_time 各建普通非聚集索引查询档案台账时大概率走索引而不是全表扫描。参数说明role_type 用 TINYINT 而不是字符串权限判断时数字比较快也避免“管理员”和“admin”混着写。is_active 是停用标记而不是删除用户人员离职后保留账号但置 0关联的历史记录不会断。字段可空规则按实际业务定比如联系电话允许空但单位名称和档案编号必须 NOT NULL。2.2 Access 还是 SQL Server小型档案工程别让选型拖垮部署数据访问层没写之前先决定数据库。很多 C# 入门教程和源码包里用 Access 文件.accdb做演示因为不用装数据库服务拷一个文件就能运行适合单机体验。它的问题也很突出多人同时写同一个文件时频繁提示“文件正在使用中”数据量稍大后整表锁死。SQL Server Express 是免费的但需要安装服务和配置连接字符串第一次配可能懵配好之后并发、备份、权限都省心得多。数据库适合场景主要坑Access单机或两三人的轻量台账并发写入锁文件不适合多人同时登记SQL Server Express5-20 个客户端的局域网使用需要装服务连接字符串容易配错SQLite嵌入单机演示并发写能力弱不适合正式档案主库我一般会建议如果只是三个人内部的电子台账Access 也能顶。但只要是“两三个人同时登记档案”的场景Access 锁库就会开始频繁冒出来。所以 C/S 结构的档案系统最稳的做法是直接用 SQL Server Express连接字符串放进 App.config发布后单独改配置文件即可不用动代码。2.3 中文乱码、相对路径与索引建表阶段就要处理的三个坑“乱码是玄学”这句话在档案系统里不成立。乱码的原因通常只有三个数据库排序规则、连接字符串里的编码设置、C# 源文件本身保存时的编码。建表时统一用 NVARCHAR数据库排序规则选 Chinese_PRC_CI_AS源文件统一用 UTF-8 带 BOM 保存这类问题基本能消灭一大半。SQL Server 的连接字符串不需要像 MySQL 那样额外写 CharSet保持默认即可反过来如果你用的是 MySQL就需要在连接串里显式指定 utf8。附件路径是另一个血泪经验不要在数据库里存“D:\档案\xxx.pdf”这类绝对路径而是存相对于程序根目录的路径比如“Data\Archives\2024\GS-2024-001.pdf”。否则程序换电脑、换盘符之后所有附件链接都会失效。索引方面archive_record 的 created_time 字段要建索引因为“按归档日期统计”几乎是所有管理者的第一需求如果档案表日后超过十万条没有这个索引查询会肉眼可见地卡住。3. 把 C# 业务跑起来三层架构下的登录、台账与增删改查3.1 别把代码全塞进 Form.cs三层架构怎么切WinForm 项目最容易变成“一个窗体一个上万字的 cs 文件”。按钮事件里直接写 SQL查询函数到处复制改一个字段名要全局搜索。常见做法是分四层UI 表示层只管显示和按钮BLL 业务逻辑层负责状态规则和权限判断DAL 数据访问层只做 SQL 和数据库交互Models 放实体类公共的配置读取和日志单独放 Common。类库的使用不复杂就是让 UI 引用 BLLBLL 引用 DALUI 不直接引用 DAL。UnitArchive/ ├── UnitArchive.UI // WinForm 窗体工程 ├── UnitArchive.BLL // 业务规则调用 DAL ├── UnitArchive.DAL // SQL 语句与数据库连接 ├── UnitArchive.Models // 实体类对应表结构 └── UnitArchive.Common // 配置、日志、权限上下文为什么禁止 UI 直接访问 DAL因为一旦以后改了表结构或换了数据库不用把几十个窗体全部翻一遍。中间隔一层业务层改动会收敛到少数几个文件里。对于几千条记录的档案系统不用过度设计但“分层”这个动作几乎没有成本建议从一开始就做。3.2 SqlHelper整个数据访问层的公共底座DAL 里我最先写的是一个静态工具类 SqlHelper统一创建连接、执行查询、执行非查询。所有窗体都不直接 new SqlConnection而是调用它。这个类的代码可以直接抄进自己的工程public static class SqlHelper { private static readonly string ConnStr ConfigurationManager.ConnectionStrings[ArchiveDb].ConnectionString; public static DataTable ExecuteQuery(string sql, SqlParameter[] parameters) { using var conn new SqlConnection(ConnStr); using var cmd new SqlCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); using var da new SqlDataAdapter(cmd); var dt new DataTable(); da.Fill(dt); return dt; } public static int ExecuteNonQuery(string sql, SqlParameter[] parameters) { using var conn new SqlConnection(ConnStr); using var cmd new SqlCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } }这里 using var 是 C# 8 的写法在数据量小的 WinForm 项目里用起来很顺手。ExecuteQuery 返回 DataTable界面层直接绑定 DataGridViewExecuteNonQuery 返回影响行数用来判断插入和更新是否成功。唯一的注意点是连接字符串读取失败时要快速暴露最好在静态构造函数里加一个非空判断别让错误等到第一句 SQL 才炸出来。3.3 登录校验与权限上下文密码哈希和当前用户登录模块是几乎所有信息管理系统的入口但我见过太多直接把明文密码拼进 SQL 的样例代码。实际操作时密码字段必须存储哈希值。下面这个 Login 方法演示了参数化查询和哈希校验的常见写法public class UserService { public UserModel Login(string username, string rawPassword) { string salt UnitArchive_2020; string hash ComputeSha256(salt rawPassword); string sql SELECT user_id, username, real_name, role_type FROM sys_user WHERE username username AND password_hash hash AND is_active 1; var param new SqlParameter[] { new SqlParameter(username, SqlDbType.NVarChar, 50) { Value username }, new SqlParameter(hash, SqlDbType.NVarChar, 64) { Value hash } }; var table SqlHelper.ExecuteQuery(sql, param); if (table.Rows.Count 0) return null; return new UserModel { UserId Convert.ToInt32(table.Rows[0][user_id]), Username table.Rows[0][username].ToString(), RealName table.Rows[0][real_name].ToString(), RoleType Convert.ToByte(table.Rows[0][role_type]) }; } private string ComputeSha256(string input) { using var sha System.Security.Cryptography.SHA256.Create(); var bytes System.Text.Encoding.UTF8.GetBytes(input); var result sha.ComputeHash(bytes); return Convert.ToHexString(result); } }逻辑说明加盐是为了防止“同一个密码在任何系统里哈希都一样”带来的批量撞库这里用了固定的盐简单项目够用正式环境建议换成每个用户独立的盐或直接上 BCrypt。登录成功后把 UserModel 放进一个静态类 UserContext后续窗体和按钮权限都从这里取当前用户名和角色。SqlParameter 是防 SQL 注入的关键参数类型和长度要和表定义一致否则可能出现隐式转换或精度截断问题。后面写权限判断时按照 role_type 做开关就行管理员可以增删改普通录入员只能登记和查询只读用户只能看。这个逻辑放进 BLL而不是在窗体里重复判断。3.4 档案台账查询与新增DataGridView 绑定和参数化 SQL台账页是使用频率最高的地方。查询要用参数化动态拼接 SQL不能直接拼接文本框内容。下面这段是最常见的关键字加分类筛选实现public DataTable SearchArchives(string keyword, int? categoryId) { string sql SELECT a.archive_id, a.archive_no, a.title, o.org_name, c.category_name, a.storage_location, a.borrow_status, a.borrow_user FROM archive_record a LEFT JOIN org_info o ON a.org_id o.org_id LEFT JOIN archive_category c ON a.category_id c.category_id WHERE 1 1; var paramList new ListSqlParameter(); if (!string.IsNullOrWhiteSpace(keyword)) { sql AND (a.title LIKE kw OR o.org_name LIKE kw OR a.archive_no LIKE kw); paramList.Add(new SqlParameter(kw, SqlDbType.NVarChar, 100) { Value % keyword % }); } if (categoryId.HasValue) { sql AND a.category_id cat; paramList.Add(new SqlParameter(cat, SqlDbType.Int) { Value categoryId.Value }); } sql ORDER BY a.created_time DESC; return SqlHelper.ExecuteQuery(sql, paramList.ToArray()); }逻辑说明WHERE 11 是动态拼接 WHERE 条件的常见写法让后面的 AND 可以直接追加模糊查询用 LIKE 参数值写成“%关键词%”。注意关键词里如果包含百分号和下划线需要转义否则会被当成通配符。categoryId 用可空整数不选分类时传 null就不会拼上分类条件。返回的 DataTable 直接给 DataGridView 绑定几万条以内的数据响应足够快。新增档案的写法同理但有一个细节值得说明borrow_status 存数字而不是中文0 代表在库、1 代表借出、2 代表待归还。用数字的好处是下拉框和报表统计都能稳定映射不会因为有人录入“已借出”、有人录入“借出中”导致统计出错。创建时间让数据库写 GETDATE()不要拿客户端机器的时间拼进 SQL否则一旦客户端和服务器差几十秒当天归档的边界查询就会出问题。3.5 借阅归还的简单状态机别让状态字段乱跳借阅归还如果只是两行 UPDATE容易遇到“同一份档案被两个窗口同时借出”的竞态。借助状态机这个思路把 borrow_status 当作一个状态机来约束只有状态为 0在库时才能执行借出只有状态为 1借出时才能执行归还。逻辑放在 BLL 层先查状态再更新状态条件更新直接写进 UPDATE 的 WHERE 里。public bool BorrowArchive(int archiveId, int userId) { string sql UPDATE archive_record SET borrow_status 1, borrow_user userId, borrow_time GETDATE() WHERE archive_id archiveId AND borrow_status 0; var param new SqlParameter[] { new SqlParameter(archiveId, SqlDbType.Int) { Value archiveId }, new SqlParameter(userId, SqlDbType.Int) { Value userId } }; return SqlHelper.ExecuteNonQuery(sql, param) 0; }这个写法把“状态必须是从 0 变到 1”的约束放在 UPDATE 里即使两个窗口同时提交也只有一个能影响一行另一个返回 0业务层就可以提示“档案已借出”。归还时同理把 WHERE 改成 borrow_status 1。这个技巧比先 SELECT 再 UPDATE 要省一次数据库往返也彻底消除了判断窗口期。3.6 操作日志写入界面别卡落后台处理操作日志表是最后补也可以、但最好一开始就加的模块。登录成功、新增档案、借出、归还、删除每条核心动作都往 sys_log 写一条。日志写入本身很简单但不要在 UI 线程里同步等待档案系统用户少直接用同步插入也不至于卡顿如果以后人多了就改成 BackgroundWorker 或 ThreadPool 排队写入。写日志时把当前用户名和动作描述作为参数化 SQL 插入禁止把拼好的字符串直接放进去。4. 常见问题排查这套 C# 档案系统最容易翻车的五个环节4.1 中文乱码界面显示正常但数据库变成问号现象C# 程序里输入中文保存后到数据库看到问号或者“锟斤拷”再次读出来就变成乱码。 原因建表时用了 VARCHAR 而不是 NVARCHAR或者数据库默认排序规则不支持中文也可能是插入前字符串已经被错误编码转换过一次。 解决先把表字段统一改为 NVARCHAR/NCHAR再把数据库排序规则设置为 Chinese_PRC_CI_AS最后用一段小查询确认存量数据没有坏掉。如果已经坏了在原始录入还没有被覆盖时赶紧做恢复这是唯一能吃的后悔药。往后写新增代码时所有输入框都直接传 string不要在中间做 Encoding.GetEncoding 之类的手动转换。4.2 同时操作报“文件正在使用中”现象两个人同时打开系统一个人保存时另一个人弹错说数据库文件被占用。 原因Access 是文件型数据库写操作会锁整个文件哪怕一个是查询一个是写入也可能因为文件共享模式互相阻塞。 解决最治本的是把后端换成 SQL Server ExpressAccess 只保留为演示版本。如果短期内换不了库先保证所有客户端都能以独占方式打开文件并在写入前加互斥信号量把并发写串行化。这个办法能缓解问题但不解决根本正式使用建议还是尽快迁移。4.3 按日期查询漏数据时间边界踩坑现象查询某一天归档的档案结果少了几条或多了几条。 原因常见写法是先 ToString(yyyy-MM-dd) 再拼进 SQL再用 BETWEEN 2024-01-01 AND 2024-01-01把当天 23:59 之后的数据全漏掉反过来如果写成 AND 2024-01-02又会多算第二天零点以后的数据。 解决始终用 DateTime 类型作为查询参数查询某一天时使用“大于等于当天 0 点且小于次日 0 点”。这个习惯能消除大量日期类 bug远比在界面层做字符串格式化可靠。4.4 误删档案找不到备份现象操作员发现录入错误直接在台账里删除过几天要找回来发现系统里已经没了。 原因删除操作没有回收站数据库也没有每日备份策略。 解决给 archive_record 表增加 is_deleted 字段查询默认为 0管理界面提供“已删除档案”的恢复入口。同时用 Windows 任务计划每天凌晨执行备份脚本很简单用 sqlcmd 运行 BACKUP DATABASE备份文件名带上日期保留最近三十天。$stamp Get-Date -Format yyyyMMdd sqlcmd -S . -U sa -P your_password -Q BACKUP DATABASE UnitArchiveDB TO DISKD:\backup\UnitArchiveDB_$stamp.bak注意这个脚本放到 Windows 下运行时用 PowerShell 的 Get-Date 格式化日期不要在 cmd 里直接用 bash 的 $(date)否则不会生效。备份恢复是这类系统最重要的兜底措施别等到数据丢了才想起。4.5 发布后找不到附件或没权限现象开发机器上跑得好好的拷贝到单位服务器后附件打不开或是保存报“对路径的访问被拒绝”。 原因代码里写死了开发机的绝对路径或者程序放在 C:\Program Files 下普通用户没有写权限。 解决附件根目录、日志目录都通过 App.config 配置默认取 AppDomain.CurrentDomain.BaseDirectory 下的 Data 目录程序启动时先检查目录是否存在不存在就创建创建失败则弹出明确提示。路径拼接统一用 Path.Combine不要用手工拼接的字符串避免盘符和分隔符在不同机器上出问题。这一条几乎能排掉一半的新手部署事故。5. 交付前的最后一步十分钟验证与可维护配置5.1 用一条主链路跑通验收部署完成后我习惯按主链路走一遍管理员登录新增一家单位登记一份带附件的档案在台账中搜索到它双击打开详情执行借出执行归还查看操作日志确认每一步有记录最后确认备份目录已生成文件。这 9 步一次通过核心业务就是通的剩下的是美化。很多项目交付后反复返工都是卡在“能登录”到“能登记”的中间环节大多是权限或外键关联没配对。5.2 发布时给连接字符串留后门用 XCOPY 或 ClickOnce 发布都行但连接字符串不要编译死在程序集里。把 SQL Server 实例名、账号、密码放到 App.config发布后用文本编辑器就能修改。给不会改配置的用户做一个“数据库设置”小窗体首次启动引导填写服务器地址和账号比让用户打开 xml 改配置更友好。这个设定在工程里成本极低却能把维护电话减少一大半。我做这类系统最后悔的事就是第一版把权限判断写死在各个窗体的 Load 事件里后面每次加窗体都要重发版本翻车好几次。后来才把所有权限判断收敛到公共方法里按角色统一控制按钮和窗体可见性。C# 档案管理这个方向本身不复杂复杂的是数据边界、状态流转和部署后的可维护性。希望这些步骤和教训能帮你绕过我踩过的坑按这个顺序做至少不会在第一周就被数据问题追着跑。希望帮到你。本文还有配套的精品资源点击获取