影刀RPA深度实战合同自动化管理与到期续签自动提醒系统作者林焱一、痛点切入我在之前做内控咨询的时候遇到过这样一个真实案例一家制造企业跟核心供应商的年度框架合同过期了3个月双方都还在按旧合同执行直到供应商涨价才意识到合同早就到期了。因为没有续签导致价格谈判失去主动权多花了80多万。这还不是最夸张的。更常见的是合同审批流程卡在某个人那里等到业务急着要用的时候才发现合同还没签——这种情况每个做过合同管理的人都深有体会。合同管理的核心痛点就四个到期没人管、审批没人催、归档到处丢、续签老忘记。我用影刀RPA搭建了一套合同全生命周期管理系统覆盖了从到期预警→模板填充→审批催办→签署归档→续签提醒的完整流程。这篇文章把这套系统的搭建方法完整分享出来。二、核心方法场景描述企业合同管理需要覆盖以下环节到期提醒合同到期前自动提醒相关人员续签/新建根据模板自动生成新合同审批推进监控审批流程超时自动催办签署归档签署完成后自动归档到指定位置续签提醒周期性合同的续签循环提醒整体架构合同数据源(Excel/ERP) → 到期监测 → 提醒通知 → 合同生成 → 审批推进 → 签署归档 ↑ ↓ └──────────────── 状态更新 ←──────────────────────────────────┘分步操作第1步建立合同台账首先要有一个规范的合同电子台账。如果公司有合同管理系统如泛微、致远直接从系统导出如果没有用Excel维护也行。合同台账建议包含以下字段字段说明示例合同编号唯一标识HT-2024-0001合同类型采购合同/销售合同/租赁合同/服务合同采购合同合同名称合同标题XX设备采购合同对方单位合同对方全称XX科技有限公司合同金额金额500000签订日期合同签订日2024-01-15生效日期合同生效日2024-01-15到期日期合同到期日2025-01-14续签方式自动续签/提前续签/到期终止提前续签负责人谁负责续签张三审批状态待审批/审批中/已完成/已归档已完成合同文件路径电子版合同存储位置/contract/2024/HT-2024-0001.pdf备注其他重要信息提前30天通知是否续签第2步自动到期监测这是整个系统的核心——到期前自动提醒。子流程名称合同到期监测 触发方式每天定时运行建议每天早上8:00运行 步骤 1. 【读取Excel】读取合同台账 文件路径{合同台账路径} 2. 【日期计算】计算每个合同距离到期还有多少天 影刀公式DateDiff(d, 当前日期, 到期日期) 3. 【筛选】筛选出需要提醒的合同 筛选条件 ── 距离到期 ≤ 7天紧急提醒尚未续签的 ── 距离到期 ≤ 30天常规提醒 ── 距离到期 ≤ 90天提前提醒需要提前续签的合同类型 4. 【条件判断】是否有需要提醒的合同 ── 有继续第5步 ── 没有记录日志今日无到期合同流程结束 5. 【发送通知】通过飞书/钉钉/邮件发送提醒 通知模板见下文 6. 【写入日志】记录本次监测结果第3步合同模板自动填充合同模板通常是一个Word文档里面有很多占位符如{{甲方}}、{{合同金额}}等需要根据业务数据自动填充。子流程名称合同模板自动填充 参数 合同模板路径 填充数据字典 输出路径 步骤 1. 【打开Word】使用影刀「Word-打开文档」指令打开合同模板 注意先复制一份模板副本不要修改原始模板 2. 【循环】遍历填充数据字典 对每个键值对 ── 【Word-查找替换】 查找内容{{字段名}}如{{甲方}} 替换为对应的值 ⚠️ 注意影刀的Word替换不支持替换占位符中的特殊字符时 可以考虑先导出为文本替换再导入Word 或者使用Python方案更灵活 python from docx import Document def fill_contract_template(template_path, data, output_path): 填充合同模板 doc Document(template_path) # 替换段落中的占位符 for paragraph in doc.paragraphs: for key, value in data.items(): placeholder f{{{{{key}}}}} # 生成 {{key}} 格式的占位符 if placeholder in paragraph.text: # 获取原始格式 inline paragraph.runs for run in inline: if placeholder in run.text: run.text run.text.replace(placeholder, str(value)) # 替换表格中的占位符 for table in doc.tables: for row in table.rows: for cell in row.cells: for paragraph in cell.paragraphs: for key, value in data.items(): placeholder f{{{{{key}}}}} if placeholder in paragraph.text: for run in paragraph.runs: if placeholder in run.text: run.text run.text.replace(placeholder, str(value)) doc.save(output_path) return output_path【保存文档】保存填充后的合同【转换PDF】可选使用影刀「Word-导出PDF」将Word转为PDF#### 第4步审批流程自动推进 如果公司使用OA系统飞书审批、钉钉审批、企业微信审批可用影刀对接子流程名称合同审批催办触发方式每天下午14:00运行步骤【读取Excel】读取合同台账中审批状态为审批中的记录【循环】遍历每条记录的审批流程── 【调用OA API】查询当前审批节点和审批人飞书审批APIGET /approval/v4/instances/{instance_id}返回当前节点审批人列表── 【条件判断】判断审批是否超时如果当前审批人超过24小时未处理── 【发送飞书消息】给审批人发送催办消息── 【抄送】抄送其直属上级超过48小时── 【更新台账】更新审批状态#### 第5步签署后自动归档 合同签署后电子签或实体签自动归档到指定位置子流程名称合同自动归档步骤【监测文件】监测合同签署完成目录目录{合同签署完成目录}文件类型*.pdf【OCR解析】解析合同封面/首页的关键信息── 合同编号── 合同名称── 签署日期【重命名】按规则重命名文件格式{合同编号}{合同名称}{签署日期}.pdf【移动文件】移动到归档目录目录结构{归档根目录}/├── 2024/│ ├── 采购合同/│ ├── 销售合同/│ └── 服务合同/【更新台账】更新合同台账中的合同文件路径和审批状态#### 第6步续签自动提醒 对于设置了自动续签或需要续签的合同子流程名称续签提醒与发起触发方式合同到期前X天可配置如90天/60天/30天步骤【筛选】筛选出即将到期且需要续签的合同条件到期日期 - 当前日期 ≤ 续签提前天数条件续签状态 “待续签” 且 合同状态 ≠ “已终止”【发送提醒】给合同负责人发送续签提醒通知模板 合同续签提醒合同编号{合同编号}合同名称{合同名称}合同对方{对方单位}合同金额{合同金额}元到期日期{到期日期}距离到期{剩余天数}天操作建议请尽快确认是否续签── 如果确认续签点击链接发起续签审批── 如果不续签点击链接确认终止【自动续签】对于设置了自动续签的合同── 生成新的续签合同基于原合同模板更新日期── 自动发起审批流程── 更新台账--- ## 三、代码实现 ### 3.1 合同到期提醒完整代码 python 合同到期自动监测与提醒引擎 每天定时运行扫描所有合同对即将到期的合同发送提醒 import pandas as pd from datetime import datetime, timedelta class ContractExpiryMonitor: 合同到期监测器 def __init__(self, contract_file_path, alert_days_configNone): 初始化监测器 contract_file_path: 合同台账Excel文件路径 alert_days_config: 提醒时间配置 self.contract_file contract_file_path self.alert_days alert_days_config or { 紧急: 7, # 7天内到期为紧急 常规: 30, # 30天内到期为常规提醒 提前: 90 # 90天内为提前提醒适用于需提前续签的合同 } def scan_contracts(self): 扫描合同台账找出需要提醒的合同 # 读取合同台账 df pd.read_excel(self.contract_file) today datetime.now().date() alerts { 紧急: [], 常规: [], 提前: [] } expired [] # 已过期的合同 for _, row in df.iterrows(): contract_id row[合同编号] contract_name row[合同名称] expiry_date pd.to_datetime(row[到期日期]).date() contract_type row.get(续签方式, 提前续签) responsible row.get(负责人, ) status row.get(合同状态, ) # 跳过已终止/已完成的合同 if status in [已终止, 已续签, 已归档-历史]: continue days_remaining (expiry_date - today).days # 分类 if days_remaining 0: # 已过期 expired.append({ 合同编号: contract_id, 合同名称: contract_name, 到期日期: expiry_date.strftime(%Y-%m-%d), 已过期天数: abs(days_remaining), 负责人: responsible, 警示级别: 严重, 建议: 合同已过期请立即处理如需续签请走紧急审批通道 }) elif days_remaining self.alert_days[紧急]: alerts[紧急].append({ 合同编号: contract_id, 合同名称: contract_name, 到期日期: expiry_date.strftime(%Y-%m-%d), 剩余天数: days_remaining, 负责人: responsible, 警示级别: 紧急, 建议: 合同即将到期请立即联系对方确认续签事宜 }) elif days_remaining self.alert_days[常规]: alerts[常规].append({ 合同编号: contract_id, 合同名称: contract_name, 到期日期: expiry_date.strftime(%Y-%m-%d), 剩余天数: days_remaining, 负责人: responsible, 警示级别: 关注, 建议: 请在本周内启动续签评估流程 }) elif contract_type 提前续签 and days_remaining self.alert_days[提前]: alerts[提前].append({ 合同编号: contract_id, 合同名称: contract_name, 到期日期: expiry_date.strftime(%Y-%m-%d), 剩余天数: days_remaining, 负责人: responsible, 警示级别: 提醒, 建议: f距离合同到期还有{days_remaining}天建议提前启动续签评估 }) return alerts, expired def format_alert_message(self, alerts, expired): 格式化提醒消息 message_lines [] message_lines.append(f 合同到期监测报告 - {datetime.now().strftime(%Y-%m-%d)}) message_lines.append( * 40) if expired: message_lines.append(f\n 已过期合同{len(expired)}份) for item in expired: message_lines.append(f [{item[合同编号]}] {item[合同名称]}) message_lines.append(f 到期日{item[到期日期]}已过期{item[已过期天数]}天) message_lines.append(f 负责人{item[负责人]} | {item[建议]}) if alerts[紧急]: message_lines.append(f\n 紧急提醒{len(alerts[紧急])}份7天内到期) for item in alerts[紧急]: message_lines.append(f [{item[合同编号]}] {item[合同名称]}) message_lines.append(f 到期日{item[到期日期]}剩余{item[剩余天数]}天) if alerts[常规]: message_lines.append(f\n 常规提醒{len(alerts[常规])}份30天内到期) for item in alerts[常规]: message_lines.append(f [{item[合同编号]}] {item[合同名称]} - {item[剩余天数]}天后到期) if alerts[提前]: message_lines.append(f\n 提前提醒{len(alerts[提前])}份90天内到期) for item in alerts[提前]: message_lines.append(f [{item[合同编号]}] {item[合同名称]} - {item[剩余天数]}天后到期) if not expired and not any(alerts.values()): message_lines.append(\n✅ 暂无即将到期或已过期的合同。) message_lines.append(f\n---\n此消息由RPA合同管理系统自动生成) return \n.join(message_lines)3.2 续签合同自动生成 续签合同自动生成 基于原始合同模板更新关键日期和序列号自动生成续签合同 defgenerate_renewal_contract(original_contract,renewal_data): 生成续签合同 original_contract: 原合同信息 {合同编号, 对方单位, 合同金额, 模板路径} renewal_data: 续签信息 {新生效日期, 新到期日期, 调整金额(如有), 新合同编号} fromdocximportDocumentfromdatetimeimportdatetimeimportshutilimportos template_pathoriginal_contract[模板路径]old_contract_idoriginal_contract[合同编号]new_contract_idrenewal_data[新合同编号]# 生成新的合同编号如果未指定ifnotnew_contract_id:# 格式HT-2025-0001-续1partsold_contract_id.split(-)seqint(parts[-1])ifparts[-1].isdigit()else0new_contract_idfHT-{datetime.now().year}-{seq1:04d}# 复制模板new_doc_pathf{os.path.dirname(template_path)}/{new_contract_id}_续签合同.docxshutil.copy2(template_path,new_doc_path)# 打开新文档并填充数据docDocument(new_doc_path)# 替换合同编号forparagraphindoc.paragraphs:forruninparagraph.runs:ifold_contract_idinrun.text:run.textrun.text.replace(old_contract_id,new_contract_id)# 替换日期replace_dates{生效日期:renewal_data.get(新生效日期,datetime.now().strftime(%Y年%m月%d日)),合同有效期:f{renewal_data.get(新生效日期,)}至{renewal_data.get(新到期日期,)},签订日期:datetime.now().strftime(%Y年%m月%d日),}forparagraphindoc.paragraphs:forkey,valueinreplace_dates.items():forruninparagraph.runs:ifkeyinrun.text:run.textrun.text.replace(run.text.split(key)[0],str(value))# 如果有金额调整if调整金额inrenewal_dataandrenewal_data[调整金额]!original_contract[合同金额]:old_amountstr(original_contract[合同金额])new_amountstr(renewal_data[调整金额])forparagraphindoc.paragraphs:forruninparagraph.runs:ifold_amountinrun.text:run.textrun.text.replace(old_amount,new_amount)doc.save(new_doc_path)return{新合同编号:new_contract_id,文件路径:new_doc_path,生成时间:datetime.now().isoformat()}四、踩坑经验坑1合同台账的到期日期格式不统一现象合同台账是多个部门维护的有些日期是2024/12/31有些是2024.12.31有些甚至写了2024年12月31日导致日期计算失败。原因人工维护Excel没有做数据校验各人填写的格式五花八门。解决在读取合同台账后先用正则提取各种日期格式做统一转换对无法解析的日期标记为日期格式异常请手动修正如果公司有合同管理系统直接从系统导出格式统一如果只能用Excel用数据验证限制日期格式输入坑2OA审批流程API的权限问题现象用影刀调用飞书/钉钉的审批API时返回没有权限。原因调用审批API需要特定的应用权限飞书需要approval:instance:readonly等权限scope。解决先在飞书开放平台的应用中配置好审批权限scope确保应用被安装到了需要查询审批的空间中使用管理员账号获取access_token查询审批实例时只能查到当前应用发起的审批跨应用查不到坑3PDF合同OCR识别关键字段失败现象OCR识别PDF合同的合同编号时把HT-2024-0001识别成了HT-2024-0O01零和大写O混淆。原因扫描件质量不高 OCR引擎对数字字母的识别精度有限。解决合同编号设计时避免使用容易混淆的字符0/O, 1/I/l, 2/Z等使用正则修正合同编号如果有固定格式如HT-数字-数字做格式校验和修正优先使用电子签署的合同文字可复制而非扫描件坑4续签提醒重复发送现象每天都发一遍续签提醒负责人烦死了把机器人消息屏蔽了。原因没有记录已发送状态每天扫描台账都会重新发送提醒。解决增加提醒记录表记录每次提醒的发送时间、接收人和合同编号同一合同同一级别提醒7天内不重复发送可配置如果接收人已确认已知晓不再发送提醒坑5合同模板中的嵌套表格现象合同模板中有表格如付款计划表、验收标准表用简单文本替换无效。原因Word表格中的文字替换需要遍历到表格层不能只处理段落层。解决Python方案使用python-docx库逐层遍历段落→表格→单元格→段落影刀方案把表格单独放在Excel中处理生成后再插入Word更简单的把复杂表格放在合同附件中正文不包含表格坑6合同文件路径失效现象归档后的合同过了一段时间在台账里点的文件路径打不开了。原因合同文件被手动移动了位置、文件服务器迁移了、或者路径中的中文文件夹被重命名了。解决合同文件路径使用绝对路径如\\fileserver\contract\2024\HT-2024-0001.pdf不要用相对路径定期如每月扫描一次合同台账中的文件路径是否可访问如果文件不存在自动发通知给负责人五、常见问题速查#现象原因解决方式1合同台账读取后部分行空白Excel中有合并单元格导致pandas读取异常读取前先取消合并单元格或使用pd.read_excel()的skiprows参数跳过多余表头2飞书消息推送后对方收不到机器人不在对应群组或不具备消息推送权限检查飞书机器人的权限scope确认机器人已被添加到目标群组3Word替换占位符后格式丢失占位符被拆分成多个run替换后合并导致格式错乱使用python-docx时注意保持run的格式先清除所有格式再替换最后统一设置格式4续签合同生成后找不到文件生成路径中有不存在的子文件夹生成前先检查目标路径是否存在不存在则用os.makedirs()创建5OA审批催办发给了错误的人审批节点中可能有会签/或签等多重规则未正确处理查询API返回的审批节点详情区分审批人和抄送人只催办当前审批节点的人6合同长期处于审批中但实际已签完签署完成后没有更新台账中的审批状态增加每日核对任务比对OA系统审批状态和台账状态不一致的自动同步六、推荐资源python-docx库https://python-docx.readthedocs.io/ — Python操作Word文档飞书审批开发文档飞书开放平台-审批应用API合同管理最佳实践参考《企业内部控制应用指引第16号——合同管理》影刀官方文档Word/Excel操作指令参考电子签章对接法大大、上上签等电子签平台提供API对接方案七、内容标签影刀RPA合同管理到期提醒续签自动化模板填充审批催办归档管理合同生命周期法务自动化OA对接注意合同文件属于企业核心法律文件自动化处理时请确保文件存储安全、权限管控到位。合同模板中的条款变更需要经过法务审核自动化流程仅负责技术层面的字段填充和流程推进不涉及条款内容的自动修改。涉及电子签署的方案请选择具有《电子签名法》合规资质的电子签平台。