
单位不大档案却不少。这句话大概是很多行政、人事、综合办公室同事的共鸣。我们单位不到一百号人但历年积压下来的合同、红头文件、人事材料、设备资料、验收报告堆了好几个铁皮柜电脑里还有个共享盘里面是几代人攒下来的Word、PDF、扫描件命名规则全靠个人自觉目录结构换一个人就变一套。每次要找个东西先问一圈人再翻半小时柜子运气好能找到电子版运气不好只能对着复印件猜。我算是单位里唯一会写点代码的人去年年底实在被找档案这件事折腾烦了决定自己做一套档案管理系统。正好那阵子我在试用各种AI编程助手就想着拿这个项目当实战试验田。从梳理需求到设计表结构从写后端接口到做检索和借阅功能最后部署到单位内网前后大概花了两周业余时间。过程里踩了不少坑也总结出一套“AI辅助开发”的正确用法这篇就当是一个完整复盘。这套系统的目标很明确给几十到几百人的小单位用没有专职IT没有预算买商业OA但又不想继续用Excel和文件柜硬扛。下面我按“为什么这么做、怎么做、做成什么样、踩了什么坑”的顺序把整个项目拆开讲清楚。1. 项目缘起小单位档案管理到底卡在哪里1.1 我先描述一下真实的场景很多小单位的档案工作表面上有制度实际上靠的是“某个人靠谱”。归档靠自觉检索靠记忆借阅靠口头打招呼交接靠对方心情。我观察到的典型痛点有几个纸质档案和电子档案双轨并行但两套目录对不上。文件柜里的合同编号和共享盘里的文件夹命名各成体系找档案必须先判断“当时那人按什么规则放的”。Excel名录流于形式。台账确实有但更新不及时字段随意填“备注”栏全凭个人发挥查的时候等于猜谜语。借阅完全没有记录。上级检查要借台账财务要借凭证走廊里喊一嗓子就拿走了过三个月想起来问对方说“早还了吧”。人员离职是重灾区。档案管理员一换新人只能靠同事口述来熟悉历史材料很多归档逻辑从此变成黑历史。这些痛点的共同根源不是“档案太多管不过来”而是“没有一个稳定、低门槛的载体来承载归档、检索、借阅这三件基本事”。小单位真不需要什么智能鉴定、全生命周期管理、电子签章先把“能不能在五分钟内找到一份三年前的合同”这个最基本的需求解决了系统的价值就立住了。在这个背景下我决定自建系统。考虑到单位实际条件需求必须克制界面要简单到行政大姐能直接上手部署要能在一台旧电脑上完成数据要全部留在单位内网最好不需要IT团队维护。1.2 把“够用”这两个字掰开揉碎明确需求边界是这个项目里最重要的一件事。我一开始差点掉进功能堆砌的陷阱因为AI太容易给你生成一堆看似有用的模块了比如公文流转、线上审批、多级部门协同、电子水印……但我问了自己两个问题单位现在最疼的是哪三件事我维护得起多大复杂度答案很快浮出水面第一档案目录统一化第二检索快速准确第三借阅有记录可追溯。至于权限只需要管理员、录入员、普通查看者三个角色。至于统计报表能按分类和时间段出数量就够了不需要花哨的仪表盘。这么做的好处是整个系统从数据库到页面都保持“细长条”的状态学习成本半小时内能搞定一个人能在两周内把代码写完并部署起来。说实话小单位的数字化的核心不是“强大”而是“可持续”——如果系统维护成本超过了人工翻柜子的成本那它迟早会被淘汰。2. 技术方案选型为什么是AI辅助加轻量Web开发2.1 先定原则能用轻方案不用重平台立项之前我认真比较了几条路线买现成的档案管理系统、用钉钉/企微自带的应用搭个表单、自研一套Web应用。买现成系统的方案最先被否。市面上正规档案软件确实成熟但报价基本都要几万起而且很多功能模块小单位根本用不上后期升级、维护、培训都是成本。钉钉和企微虽然能快速搭表单和审批流但档案文件本身还是散落在云盘或者聊天记录里没有真正解决“集中可控”的问题而且数据都在第三方平台心里总不踏实。最后只能自研。自研的框架选型也纠结了一阵。我自己的主力语言是Python所以Django和Flask都在候选里。最终选了Flask原因很直接Django自带Admin、ORM、中间件这些东西很全但对于我这种要精确控制逻辑、还打算让AI辅助生成大量代码的项目来说Flask更轻、更透明每一段代码我都能快速看懂。配套的数据库本来想直接用MySQL但考虑到部署环境是单位一台普通办公电脑我最后定的是SQLite起步前端不引Vue全家桶就用Flask模板加一点原生JavaScript减少构建环节。选完这套组合我才发现它和AI辅助开发的匹配度极高。因为技术栈足够主流、足够简单AI编程助手对这些框架的代码模式非常熟悉生成的代码质量普遍靠谱而且我容易判断对错。要是用冷门框架或者上微服务AI生成的东西我都不敢信。2.2 数据存储SQLite起步别一开始就上MySQL很多有后端经验的人看到SQLite第一反应是“这玩意儿能上生产吗”。在小单位内部系统这个场景下SQLite非但能用而且是非常合理的方案。它的核心优势是零配置、单文件、随拷贝随走。单位档案系统跑在一台内网Windows机器上不需要单独安装数据库服务程序放哪数据就在哪备份直接复制文件就行。容量方面也不用担心。SQLite的官方定位是单机嵌入式数据库但对小单位档案这个数据量来说绰绰有余——就算你一万名员工每人十份档案也不过十万条记录SQLite单文件撑个几十GB完全没问题而一个小单位十几年的档案撑死也就几万个文件、几十GB。要做的优化其实很少关键是打开WAL模式。SQLite默认的rollback journal模式在读写并发高时容易出现database is locked单位里虽然人不多但几个员工同时录入档案、检索是很正常的。切换到WAL之后读写可以并行基本不会再遇到锁问题。代码就一行PRAGMA journal_modeWAL;等到哪天单位规模真的大到SQLite撑不住了再换成MySQL也不迟。Flask的SQLAlchemy ORM在这层做了很好的隔离切换数据库只需改个连接串大部分代码不用动。所以没必要一开始就引入数据库服务器的运维负担。2.3 AI在整个项目里到底干了什么这是我特别想分享的部分。AI在我这个项目里远不止“写代码”这一件事它几乎参与了全流程但每个环节的分工不一样工作环节我用AI做的事人工复核与决策的重点需求梳理让AI列举小单位档案管理常见的痛点、功能清单、流程节点筛选哪些功能真的符合本单位现状砍掉伪需求数据库设计让AI根据普通常见档案场景生成表结构SQL优化字段核对字段完整性、索引合理性确认归档粒度业务代码生成档案增删改查、上传下载、借阅登记等后端接口逐一审查权限校验、文件路径处理、异常分支前端页面生成列表页、表单页、简单统计图的HTML和原生JS调整交互逻辑确保非技术同事看得懂、点不错OCR辅助用AI模型工具识别扫描文件的标题、文号、落款日期等抽检识别准确率修正字段映射规则问题排查把报错日志、异常现象发给AI让它给排查方向根据项目实际代码做最终定位避免被AI带偏整个项目做下来我的体感是AI帮我省掉了至少60%的“搬砖时间”比如重复的CRUD代码、表单校验、基础SQL语句这些无脑但耗时的工作。但它不懂业务边界也不懂单位内部的人情世故比如“某科室只允许查自己部门的档案”这种说不清道不明的规则必须靠人来定义。所以我的原则一直是AI写初稿我写终稿。所有上线跑的代码我都逐行读过。3. 系统设计落地从数据库到核心功能3.1 数据模型设计一个合理的起点档案管理系统的数据模型核心其实只有四张表用户表、档案分类表、档案主表、借阅记录表。我先把建表SQL放出来然后逐张解释为什么这样设计-- 用户表 CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, real_name TEXT, role TEXT DEFAULT viewer, -- admin / operator / viewer department TEXT, is_active INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 分类表支持两级结构用 parent_id 关联 CREATE TABLE categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, code TEXT UNIQUE NOT NULL, -- 如 HT合同、SW收文、RS人事 parent_id INTEGER DEFAULT 0 ); -- 档案主表 CREATE TABLE archives ( id INTEGER PRIMARY KEY AUTOINCREMENT, archive_no TEXT UNIQUE NOT NULL, -- 档案编号如 GS-HT-2025-0012 title TEXT NOT NULL, -- 题名 category_id INTEGER NOT NULL, -- 分类外键 department TEXT, -- 形成部门 archive_date TEXT, -- 成文日期 operator TEXT, -- 归档人 file_path TEXT NOT NULL, -- 相对存储路径 original_filename TEXT, -- 原始文件名 file_type TEXT, -- pdf / jpg / doc / xls 等 file_size INTEGER, -- 字节数 ocr_meta TEXT, -- 存放 OCR 抽取的 JSON 元数据 status TEXT DEFAULT in_storage, -- in_storage / borrowed / destroyed borrow_status TEXT DEFAULT 0, -- 0 未借出 / 1 已借出 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 借阅记录表 CREATE TABLE borrow_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, archive_id INTEGER NOT NULL, borrower_name TEXT NOT NULL, borrower_dept TEXT, borrow_date TEXT NOT NULL, expected_return_date TEXT, actual_return_date TEXT, status TEXT DEFAULT borrowed, -- borrowed / returned / overdue note TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_archives_category ON archives(category_id); CREATE INDEX idx_archives_title ON archives(title); CREATE INDEX idx_archives_no ON archives(archive_no); CREATE INDEX idx_borrow_status ON borrow_records(status);几个关键的设计决策值得展开讲。archive_no采用“单位简称-分类码-年份-四位顺序号”的结构比如GS-HT-2025-0012。这个编号不只是给人看的它本身就是检索的关键词。生成规则是每次新登记档案时在同一个分类、同一年份下找到当前最大顺序号加一后再补零。这段逻辑必须放在数据库事务里处理避免两个人同时录入时生成了重复编号。file_path存储的是相对路径而不存绝对路径。这一点是我踩过一次坑换来的教训程序部署的盘符可能换服务器路径可能变一旦把绝对路径入库迁移环境时就是灾难。我最终确定的文件组织方式是uploads/分类码/年份/档案编号/原始文件名这样每个档案一个目录将来即使数据库坏了光看文件目录也能大致还原元数据。ocr_meta字段存JSON字符串很多传统表设计会把OCR识别的标题、文号、日期、责任者分别建列但我选择合并到一个JSON字段。原因是这些字段不一定每份档案都有而且以后想增加抽取维度不用改表结构。需要查询时直接读出来反序列化即可对检索关键字段影响不大。3.2 核心功能拆解录入、检索、借阅、统计系统的功能块我压到了最低限度但每个功能块都做得扎扎实实。录入是最核心的操作。我给录入员设计了三种入口第一种是页面手工录入表单包含必填项和选填项上传文件后自动把文件移动到归档目录第二种是Excel批量导入适合补录历史台账模板固定上传后用Pandas逐行读取并生成记录第三种是AI辅助的OCR识别录入扫描件上传后自动抽取题名、文号、日期辅助录入员填写减少打字量。第三种我后面单独讲。检索是使用频率最高的功能。界面就一个大搜索框加上分类、部门、时间段的筛选项。搜索范围包括档案编号、标题、形成部门、备注关键词不启动正文全文检索理由后面说。结果列表按时间倒序排列点进去可以看详细信息、下载文件、发起借阅。借阅管理做成一个最简单的闭环登记借出、到期提醒、归还销账。到期提醒不通过邮件因为单位内网没有外部邮件服务我直接在首页顶部和列表页显示红色逾期条目让管理员每天开机第一眼就能看到。再配一个按钮“一键生成逾期清单Excel”方便管理员打印或转发到工作群。统计报表只做了三类按分类的档案数量分布、按部门的归档数量、借阅次数Top10。图表没有用第三方JS库就是后端算好数据后在页面上用CSS画横向条形图简单到不需要刷新就能看明白。3.3 权限设计三级角色一段装饰器代码权限设计我坚持了最小化原则三级角色就够了admin管理员拥有全部权限能管理用户和分类operator录入员能录入档案、编辑档案、办理借阅登记和归还viewer查看者只能检索、查看详情、下载文件不能做任何增删改。Flask后端处理权限的办法很多我用了最简单的装饰器from functools import wraps from flask import session, abort def require_role(*roles): def decorator(fn): wraps(fn) def wrapper(*args, **kwargs): role session.get(role) if role not in roles: abort(403) return fn(*args, **kwargs) return wrapper return decorator app.route(/archives/new, methods[GET, POST]) require_role(admin, operator) def archive_new(): ...这里有一个特别重要的安全细节文件下载接口必须做权限校验不能简单地用send_from_directory挂载整个upload目录。如果直接把静态目录映射出去那所有知道URL的人都能绕过登录下载全部档案。我的下载接口是这样写的前端传档案ID后端先去数据库查出file_path再校验当前用户角色最后才通过send_from_directory发送文件。用户密码的存储用的是Flask自带werkzeug.security的generate_password_hash不要用MD5、不要用SHA1这是底线。另外所有录入、修改、借出的关键操作都写了一条审计日志记录操作人、操作时间、操作内容。这件事成本极低但价值极高将来真有权限纠纷时能拿得出依据。4. 关键实现与AI辅助编码实录4.1 档案编号生成规则一个容易写错的事务档案编号看起来是个小事但实现细节很讲究。我的规则是同一分类下、同一年的编号连续递增中间删除档案也不能补出断号否则账目对不上。生成逻辑放在数据库事务里执行def gen_archive_no(category_code, year): prefix fGS-{category_code}-{year}- with db_session() as db: # 查询当前分类、当前年份的最大编号 last db.execute( SELECT archive_no FROM archives WHERE archive_no LIKE ? ORDER BY archive_no DESC LIMIT 1, (prefix %,) ).fetchone() if last: seq int(last[0].split(-)[-1]) 1 else: seq 1 return prefix str(seq).zfill(4)这个方法有两个要点第一查询和写入必须在同一个事务里完成否则并发录入时会生成重复编号第二用LIKE prefix%而不是单独存一个序列号字段是为了避免额外维护一张“编号计数器表”。小单位录入频率不高这种方案完全够用。4.2 让AI帮我做OCR元数据抽取这是整个项目里“AI含量”最高的一块也是我从“用AI写代码”到“让AI作为能力组件”的转折点。背景是这样单位历史档案里有大量扫描版PDF和照片比如上世纪的红头文件、早期合同的扫描页。这些文件在电脑里存了多年但文件名全是IMG_20140315_001.jpg这种不打开根本不知道里面是什么。人工一张张看、一条条录效率太低。我的方案是上传文件后先做OCR文字识别再用提示词让大模型从识别文本中抽取出“题名、文号、成文日期、责任者”这几个关键字段自动填到录入表单里。识别环节我本地部署了PaddleOCR。选择它而不是调用云服务API主要考虑是档案内容涉及单位内部信息不适合出内网。PaddleOCR对中文印刷体和手写体的识别效果都不错配置也不算复杂。核心代码长这样from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def ocr_pdf_page(pdf_path, page_index0): from pdf2image import convert_from_path images convert_from_path(pdf_path, dpi150, first_pagepage_index1, last_pagepage_index1) img images[0] result ocr.ocr(np.array(img), clsTrue) text \n.join([line[1][0] for line in result[0]]) return textOCR出来的文本经常是一大堆连贯的字符串格式杂乱。真正抽字段的活我交给了AI大模型。给大模型的提示词结构我是反复调过的最终版是这样你是档案信息抽取助手。给定一段OCR文本从中提取以下字段并严格输出JSON格式 - title文件题名如“关于开展2025年档案安全检查的通知” - doc_no文号如“XX办〔2025〕3号”没有则为空字符串 - date成文日期格式YYYY-MM-DD没有则为空字符串 - author发文单位/责任者没有则为空字符串 - keywords不超过5个关键词用数组表示 要求只能从给定文本中提取信息不要编造无法识别的字段填空字符串不要输出任何解释性文字。模型的输出用json.loads解析后回填到表单录入员只需要核对一遍、改改错字就能保存。实测下来的准确率大概在八成以上主要是OCR本身造成的错别字会影响字段抽取比如把“三月”识别成“3月”还好把“某局”识别成“某某局”就得人工纠正。这个功能把录入效率提升了两倍以上原来录入一份扫描件要两分钟现在半分钟搞定AI抽字段这部分是真的省力。4.3 检索模块为什么我不做全文检索检索是档案系统的门面但我在全文检索这个点上做了取舍。网上的教程都喜欢炫技动不动就是Elasticsearch或者FTS5全文索引。我的需求里90%的检索场景是“按标题关键词找文件”、“按编号找文件”、“按部门加时间段翻目录”这些操作用普通的LIKE加索引就能在几十毫秒内返回。我最终用的是这种组合在标题、档案编号、形成部门、关键词这几个字段上做LIKE %关键词%再加分类、年份、部门下拉筛选。全程不用全文索引也不引入ES维护成本为零。只有当用户输入的内容命中分类下拉时我会额外加一条category_id ?条件来收窄范围。为什么不做FTS5因为SQLite内置的FTS5对中文支持不友好默认的unicode61分词器对中文是按“连续汉字串”整体切词的要实现真正可用的中文全文检索还得挂Jieba分词插件复杂度上去了收益却不高。档案检索的典型行为是回忆“这个名字里有个词”而不是搜正文里的某个句子。如果真的需要搜正文内容我可以把OCR的全文文本存进一个额外字段然后用LIKE去查——数据量小的时候这个粗暴方案反而最可靠。4.4 备份与恢复整个系统里最重要的一环档案系统的数据价值远高于代码本身所以备份是我反复强调的重中之重。我做了两层备份数据库备份和文件目录备份。数据库备份这块很多人直接cp data.db backup.db这其实有风险。SQLite在写入过程中直接拷贝文件可能拷到一半提交的脏状态导致备份文件打不开。正确的做法是用SQLite自带的在线备份接口sqlite3 /path/to/archive.db .backup /path/to/backup/archive_backup.db文件目录备份就更简单了把uploads目录整个同步过去。Windows环境我用robocopy命令Linux环境用rsync -av。定时任务方面Windows用任务计划程序Linux用cron每天凌晨两点跑一次。定时备份脚本的参考实现Linux下的cron脚本#!/bin/bash BACKUP_DIR/data/archive_backup DATE$(date %Y%m%d) sqlite3 /opt/archive/data.db .backup $BACKUP_DIR/data_$DATE.db rsync -av /opt/archive/uploads/ $BACKUP_DIR/uploads/ find $BACKUP_DIR -name data_*.db -mtime 30 -delete备份策略上我采用的是天级全量加30天滚动保留对小单位够用了。另外一定要做恢复演练不要备份完就万事大吉。我自己的流程是每月第一个周末把备份文件恢复到一台测试机上随机抽三份档案打开看看能不能正常下载。这个动作花不了十分钟但能在灾难真发生时救命。5. 上线过程、问题排查与经验总结5.1 部署一台旧电脑就够了部署环境我选的是单位里一台配置很普通的旧办公电脑Windows 10系统i3处理器4G内存硬盘500G。数据库和文件都放在本机通过内网IP提供给全单位访问。Windows下启动Flask应用直接用Flask自带的开发服务器不适合长时间运行我用了waitress这个纯Python WSGI服务器线程池模式对小并发够用且安装简单from waitress import serve from app import app serve(app, host0.0.0.0, port8080, threads8)启动之后局域网同事在浏览器输入http://192.168.x.x:8080就能访问。如果遇到别人访问不了先查Windows防火墙是否放行了8080端口netsh advfirewall firewall add rule nameArchiveSys dirin actionallow protocolTCP localport8080整个部署过程不到半小时。有一点需要提醒最好给这台机器设一个固定的内网IP不然路由器重启后IP变了全单位的人就都打不开了。5.2 我踩过的坑和排查记录速查表开发过程中积累了不少问题挑典型整理成表以后自己排查也方便希望对正在做类似系统的人有帮助。故障现象可能原因解决方法上传中文文件名后下载变成乱码浏览器解析Content-Disposition头时对中文处理不同下载响应头使用filename*UTF-8格式编码上传大PDF时浏览器一直转圈Flask默认MAX_CONTENT_LENGTH未设置或前端没做限制后端限制50MB前端用JS提前检查文件大小SQLite报database is locked默认日志模式在并发写入时锁库开启WAL模式设置timeout30AI生成的下载接口不需要登录就能访问权限装饰器漏加在下载路由上人工审查所有路由逐个确认权限装饰器OCR结果里日期字段年年出错扫描件分辨率太低特别是老文件扫描时固定150dpi以上OCR前做灰度化和二值化备份恢复后新录入档案编号重复恢复时覆盖了正在生产环境的数据库且计数器未同步恢复前先备份现场库恢复后检查最大编号并手动更新单位同事访问浏览器看不到样式Flask默认静态目录路径没配好确认static_folder路径使用url_for(static, filename...)有档案删了但磁盘空间没释放只是删了数据库记录没删物理文件删除时同步删除文件定期清理孤儿文件这些坑里最值钱的教训是第一条和第三条。中文乱码问题几乎每个内网系统都会遇到下个文件叫“合同扫描件.pdf”结果浏览器提示下载“????.pdf”这体验太掉份了。正确做法是后端设置响应头时使用RFC 5987格式from urllib.parse import quote resp.headers[Content-Disposition] fattachment; filename*UTF-8{quote(filename)}5.3 经验总结AI辅助开发的正确姿势项目做完之后体感最深的一点AI辅助开发真正的价值不在“让AI替你思考”而在“把AI当成一个执行力极强的实习生”。你给它清晰的需求、明确的输入输出、足够的上下文它才能交出可用度高的成果。反过来如果你自己都不清楚要什么问一句“帮我做个档案系统”那AI只会给你一堆华而不实的通用代码。这套工作流我沉淀成一个模板无论是开发还是给非程序员同事做分享时都用得上写清楚角色和目标。比如“你是一名熟悉Flask的Python开发工程师请帮我实现一个档案上传接口”。给出具体的输入输出格式。比如“请求方法POST字段包括file和title返回JSON包含archive_no和message”。给足边界约束。比如“文件只允许pdf/jpg/png大小不超过50MB文件重命名用uuid加原始后缀”。让AI补充异常分支。比如“请检查如果数据库连接失败应该返回什么错误”。最后人工审查与测试一步都不能少。另外还有一个技巧可以分享写完一个模块后让AI生成“异常场景测试清单”。比如我让AI列出“档案管理系统可能存在哪些越权访问场景”它列了十来条包括viewer角色直接调用admin接口、批量导入时上传恶意文件名、伪造档案编号查询等。我把这些清单当作自测用例逐个在本地环境跑了一遍还真修掉两个隐患。这个方法比让AI直接写测试用例更实用因为AI对“系统的阴暗面”有很强的列举能力它虽然不完美但能帮你补上很多盲区。至于后续扩展我已经想好了几个方向但暂时不着急做给借阅到期加一个企业微信或钉钉机器人提醒把Excel批量导入界面做成可视化字段映射以及把OCR录入流程的参数调整做成一个可视化的配置面板。核心原则还是那句话按需生长不为炫技加功能。这套系统目前已经在单位稳定跑了三个月每天都有同事在用。对我来说它真正解决的不是“档案管理”这个宏大命题而是让“找档案”这件事从玄学变成了检索——这就够了。