简介面向已通过医师资格考试、正筹备执业注册的医学相关人员以及医疗机构或卫生行政部门的业务经办人这份PDF集中呈现了执业医师注册申请审核表的规范样式封面、申请人信息、个人工作经历、业务水平考核、执业机构与上级主管部门意见、卫生行政部门审批、健康体检表及医师聘用证明等模块一应俱全可直接打印填写或作为格式参照。资源为单个PDF文件大小仅113KB便于保存和随时打印。表中按官方填表说明逐项明确了写法例如申请执业级别需选填执业医师或执业助理医师执业类别需选填临床、中医、口腔或公共卫生学历应填写与申请类别对应的最高学历相片要求近期小二寸免冠正面半身照所有时间均用公历阿拉伯数字填写同时还对聘用科目、首次注册的考核意见填写等细节作出提示能有效降低漏填、错填的概率。目前已有81人学习下载适合第一次注册或后续变更注册时对照准备材料按表逐一确认即可减少往返沟通成本。1. 一张《执业医师注册申请审核表》就是最朴素的接口文档在医疗信息化项目里最容易被低估的就是这种申请审核表。表面看它只是给办事人填的纸可你把“填表说明”逐条读一遍会发现每一句都在定义权限、枚举、必填关系和时间窗口。“封面的医师执业证书编码由注册主管部门填写”是字段级权限“申请执业级别请选填执业医师或执业助理医师”是枚举约束“取得医师资格证书后二年内首次注册者不填写考核栏目”是条件必填这些术语足够让一个后端工程师直接画出表结构。对要做医师电子化注册申报、HIS 人事接口或体检管理系统的开发来说这份 PDF 就是业务规则的原始凭证比需求文档更接近真实逻辑。技术上的难点从来不是把表格做成网页而是把纸面规则翻译成可校验的数据模型再落进流程状态机里。2. 从填表权限到字段类型拆解注册表的隐藏数据模型2.1 三方填写边界决定了表单的字段级权限填表说明第 3 条写着“封面、表 1-2 由申请人填写表 3-4 由有关部门填写”这句话直接决定系统的权限模型。设计电子申报系统时不能做一个所有字段都开放的编辑页。申请人提交后本人字段必须进入只读态执业机构、上级主管部门、卫生行政部门各有一组自己的可编辑字段。常见做法是给字段挂 owner 和 writeRoles 属性而不是按整个页面做权限控制否则退回修改时很难说清谁能改哪一格。栏目填写人权限映射典型字段封面、表 1-2申请人提交后只读退回可改姓名、资格级别、资格证编码表 3-4 考核与培训意见培训机构/考核机构机构账号填写申请人不可见培训时间、考核结果执业机构意见拟聘用机构机构负责人签名盖章拟聘用科目上级主管部门审批意见上级主管部门审批角色拟聘用科目审核卫生行政部门审批意见卫生行政部门终审填编码执业证书编码、备注这个边界不是摆设它决定退回流程怎么设计。申请人提交后机构发现拟聘用科目填错不能直接改申请人的字段只能走“退回申请人”的动作卫生行政部门填写的执业证书编码申请人永远看不到编辑入口。如果实现时整张表只挂一个权限标识后续做操作审计时会很被动。{ applicant_info: { type: object, properties: { name: { type: string, maxLength: 50 }, id_card: { type: string, pattern: ^\\d{17}[\\dXx]$ }, practice_level: { type: string, enum: [执业医师, 执业助理医师] }, practice_category: { type: string, enum: [临床, 中医, 口腔, 公共卫生] } }, writeRoles: [applicant] }, approval_info: { type: object, properties: { practice_scope: { type: string }, certificate_no: { type: string, writeRoles: [health_admin] } } } }上面这段 JSON Schema 演示的是字段级权限的落法writeRoles 是自定义属性实际项目中我会在服务端统一校验前端只用来控制渲染。身份证号用^\d{17}[\dXx]$做格式校验枚举字段直接用常量定义避免各端各写一套字符串。2.2 人员信息字段类型、长度与一致性校验姓名、性别、出生年月、民族、学历、所学专业、家庭地址、邮编、身份证号看起来都是基础字段堆在一起就有几个容易被忽略的约束。填表说明第 4 条要求“年、月、日时间一律用公历阿拉伯数字”所以出生年月必须按日期类型提交不能接受“1988.5”或“88 年 5 月”这类写法第 7 条要求“学历应填写与申请类别相应的最高学历”这又引出一个联动校验申请临床学历专业就不能填会计。身份证号码是整张表的主索引之一。它同时被执业机构、审批部门、体检表反复引用所以我一般把它作为申请人维度去重的键而不是依赖自增 ID。出生年月必须与身份证第 7 到 14 位一致否则在注册系统里会直接报“出生日期与身份证信息不符”。照片也值得注意填表说明第 8 条写“近期小二寸免冠正面半身照”折算成扫描尺寸大约在 33mm×48mm按 300dpi 换算像素约 390×567。上传组件只校验文件后缀是远远不够的。字段校验规则常见填错方式姓名去除首尾空格验证生僻字编码繁体字、生僻字乱码出生年月公历日期与身份证一致写农历日期、格式不统一照片小二寸、近期免冠生活照、自拍、尺寸不符邮编6 位数字填了固定电话号专业技术职务任职资格按职称字典选择自由文本导致统计失败这些规则不少医院信息科会落在前端但我建议至少把“出生日期与身份证一致”“照片尺寸”“邮编格式”三道校验放到接口层因为机构端经常批量导入数据会绕过网页直接调接口。2.3 资格、类别与科目三层字典联动级别和类别是贯穿全表的字段。级别只有执业医师和执业助理医师两个值类别只有临床、中医、口腔、公共卫生四类后续所有机构意见、审批意见都会重复出现这两组值。实现时直接用码表不要散落在业务代码里做字符串比较。填表说明第 9 条写得很细临床、口腔类别按《医疗机构诊疗科目名录》一级科目填写中医类别按二级科目填写公共卫生参照公共卫生医师职业分类。这决定科目必须是树形结构并且不同类别挂不同层级。如果前端把整棵目录树一次性拉下来用户很容易选到不该选的层级审批阶段再被打回。CREATE TABLE practice_subject ( id INT PRIMARY KEY, category VARCHAR(16) NOT NULL COMMENT 临床/中医/口腔/公共卫生, parent_id INT NOT NULL DEFAULT 0, subject_name VARCHAR(64) NOT NULL, is_leaf TINYINT NOT NULL DEFAULT 0 ); -- 临床类别只允许选一级科目 SELECT id, subject_name FROM practice_subject WHERE category 临床 AND parent_id 0; -- 中医类别允许选二级科目 SELECT id, subject_name FROM practice_subject WHERE category 中医 AND parent_id 0;这段 SQL 是示意核心思想是同一张字典表在不同类别下过滤不同层级。公共卫生类别的数据源可能是独立的职业分类码表需要在接口层做映射不能想当然认为所有类别都能查到同一个 parent_id 规则。2.4 不同页面重复出现的字段怎么处理封面有级别、类别执业机构意见页有级别、类别、拟聘用科目卫生行政部门审批意见页还有级别、类别、聘用科目。同一个字段在三个环节重复出现这是盖章文件的特点不是数据库设计错误。常见做法是申请主表只存一份审批意见表记录快照。每次有“盖章”动作时把当时的关键字段冗余存储后续做变更注册时用快照对比“执业范围是否发生变化”。用操作日志做对比也行但日志通常会被清理审批表却要存很多年。快照表结构可以简单地设计成申请 ID、环节、级别、类别、科目、操作人、操作时间六列数据量不大但排查问题时非常有用。3. 首次注册与逾期注册流程状态机和条件必填3.1 从提交到发证的六步状态流转一张完整的申请表至少经过申请人、执业机构、上级主管部门、卫生行政部门四类角色。落到系统里就是一套状态机状态不宜设计得太多否则每个退回分支都要写一遍逻辑。当前状态操作角色动作下一状态草稿申请人提交申请机构审核中机构审核中执业机构通过/退回上级审批中/草稿上级审批中上级主管部门通过/退回行政终审中/草稿行政终审中卫生行政部门通过/退回待发证/草稿待发证卫生行政部门登记证书编码已发证状态值建议用数据库枚举或常量类不要用中文直接写在业务代码里判断。退回动作必须携带原因否则申请人只看到“已退回”大概率会打电话问窗口。我一般在退回记录里同时存发起人、处理人、退回原因和附件截图方便后续追踪。3.2 两年内首次注册条件必填规则填表说明第 10 条是整个流程逻辑的核心“取得医师资格证书后二年内首次注册者不填写业务水平考核机构或组织的名称和培训时间及考核结果、考核和培训机构或组织的意见栏目。”这句话翻译成代码就是“考核机构、培训时间、考核结果”三个字段是否必填取决于两个条件是否首次注册以及资格证签发日期距申请提交日是否超过 730 天。from datetime import date def assessment_required( is_first_reg: bool, cert_issue_date: date, apply_date: date ) - dict: gap_days (apply_date - cert_issue_date).days if is_first_reg and gap_days 730: return { training_org: False, training_time: False, assessment_result: False, org_opinion: False } return { training_org: True, training_time: True, assessment_result: True, org_opinion: True }这里的 730 是按公历两年粗略折算实际操作中部分地区按自然年算。我会把“二年”做成配置项放在系统参数表里避免各地口径不同还要改代码。返回的 dict 传给前端渲染必填标识后端保存接口再校验一次防止有人绕过前端直接提交空值。3.3 级别、类别、科目三者的组合校验级别、类别、拟聘用科目三者必须满足组合约束。临床类别不能选中医内科口腔类别不会出现在公共卫生职业分类里。这种校验适合做成白名单判断而不是写一堆 if else。ALLOWED_SCOPE { 临床: {内科, 外科, 妇产科, 儿科, 急诊医学科}, 口腔: {口腔科}, 中医: {中医内科, 中医外科, 中医妇产科}, 公共卫生: {传染病防控, 慢性病防控} } def check_scope(category: str, subject: str) - bool: scope_set ALLOWED_SCOPE.get(category, set()) return subject in scope_set示例里的科目集合是简化数据线上环境应从 practice_subject 表实时查询因为《医疗机构诊疗科目名录》更新时不需要发版改代码。这个函数只表达判定逻辑先按类别取集合再判断科目是否在集合内。白名单之外的值一律拒绝不要做模糊匹配。3.4 健康体检表里的级联校验体检表同样有隐藏规则。体检结果六选一健康或正常、一般或较弱、有慢性病、传染病传染期、精神病发病期、身体残疾。选了“有慢性病”必须在心血管病、脑血管病、慢性呼吸系统病、慢性消化系统病、慢性肾炎、结核病、神经或精神疾病、糖尿病、其他这九项里继续勾选选了传染病传染期、精神病发病期、残疾三者之一必须写一段具体说明。这种级联关系在表单引擎里就是“父字段值等于 X 时子字段组必填”。我把校验放在体检数据保存接口里因为体检数据经常由机构端批量导入前端校验会被跳过。def health_check_errors(result: str, sub_items: list, description: str) - list: errors [] if result 有慢性病 and not sub_items: errors.append(选择有慢性病时必须勾选至少一项细分) if result in {传染病传染期, 精神病发病期, 身体残疾} \ and not description.strip(): errors.append(选择第④⑤⑥项时必须填写具体说明) return errors参数 sub_items 对应九项慢性病细分description 对应“请具体说明”的输入框。返回的 errors 列表为空表示通过否则每条错误文本可以直接用于接口响应提示。4. 批量申报场景下的 PDF 解析与字段抽取4.1 先判断 PDF 是文字层还是扫描件处理这份执业医师注册表时第一步不是写解析代码而是判断 PDF 形态。电子签章系统或填表工具导出的 PDF 带文字层pdfplumber 能直接抽文本窗口扫描后归档的 PDF 是纯图片必须先做 OCR。判断方法很简单用 pdfplumber 提取第一页文本如果返回为空或只有页眉页脚基本可以判定是扫描件。PDF 形态判断方式处理方案文字层 PDFextract_text 返回非空pdfplumber 正则纯扫描件extract_text 为空或乱码pdftoppm 转图 OCR混合型部分页有字、部分页是图按页判断逐页路由混合型经常出现在多页材料里比如封面是电子填写的体检表却是手写后扫描的。按页判断再分配处理路径比整份文件统一处理更可靠。4.2 基于锚点的字段抽取封面字段最规整适合做第一版解析。用 pdfplumber 提取文本身份证号用\d{17}[\dXx]这个正则命中姓名则在以“姓名”开头的行里取值。需要注意 PDF 抽取时可能把号码折成两段所以先去掉空格再匹配。import pdfplumber import re ID_CARD_RE re.compile(r(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([\dXx])) def parse_first_page(pdf_path: str) - dict: records {} with pdfplumber.open(pdf_path) as pdf: first_text pdf.pages[0].extract_text() or match ID_CARD_RE.search(first_text.replace( , )) if match: records[id_card] match.group(0) records[birth_date] f{match.group(2)}-{match.group(3)}-{match.group(4)} for line in first_text.splitlines(): if line.startswith(姓名): records[name] line.replace(姓名, ).strip() break return records身份证号正则分了六组前 6 位地区、中间 8 位生日、后 4 位顺序和校验位这样一次匹配就能同时拿到完整号码和出生日期比先 search 再切片少一步。姓名解析按行锚点走是因为封面字段是固定的“姓名张三”布局OCR 或导出文本的换行位置通常稳定。4.3 表内多页数据与坐标裁剪封面处理完第二页起还有个人工作经历、健康体检表、聘用证明等段落。这些区域不像封面那么规整常见做法是先用page.rects或page.lines找到表格横线位置确定各行坐标再做裁剪而不是写死坐标。表格线位置在不同扫描版本里会有偏移写死坐标的代码换一份档案就废了。def crop_region(page, bbox): cropped page.crop(bbox) return cropped.extract_text() or bbox 是 (left, top, right, bottom) 四元组单位是点。比如体检表里“脉搏”和“血压”在同一行全文抽取会丢失表格结构裁剪到对应行区域后每一行的文本就能按顺序拼出来。实际开发中我会先打印页面的 lines 和 rects 坐标人工核对一到两份样本再确定裁剪区域而不是直接靠猜。4.4 扫描件与手写内容的 OCR 兜底扫描件没有文字层只能在图像层面处理。我一般用 pdftoppm 把 PDF 转成 300dpi 的 PNG再做灰度、二值化和去横线最后交给 Tesseract 的中文识别模型。表格里的下划线对 OCR 干扰很大识别结果经常把字段名和填写内容粘在一起所以预处理不能省。# 转图片300dpi 保证小字号可辨 pdftoppm -png -r 300 input.pdf page # 去横线后识别chi_sim 是简体中文模型 tesseract page-1.png stdout -l chi_sim --psm 6--psm 6告诉 Tesseract 把整页当作一个文本块比默认的自动分块更适合表格类文档如果字段还会错位可以改成--psm 11处理稀疏文本。OCR 结果需要设置置信度阈值低于阈值的字段进人工复核队列不要直接入库。手写数字识别率会明显低于印刷体体检表里的血压、心率这类数字字段我基本默认走人工复核。4.5 批量处理时的异常隔离一个目录可能放几十上百个 PDF单个文件解析失败不能中断整个任务。把解析函数包一层异常捕获失败文件先记录路径和报错信息跑完统一看报告。pdfplumber 打开文件后要用 with 语句释放句柄Windows 下文件句柄不释放批处理到第几十个文件时会报权限错误这是最容易踩的坑。5. 校验清单与高频退回原因的自检脚本5.1 把填表说明翻译成退回原因表审批系统里最怕的不是规则复杂而是规则分散在各处没人维护。把填表说明里的约束收集成一张清单既能指导测试用例设计也能在运行期直接映射退回原因。退回现象对应规则实现建议照片不是小二寸免冠照填表说明第 8 条上传组件裁剪并校验宽高比拟聘用科目填错层级填表说明第 9 条按类别加载科目字典并限制可选层级首次注册却未填考核信息填表说明第 10 条用 3.2 节的条件必填校验封面医师执业证书编码被填写填表说明第 3 条该字段对申请人设置为只读执业范围不是标准名称填表说明第 12 条保存时与执业范围字典比对5.2 对已录入数据做定期体检前端拦截只能解决新录入数据存量数据也要定期扫描。下面这个函数检查身份证号格式、封面证书编码误填、考核结果缺失三类问题适合做成定时任务每天跑一次。import re ID_CARD_RE re.compile(r^\d{17}[\dXx]$) def batch_check(rows: list[dict]) - list[str]: problems [] for row in rows: if not ID_CARD_RE.match(row[id_card] or ): problems.append(f{row[id]}: 身份证号格式不合法) if row.get(certificate_no_by_applicant): problems.append(f{row[id]}: 封面证书编码不应由申请人填写) if row[need_assessment] and not row[assessment_result]: problems.append(f{row[id]}: 考核结果缺失) return problemsneed_assessment 就是 3.2 节算出来的布尔值在数据库里已经在提交时算好存字段里体检时直接查不需要重新算时间差。这类定时检查能提前暴露表单引擎配置错误而不是等问题积累到申请人投诉。5.3 一个值得保留的细节用快照做变更对比最后分享一个实际申报系统里常用的做法。每个环节盖章保存时把级别、类别、拟聘用科目、执业证书编码四个关键字段连同操作人、操作时间写进审批快照表。后续做变更注册或者跨系统对账时直接比较快照值不需要翻日志。变更前后值不一致还能自动生成变更说明减少窗口人员手工录入成本也比临时比对主表字段的修改时间更快。本文还有配套的精品资源点击获取