简介:河南省在建工程技术资料档案管理系统操作手册以PDF格式呈现,面向建筑施工企业的档案录入人员、项目工程师与项目经理,也适合需要熟悉工程资料归档流程的技术管理人员查阅。手册围绕资料收集、整理、录入到归档的完整链条展开,重点讲清各方职责划分与操作规范,帮助使用者解决案卷著录项填写不统一、组卷口径不一致等实际问题。整包仅含1个PDF文件,约35KB,便于在电脑或手机上留存查阅。目录依次覆盖职责说明、系统实施流程、具体资料录入操作、案卷与卷内各著录项填写规范、录入人员设置及工程技术资料组卷要求,并附建筑工程技术资料目录清单,对案卷号、密级、起止日期、部门、分类号、保管期限、卷内顺序号等字段逐条给出填写口径,标注「★」的资料需挂接原文彩色扫描件。目前已有90人学习,读者可据此对照项目实际,厘清从预归档到正式归档的操作要点,提高资料录入的规范性与检索效率。
1. 从验收前找一份检验批说起:河南省在建工程技术资料档案管理系统到底管什么
验收前三天,项目部最常出现的场景不是资料写不出来,而是"知道有这份资料,但不知道在谁电脑里、在第几卷、页码对不对"。河南省在建工程技术资料档案管理系统要解决的正是这个:它不把资料当普通文件堆在服务器上,而是从一份检验批记录、一张混凝土试块报告产生的当天,就把它挂到"单位工程—分部—分项—检验批"的固定节点上,同时记下编号、日期、责任人、监理签认状态。
系统真正管的是四件事:资料落在哪个目录节点、文件本体的元数据是什么、当前处于编制还是已归档状态、谁能改谁只能看。读这份操作手册落地的人分两类,一类是资料员和项目总工,按章节顺序把资料录进去;另一类是做系统实施和二次开发的工程师,需要把纸质用表、卷内目录、移交清单翻译成表结构和校验规则。前者关心"这一步点哪里",后者关心"这一步背后落了哪些字段、校验拦在哪一层"。后面几章按这个顺序展开。
2. 在建工程技术资料档案管理系统的数据模型怎么定
2.1 工程—案卷—卷内文件—元数据:四层结构为什么比文件夹树好用
很多项目最初就是用共享盘加文件夹做归档,前两个月很顺,到中期开始崩。原因是文件夹只有"位置"一个维度:一份文件想同时属于"3 号楼"和"混凝土工程"和"2024 年 5 月",只能靠一层层的名字拼,一旦有人改了目录名或者把文件拖走,历史不可追溯,权限也只能按目录粗粒度给。把分类维度和存储维度拆开,用四层结构落库,后面所有检索、统计、移交都从这里长出来。
| 层级 | 对应实体 | 关键字段 | 关系 |
|---|---|---|---|
| 工程 / 单位工程 | project | 工程编号、工程名称、建设单位、开工日期、状态 | 1:N |
| 案卷 | volume | 案卷号、案卷题名、编制单位、保管期限、密级 | 1:N |
| 卷内文件 | document | 文件编号、题名、责任者、日期、起止页、份数 | 1:N |
| 文件对象 / 元数据 | doc_meta | 格式、页数、哈希、OCR 文本、存储路径 | 1:1 |
分部、分项、检验批不单独建层级,而是作为 document 上的分类码存在。这样做的原因是分部数量随工程类型变化大,做成表会撑出十几层深的树,查询和界面都会很难受;做成两位分类码,既能在卷内目录里排序,又能当检索条件用。
2.2 把卷内目录搬进数据库:核心表的建表语句
纸质卷内目录上的每一列,都要在库里找到落点。下面这段 DDL 是常见做法,字段名按实际系统习惯改即可,关键是约束要立在库这一层,而不是只写在页面校验里。
-- 案卷表:一卷对应一个可装订、可移交的归档单元 CREATE TABLE volume ( id BIGSERIAL PRIMARY KEY, project_id BIGINT NOT NULL, -- 所属工程 / 单位工程 volume_no VARCHAR(32) NOT NULL, -- 案卷号,如 03-02-005 title VARCHAR(200) NOT NULL, -- 案卷题名,来自卷内目录首行 archive_type VARCHAR(20) NOT NULL, -- 基建 / 监理 / 施工 / 竣工验收 retention VARCHAR(10) DEFAULT '永久', -- 保管期限:永久 / 30年 / 10年 status SMALLINT DEFAULT 0, -- 0编制中 1待审核 2已归档 3已移交 created_at TIMESTAMPTZ DEFAULT now(), UNIQUE (project_id, volume_no) ); -- 卷内文件表:卷内目录的每一行 CREATE TABLE document ( id BIGSERIAL PRIMARY KEY, volume_id BIGINT REFERENCES volume(id), doc_no VARCHAR(64) NOT NULL, -- 文件编号,编码规则见 2.3 title VARCHAR(200) NOT NULL, -- 文件题名 author VARCHAR(64), -- 责任者,落到具体单位或个人 doc_date DATE, -- 文件形成日期,不是上传日期 page_from INT, -- 卷内起页 page_to INT, -- 卷内止页 copies SMALLINT DEFAULT 1, -- 份数 status SMALLINT DEFAULT 0, -- 与案卷状态联动 ocr_text TEXT, -- 扫描件识别结果 content_tsv TSVECTOR -- 全文检索列,见 4.1 ); CREATE INDEX idx_doc_volume ON document (volume_id, page_from); CREATE UNIQUE INDEX uk_doc_no_current ON document (doc_no) WHERE status < 2;几个字段值得单独说。retention用中文枚举而不是数字,是因为移交清单直接按中文输出,少一层映射少一次出错;doc_date必须与上传时间分开,扫描补录时两者能差半年;uk_doc_no_current是部分唯一索引,只约束未归档的记录,归档后的历史版本允许同编号共存,这正是 4.3 里版本留痕的基础。如果数据库用的是 MySQL,TSVECTOR换成FULLTEXT或者干脆把正文同步到检索引擎。
录入时最常见的失败是"文件能传上去但归档按钮是灰的",八成是doc_date为空或者volume_id还没绑定,这一类问题在 5 章的自检脚本里会被提前拦下。
2.3 文件命名与唯一编码:让每一份资料自带检索路径
文件名带中文不是错,但拿中文文件名当主键一定会出事:同名的"检验批质量验收记录"在一条线上能有几百份,跨系统导出时编码还可能被截断。稳妥的做法是给每份资料生成一个可反解的英文数字编号,原始中文题名只作为title字段保存。编码规则建议固定六段:
import re # 项目码6位 - 单位工程2位 - 分部2位 - 分项2位 - 流水号4位 - 版本号2位 PATTERN = re.compile(r"^[A-Z0-9]{6}-\d{2}-\d{2}-\d{2}-\d{4}-V\d{2}$") def build_doc_no(project: str, unit: int, branch: int, item: int, seq: int, ver: int = 1) -> str: """按固定位数补零,保证字符串排序等于业务排序""" return f"{project}-{unit:02d}-{branch:02d}-{item:02d}-{seq:04d}-V{ver:02d}" def parse_doc_no(doc_no: str) -> dict: """反向解析,用于按分部批量筛资料和生成卷内目录""" if not PATTERN.match(doc_no): raise ValueError(f"编号不符合规则: {doc_no}") p = doc_no.split("-") return dict(project=p[0], unit=p[1], branch=p[2], item=p[3], seq=p[4], version=p[5])流水号补四位、版本号补两位,看起来是小事,实际决定了排序和分页。补零之后,字符串升序就是业务升序,卷内目录不用再写自定义比较器。版本号放进编号而不是放在文件属性里,好处是同一条记录的多次修订天然可区分,V01和V02在库里是两行,谁签的、什么时候替换的一目了然。反解析函数在批量导入时尤其有用:清单里只给编号,系统就能自动填出单位工程和分部,减少人工选错。
用PATTERN做前置校验时注意,项目码允许大写字母和数字,别只写\d{6},实际项目码里常带字母。导入十万条清单时,建议先整表跑一遍正则再入库,把不合规的行导成 Excel 退回,比一条条弹窗提示高效得多。
3. 从单份资料录入到批量归档组卷:操作手册里最常翻的三步
3.1 单份资料录入:先校验元数据再上传文件
顺序很关键。先填元数据、后传文件,是为了避免文件已经落到对象存储、记录却因为字段缺失没写进去,最后变成"有文件没台账"的孤儿文件。下面这些字段建议在页面上就设成必填,别留给后端补。
| 字段 | 校验规则 | 不通过的典型表现 |
|---|---|---|
| 文件编号 | 命中 2.3 的正则,且当前状态下不重复 | 提示编号已存在,引导走版本升级 |
| 文件题名 | 非空,长度 4–200 | 只写"记录表"三个字,后续检索无效 |
| 责任者 | 非空 | 用简称而非全称,移交时对不上 |
| 形成日期 | 不晚于当天,不早于开工日期 | 扫描补录时容易填成上传日 |
| 所属案卷 | 必须已选,且案卷状态为编制中 | 案卷已归档,需先解锁 |
| 文件格式 | PDF / OFD / JPG,单个 ≤50MB | 手机拍的十几兆 JPG 会拖慢预览 |
操作上还有一个细节:扫描件在录入前先做一遍方向校正和去黑边,再上传。系统的 OCR 对倾斜页面的识别率会明显下降,这一步花两分钟,后面检索能省很多翻页。
3.2 批量导入:Excel 清单加一个约定好的文件包目录
一个分部做完,往往积压几百份资料,逐条录不现实。批量导入的关键不是接口快慢,而是清单和文件包之间有一个谁都不能违反的对应关系:文件包里的 PDF 以文件编号命名,清单第一列也是文件编号。目录约定一旦定下来,脚本就能自己找文件、自己对不上就报错。
import pandas as pd from pathlib import Path BASE = Path("/data/import/project_2024") # 导入包根目录 df = pd.read_excel(BASE / "导入清单.xlsx", dtype=str) # 全部按字符串读,避免编号被转成科学计数法 ok, bad = 0, [] for _, row in df.iterrows(): doc_no = row["文件编号"].strip() src = BASE / "files" / f"{doc_no}.pdf" # 文件包内一律用编号命名 if not src.exists(): bad.append((doc_no, "文件缺失")); continue size_mb = src.stat().st_size / 1024 / 1024 if size_mb > 50: # 超大文件先拆分或压缩再导 bad.append((doc_no, f"体积超限 {size_mb:.1f}MB")); continue if not row["文件题名"].strip(): bad.append((doc_no, "题名为空")); continue upload(volume_no=row["案卷号"], doc_no=doc_no, title=row["文件题名"], author=row["责任者"], doc_date=row["形成日期"], file=src) ok += 1 print(f"成功 {ok} 条,失败 {len(bad)} 条") pd.DataFrame(bad, columns=["文件编号", "原因"]).to_excel(BASE / "导入失败清单.xlsx", index=False)dtype=str这一句别省,Excel 里 12 位以上的数字编号经常被自动转成科学计数法,读进来就成了1.23457E+11,后面所有匹配全错。失败清单一定要落盘成文件,导完让资料员照着改,比在控制台刷屏有用。
3.3 归档组卷:卷内目录生成与 PDF 合并
案卷状态从"编制中"改成"待审核"之前,通常要完成两件事:卷内文件按页码顺序合并成一个整卷 PDF,以及生成一份与页码完全对应的卷内目录。合并顺序必须由page_from决定,不能依赖文件名的字母序。
#!/usr/bin/env bash # 按卷内页码顺序合并归档 PDF(页码存在库里,这里用导出的序号文件名模拟) VOL="/data/volume/03-02-005" ls "$VOL"/pages | sort -t- -k5,5n # 先确认序号顺序,避免合并后页码错位 python - <<'PY' from pypdf import PdfWriter, PdfReader import glob, os files = sorted(glob.glob("/data/volume/03-02-005/pages/*.pdf"), key=lambda p: int(os.path.basename(p).split("-")[4])) # 按流水号排序 w = PdfWriter() cursor = 1 for f in files: r = PdfReader(f) w.append(r) # 逐份追加,保留原始页 print(f"{os.path.basename(f)}\t起页 {cursor}\t页数 {len(r.pages)}") cursor += len(r.pages) with open("/data/volume/03-02-005/卷内文件.pdf", "wb") as out: w.write(out) print(f"合计 {cursor - 1} 页") PY合并后打印的起页信息就是卷内目录的原始数据,直接回写数据库的page_from和page_to,比手工数页码可靠。整卷 PDF 建议再做一次文本层检测:如果卷内文件.pdf用文本搜索搜不到任何关键词,说明之前上传的是纯图片,需要回退到 OCR 流程补文本层,否则检索模块在这卷上是空的。
4. 检索、权限与版本:让在建资料找得到、改得对、追得回
4.1 图纸和扫描件的全文检索怎么落
在建工程里超过一半的文件是扫描件和图纸,纯靠文件名检索等于没检索。落地方案分两步:上传后异步跑 OCR 把文本写进ocr_text,再把title、doc_no、ocr_text合并进检索引擎。中文分词在多数数据库自带的全文检索里支持一般,常见做法是外挂带中文分词插件的检索引擎,或者在写入前用分词库处理一遍。
-- 写检索列:把编号、题名、识别文本合并,编号用分词友好写法保留 UPDATE document SET content_tsv = to_tsvector('simple', coalesce(doc_no,'') || ' ' || coalesce(title,'') || ' ' || coalesce(ocr_text,'')) WHERE id = :id; -- 查"混凝土试块"相关记录,相关度优先、日期次之 SELECT doc_no, title, author, doc_date, volume_id, ts_rank(content_tsv, q) AS rank FROM document, to_tsquery('simple', '混凝土 & 试块') q WHERE content_tsv @@ q AND status < 2 -- 只看在建资料,已移交的走另一套库 ORDER BY rank DESC, doc_date DESC LIMIT 50;'simple'配置不做词干还原,对中文和编号混合的场景反而稳,因为编号被切碎后就搜不到了。OCR 文本入库前做一次去空白和全角转半角,能明显减少"搜不到"的投诉。检索结果一定要带上volume_id和卷内页码,资料员拿着结果就能直接去翻纸质卷。
4.2 角色权限矩阵与在建、归档状态机
权限别写成一个个布尔开关撒在代码里,按角色收敛成一张矩阵,实施和运维都看得懂。
| 角色 | 录入 | 修改元数据 | 替换文件 | 归档 | 删除 | 移交导出 |
|---|---|---|---|---|---|---|
| 资料员 | 是 | 仅自己录入 | 仅编制中 | 否 | 否 | 否 |
| 专业工程师 | 是 | 本专业 | 仅编制中 | 否 | 否 | 否 |
| 项目总工 | 是 | 本项目 | 待审核及以前 | 是 | 否 | 否 |
| 监理 | 否 | 审核意见 | 否 | 否 | 否 | 否 |
| 档案管理员 | 否 | 否 | 否 | 是 | 仅未归档 | 是 |
| 系统管理员 | 否 | 否 | 否 | 否 | 审计后 | 否 |
状态机配合权限一起生效:status=2(已归档)之后,document的更新接口直接返回 403,而不是让前端把按钮置灰。前端的灰按钮挡不住直接调接口,这一点在做验收演示时经常被忽略。
4.3 版本留痕与签章冻结
一份资料在签认之前可能改五遍,签认之后再改就是事故。做法是把"可改"和"已固定"用一条明确的分界线切开:签章动作完成的那一刻,当前记录置为status=2并写入frozen_at、frozen_by和文件哈希。之后再要修改,只能新增一条V(n+1)记录,旧记录保留在库里。
哈希值在这里是核心,sha256存一份,移交或调阅时重新算一遍比对,能证明文件从签章到现在没被动过。前端展示时建议把版本历史排成时间线,而不是只显示最新版,否则出了问题连"当时提交的是哪一版"都说不清。
5. 交付前的自检:把操作手册里的检查项变成一条命令
操作手册末尾通常附一页检查清单,人工对着清单翻几百卷非常费时间,也很容易漏。更稳的做法是把清单翻译成脚本,在提交归档申请之前跑一次,把问题清单导出来派活。下面这段是最常用的几项。
import pandas as pd, re, hashlib REQ = ["doc_no", "title", "author", "doc_date", "page_from", "page_to"] PATTERN = re.compile(r"^[A-Z0-9]{6}-\d{2}-\d{2}-\d{2}-\d{4}-V\d{2}$") def sha256_of(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def check(df, base_dir): issues = [] seen = {} for _, r in df.iterrows(): doc_no = str(r["doc_no"]) for f in REQ: # 必填字段 if pd.isna(r[f]) or str(r[f]).strip() == "": issues.append((doc_no, f"缺少必填字段 {f}")) if not PATTERN.match(doc_no): # 编号规则 issues.append((doc_no, "编号不符合六段规则")) if pd.notna(r["page_from"]) and pd.notna(r["page_to"]) and r["page_to"] < r["page_from"]: issues.append((doc_no, "起止页倒置")) if doc_no in seen: # 同编号重复,且都没有版本区分 issues.append((doc_no, f"与第 {seen[doc_no]} 行重复")) seen[doc_no] = r.name p = base_dir / f"{doc_no}.pdf" if not p.exists(): issues.append((doc_no, "文件缺失")) elif r.get("file_hash") and sha256_of(p) != r["file_hash"]: issues.append((doc_no, "文件哈希与记录不一致")) return pd.DataFrame(issues, columns=["文件编号", "问题"]) # 用法:读导出清单,跑完直接生成整改工单 df = pd.read_excel("/data/export/卷内清单.xlsx", dtype=str) out = check(df, base_dir=__import__("pathlib").Path("/data/volume/03-02-005/pages")) out.to_excel("/data/export/归档前自检问题.xlsx", index=False) print(out["问题"].value_counts().to_string())几处参数值得按项目调整。REQ里如果有项目不强制要求的字段(例如部分监理资料不填页号),从列表里去掉即可,别为了跑通脚本硬填假数据;file_hash在早期库里可能为空,判断前先做if r.get("file_hash")的保护,否则会刷出一堆无意义的哈希不一致;base_dir指向的是文件包目录而不是数据库,文件包命名规则改了,这里必须同步改,否则缺失项会全量误报。
最后一层校验靠不住的地方是页码连续性。库里page_from和page_to只保证不重叠、不倒置,不保证中间不缺页。跑完上面的脚本后,再按volume_id分组排序,检查第 n 行的page_from是否等于上一行page_to + 1,不等就是缺页或者页码没回写,这一条检查十分钟能写完,赶上验收前能省一整晚。
本文还有配套的精品资源,点击获取