零基础搭建单机版档案系统保管期限管理功能
一、开发环境准备
在开始编写单机版档案软件的保管期限功能之前,必须确保本地开发环境已经配置妥当。本指南采用 Python 3.8+ 作为开发语言,SQLite 作为本地数据库,无需安装任何额外的数据库服务,真正做到零依赖、单机运行。
请打开终端或命令提示符,输入以下命令检查 Python 版本:
```bash python --version ```如果未安装 Python,请前往 Python 官网下载并安装 3.8 或更高版本。本案例不依赖第三方库,直接使用 Python 标准库即可完成所有开发工作。
二、数据库表结构设计
实现档案保管期限管理的核心在于数据库设计。我们需要两张表:retention_periods(保管期限表)和 archives(档案表)。SQLite 数据库文件将直接存储在项目根目录下,命名为 archive_system.db。
请在项目目录下创建一个名为 init_db.py 的文件,并写入以下完整的建表 SQL 语句。这些语句定义了字段类型、主键以及表之间的关联关系。
```python import sqlite3 def init_database(): 连接到SQLite数据库,文件不存在会自动创建 conn = sqlite3.connect('archive_system.db') cursor = conn.cursor() 1. 创建保管期限表 code: 期限代码,如 'Y' (永久), '30' (30年) years: 保管年限,永久则存 NULL 或 -1,这里用 NULL 表示永久 cursor.execute(''' CREATE TABLE IF NOT EXISTS retention_periods ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, years INTEGER ) ''') 2. 创建档案表 retention_id: 关联保管期限表的外键 create_date: 档案归档日期,用于计算到期时间 cursor.execute(''' CREATE TABLE IF NOT EXISTS archives ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, file_number TEXT, retention_id INTEGER, create_date TEXT NOT NULL, FOREIGN KEY (retention_id) REFERENCES retention_periods (id) ) ''') 3. 初始化基础保管期限数据 插入常用的保管期限:永久、30年、10年 cursor.execute("INSERT OR IGNORE INTO retention_periods (code, name, years) VALUES ('Y', '永久', NULL)") cursor.execute("INSERT OR IGNORE INTO retention_periods (code, name, years) VALUES ('30', '30年', 30)") cursor.execute("INSERT OR IGNORE INTO retention_periods (code, name, years) VALUES ('10', '10年', 10)") conn.commit() conn.close() print("数据库初始化完成,表结构及基础数据已创建。") if __name__ == '__main__': init_database() ```运行上述脚本完成数据库的初始化:
```bash python init_db.py ```三、核心功能代码实现
接下来编写核心业务逻辑。我们将创建一个 archive_manager.py 文件,实现三个关键功能:档案录入(绑定保管期限)、档案列表查询(显示保管期限和到期时间)以及 到期档案筛选。
以下代码包含完整的日期计算逻辑和异常处理,可直接复制使用:
```python import sqlite3 from datetime import datetime, timedelta def get_connection(): return sqlite3.connect('archive_system.db') def add_archive(title, file_number, retention_code, create_date_str): """ 录入档案并自动关联保管期限 """ try: conn = get_connection() cursor = conn.cursor() 1. 根据代码查询保管期限ID cursor.execute("SELECT id, years FROM retention_periods WHERE code = ?", (retention_code,)) retention = cursor.fetchone() if not retention: print(f"错误:未找到代码为 {retention_code} 的保管期限。") conn.close() return retention_id, years = retention 2. 插入档案数据 cursor.execute(''' INSERT INTO archives (title, file_number, retention_id, create_date) VALUES (?, ?, ?, ?) ''', (title, file_number, retention_id, create_date_str)) conn.commit() conn.close() print(f"成功录入档案:{title}") except Exception as e: print(f"录入失败: {e}") def list_all_archives(): """ 查询所有档案,计算并显示到期时间 """ conn = get_connection() cursor = conn.cursor() 联表查询:获取档案详情及对应的保管期限信息 cursor.execute(''' SELECT a.id, a.title, a.file_number, a.create_date, r.name, r.years FROM archives a JOIN retention_periods r ON a.retention_id = r.id ORDER BY a.create_date DESC ''') rows = cursor.fetchall() conn.close() print(f"\n{'ID':<5} {'标题':<20} {'档号':<15} {'归档日期':<12} {'保管期限':<10} {'到期时间'}") print("-" 80) for row in rows: arch_id, title, file_num, create_date, ret_name, years = row 计算到期时间 expiry_date = "永久" if years is not None: try: 将字符串日期转换为日期对象进行计算 base_date = datetime.strptime(create_date, "%Y-%m-%d") 加上年份 target_date = base_date + timedelta(days=years 365) expiry_date = target_date.strftime("%Y-%m-%d") except ValueError: expiry_date = "日期格式错误" print(f"{arch_id:<5} {title:<20} {file_num:<15} {create_date:<12} {ret_name:<10} {expiry_date}") print("-" 80) def check_expired_archives(check_date_str=None): """ 检查已到期的档案 check_date_str: 指定检查的基准日期,默认为今天 """ if not check_date_str: check today = datetime.now().strftime("%Y-%m-%d") else: check today = check_date_str conn = get_connection() cursor = conn.cursor() 获取所有非永久保管的档案 cursor.execute(''' SELECT a.id, a.title, a.create_date, r.years FROM archives a JOIN retention_periods r ON a.retention_id = r.id WHERE r.years IS NOT NULL ''') rows = cursor.fetchall() conn.close() expired_list = [] try: current_date = datetime.strptime(check_today, "%Y-%m-%d") for row in rows: arch_id, title, create_date, years = row base_date = datetime.strptime(create_date, "%Y-%m-%d") 计算到期日期 expiry_date = base_date + timedelta(days=years 365) if expiry_date <= current_date: expired_list.append((arch_id, title, expiry_date.strftime("%Y-%m-%d"))) except ValueError: print("日期格式错误,请使用 YYYY-MM-DD") return if expired_list: print(f"\n=== 截止 {check_today} 已到期档案列表 ===") for item in expired_list: print(f"ID: {item[0]}, {item[1]}, 到期日: {item[2]}") else: print("\n暂无到期档案。") 简单的命令行交互菜单 def main(): while True: print("\n1. 录入档案") print("2. 查看所有档案") print("3. 检查到期档案") print("4. 退出") choice = input("请输入操作编号: ").strip() if choice == '1': title = input("档案 ") file_num = input("档号: ") print("可选期限代码: Y(永久), 30(30年), 10(10年)") ret_code = input("保管期限代码: ").strip() c_date = input("归档日期(YYYY-MM-DD): ").strip() add_archive(title, file_num, ret_code, c_date) elif choice == '2': list_all_archives() elif choice == '3': check_expired_archives() elif choice == '4': break else: print("输入无效,请重新输入。") if __name__ == '__main__': main() ```四、系统运行与实操验证
代码编写完成后,直接运行 archive_manager.py 启动单机版系统:
```bash python archive_manager.py ```系统启动后会显示一个简单的交互菜单。以下是具体的操作步骤,用于验证保管期限逻辑的准确性:
步骤 1:录入不同期限的档案

在菜单中选择 1,按照提示依次录入以下测试数据:
- 档案 A:标题“2020年度财务报表”,档号“CW-2020-001”,期限代码“10”,归档日期“2020-01-01”。
- 档案 B:标题“公司成立章程”,档号“XZ-001”,期限代码“Y”,归档日期“2010-01-01”。
- 档案 C:标题“2010年合同副本”,档号“HT-2010-055”,期限代码“10”,归档日期“2010-05-20”。
注意: 在输入日期时,必须严格遵循 YYYY-MM-DD 格式,否则程序会报错。这是单机版软件数据校验的基础。
步骤 2:查看档案列表与自动计算
在菜单中选择 2。系统将列出所有档案,并自动计算“到期时间”。
- 对于档案 A(10年期限),系统应显示到期时间为 2030-01-01。
- 对于档案 B(永久期限),系统应显示到期时间为 永久。
- 对于档案 C(10年期限),系统应显示到期时间为 2020-05-20。
此步骤验证了数据库关联查询以及 Python 端的日期加法逻辑是否正确执行。
步骤 3:验证到期筛选逻辑
在菜单中选择 3。系统默认以当前系统日期为基准进行比对。
- 如果当前日期已过 2020-05-20,档案 C 将出现在“已到期档案列表”中。
- 档案 A 和 档案 B 不会出现在列表中(A 未到期,B 为永久)。
为了测试特定日期的到期情况,可以修改代码中的 check_expired_archives 调用部分,传入一个指定日期字符串,例如 '2025-01-01',再次运行即可看到筛选结果的变化。
五、常见问题处理
在实操过程中,可能会遇到以下两个典型问题,解决方案如下:
1. 数据库被锁定
如果程序异常退出,可能会留下数据库锁文件。解决方法是删除项目目录下的 archive_system.db-journal 文件,或者重启电脑释放文件句柄。
2. 日期格式不匹配
SQLite 本身不强制日期格式,但 Python 代码中使用了 datetime.strptime 进行解析。如果输入“2020/01/01”会导致解析失败。务必保证输入格式包含连字符“-”。