资讯动态

企业办公审批小程序开发:审批流建模、引擎实现与避坑指南

发布时间:2026/10/18 15:19:06 来源:尧图企业网站定制
简介这是面向企业、机关单位日常办公场景的微信小程序审批系统覆盖请假、报销、外出、合同、采购、入职等常见审批流程并按管理员、员工、审批人员三类角色划分权限审批结果可通过微信消息实时通知申请者。整个资源包共489个文件压缩后约2.12MB以JS逻辑文件、WXML页面结构、WXSS样式及JSON配置为主同时附带PNG/JPG图片素材、安装使用手册和若干工具脚本便于直接导入微信开发者工具查看运行效果。目前已有47人学习下载适合需要快速搭建内部审批应用或进行小程序开发练习的开发者。资源中除完整前端页面外还包含数据库操作工具、页面辅助函数、图表库及二维码库等模块目录结构清晰对照操作手册即可理解项目组织方式能有效缩短二次开发与部署上手的成本。1. 企业办公审批小程序把请假、报销、采购审批搬上线先解决两件事某公司行政在月底核对报销时发现一张差旅单在两位负责人之间来回传送了一周原因仅仅是某人在午休前点了“已读”但没点“通过”。这种场景几乎每家单位都有请假靠口头、报销靠截图、合同审批靠邮件往返。企业办公审批小程序要解决的就是把请假审批、报销审批、外出审批、合同审批、采购审批这些流程从聊天记录和纸质单据里抽出来变成一份有状态、有节点、有留痕的数字单据。适合正在做内部系统建设的企业IT同事也适合接定制交付的开发团队。这篇文章会直接讲清楚怎么建模流程、怎么设计数据表、怎么写审批引擎以及最容易翻车的五个地方。2. 审批流建模先定状态机再定节点最后用 YAML 描述规则2.1 审批流不是一张流程表而是一个状态机很多第一次做审批系统的人会把流程设计成线性表格发起人 → 部门主管 → 财务 → 结束。这样画流程图没问题但落地到系统里会立刻遇到三个现实问题审批人驳回后单子回到哪、发起人能不能撤回、转交之后当前节点怎么算。如果不先把状态定义清楚代码迟早绕成一团。我一般会先把全局状态定死审批中、已通过、已驳回、已撤回、已作废。再给每个审批节点定义局部状态待处理、已通过、已驳回、已转交。全局状态和节点状态分开记录因为一个审批单可能卡在第二个节点但整体状态仍然是审批中。驳回不是把整个单子作废而是回到发起人可修改的节点改完重新提交再进入审批流。撤回只能发生在审批中的单据上而且要判断当前是否已经有人处理过否则会产生并发冲突。这里的核心是区分“单据状态”和“节点状态”。单据状态描述整条审批的生死节点状态描述某一个环节的进度。后端存储时用一张表单主表记单据状态用一张审批记录表记节点状态后续做列表筛选、超时提醒、统计报表都靠这两个维度。2.2 节点类型单人审批、会签、或签、条件节点业务流程里常见的审批节点可以分成四种类型我在实际建模时基本只用这四种多了容易把配置搞复杂。节点类型处理方式典型场景单人审批指定一个审批人通过即进入下一节点部门主管审批请假或签多个审批人中任意一人通过即通过外出审批、小额报销会签多个审批人全部通过才通过合同审批、采购审批条件节点按表单字段值路由到不同分支按请假天数、报销金额分流会签和或签是翻车重灾区后面避坑章会细说。条件节点我建议写在每个业务表单里而不是写死在代码里。比如请假审批按天数分流报销审批按金额分流合同审批按合同类型和金额组合分流采购审批按预算和供应商类型分流。条件规则用表达式字符串存起来运行时解析这样业务人员改规则不用改代码。2.3 用 YAML 描述一条请假审批流建模我用 YAML 做流程定义比直接在数据库里建几十张流程表更直观也方便版本管理。下面是一条带条件分支的请假审批流。workflow: leave_approval name: 请假审批 version: 1.0 start: condition_days nodes: - id: condition_days type: condition conditions: - expr: leave_days 3 next: manager_approve - expr: leave_days 3 next: manager_approve_and_hr - id: manager_approve type: approval assignee: ${department_manager} action: or reject_to: start next: end - id: manager_approve_and_hr type: approval assignee: [${department_manager}, hr_manager] action: and reject_to: start next: end - id: end type: end这个 YAML 文件的执行逻辑发起人提交请假单后进入条件节点系统读取表单里的 leave_days 字段小于等于 3 天走单人审批超过 3 天走会签。${department_manager} 是占位符运行时从组织架构接口解析成具体员工ID避免把审批人写死在流程定义里。action 为 or 表示或签为 and 表示会签。reject_to 指向 start表示驳回后回到发起人节点发起人修改后重新提交。把这个 YAML 转换成引擎可执行的 DAG 也很简单遍历 nodes根据 next 字段建立邻接关系形成一个有向无环图。条件节点在运行时根据表单值选择唯一出口审批节点根据 action 决定等待策略。流程定义和业务数据分离之后新增一种审批类型只需要加一个 YAML 文件。2.4 条件分支的顺序与规则写法配置条件节点时最容易忽视的是顺序问题。以报销审批为例如果先写“金额小于等于 5000 走财务初审”再写“金额大于 5000 走财务总监”看起来没错。但一旦金额字段为空或者填成了字符串两条表达式都会匹配失败流程直接跑挂。所以每条条件我都会配置一个 fallback 节点表达式匹配不到时默认走 fallback。另外条件判断的顺序是从上到下短路执行匹配到第一条就不再往下看所以要把更严格、更具体的条件写在前面。比如金额小于 5000 且属于差旅费就应该放在普通小额报销的前面。3. 表单与数据层设计一张主表存公共字段一张 JSON 存差异字段3.1 请假、报销、外出、合同、采购五类单据的字段拆分标题里列的几类审批单字段差异很大但公共部分其实只有发起人、部门、时间、状态、当前节点。我把每类单据的差异字段单独列一张表设计的时候照着填就行。单据类型差异字段请假审批开始时间、结束时间、请假天数、请假类型年假/病假/事假、事由、紧急联系人报销审批费用类型、报销金额、发生日期、发票号数组、收款账户、报销说明外出审批外出开始/结束时间、目的地、同行人、交通方式、外出事由合同审批合同名称、合同类型、甲方、乙方、合同金额、签订日期、生效日期、合同附件URL采购审批采购物品清单、预算金额、供应商、收货地址、期望到货日期、采购事由字段拆完之后你会发现请假和外出几乎是同一套结构只是业务含义不同。合同和采购都有金额和供应商但合同还牵扯附件和签署日期。用 JSON 存差异字段是合理的不要为每一类单据建一张完整的大宽表否则加一个字段就要改表结构后期维护成本很高。3.2 数据表设计form 主表加审批记录表我常用的表结构是两张核心表加一张流程定义表。流程定义表存 YAML 内容另外两张表负责运行时数据。CREATE TABLE approval_form ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_no VARCHAR(32) NOT NULL COMMENT 单据编号, form_type TINYINT NOT NULL COMMENT 1请假 2报销 3外出 4合同 5采购, applicant_id BIGINT NOT NULL COMMENT 发起人, dept_id BIGINT COMMENT 所属部门, current_node_id VARCHAR(64) COMMENT 当前节点ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0审批中 1已通过 2已驳回 3已撤回, form_data JSON NOT NULL COMMENT 差异字段, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status (status), KEY idx_applicant (applicant_id) ); CREATE TABLE approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, form_id BIGINT NOT NULL COMMENT 表单ID, node_id VARCHAR(64) NOT NULL COMMENT 节点ID, approver_id BIGINT NOT NULL COMMENT 处理人, action VARCHAR(16) NOT NULL COMMENT approve/reject/transfer, comment TEXT COMMENT 审批意见, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_form (form_id) );form_data 字段用 JSON 类型请假单存 leave_days、start_time、end_time报销单存 amount、invoice_list、receiver_account。查询时如果只需要按申请人或者状态筛选走主表普通索引就够了。如果业务上有“按报销金额范围筛选”的需求就把金额抽出到冗余列尽量不要用 JSON 字段做范围查询性能会很难看。approval_record 表记录每一次审批动作不只是通过和驳回转交、退回、撤回我也都会写一条记录方便事后追溯。这张表会随着审批量增长得很快建议按月份归档或者分表保留热数据一年。前端展示审批时间线时直接查这张表按 created_at 排序即可。3.3 审批详情页的数据组装逻辑详情页是审批系统用得最多的页面组装逻辑我放在一个函数里处理。def get_approval_detail(form_id): form load_form(form_id) workflow load_workflow(form[form_type]) records load_records(form_id) node_map build_node_map(workflow) return { form: form, timeline: build_timeline(records, node_map), current_node: node_map.get(form[current_node_id]), }load_form 只查主表返回一条记录。load_workflow 按 form_type 找到对应的流程定义 YAML。load_records 查 approval_record 表拿到这个单子的所有审批记录。build_node_map 把 YAML 解析成以节点ID为键的字典这样时间线上的每一步都能知道当时处理的是哪个节点、当前卡在哪个节点。前端拿到这个结构一屏渲染完不需要再发多次请求。这里有个细节审批记录里的 node_id 必须存节点ID不要存节点名称因为流程定义有版本升级的可能节点ID不变名称可能改。4. 审批引擎与消息通知最小实现代码与三层集成策略4.1 审批引擎的最小实现或签、会签与驳回审批引擎是整个系统的心脏。我见过有人把审批逻辑写在 controllers 里每个接口各自判断最后流程一复杂就没人敢动。正确做法是抽出一个独立模块输入是当前表单、动作、操作人输出是更新后的表单和待通知的审批人。def process_approval(workflow, form, action, operator, comment): node get_node(workflow, form[current_node_id]) if action reject: form[status] 2 # 已驳回 form[current_node_id] node.reject_to add_record(form[id], node.id, operator, reject, comment) notify_user(form[applicant_id], 审批被驳回, comment) return form add_record(form[id], node.id, operator, approve, comment) if node.action or: form[current_node_id] node.next elif node.action and: approved_count count_approved(form[id], node.id) if approved_count len(node.assignee_ids): notify_next_approver(node.assignee_ids, form) return form form[current_node_id] node.next if form[current_node_id] end: form[status] 1 # 已通过 trigger_hook(form) notify_user(form[applicant_id], 审批通过, ) else: next_node get_node(workflow, form[current_node_id]) notify_next_approver(next_node.assignee_ids, form) return form这个函数把核心逻辑压到了最简。reject 分支会把状态改成已驳回把 current_node_id 回退到配置的节点一般是 start。or 分支只要有一个审批人点击通过当前节点立即完成进入下一节点。and 分支每收到一个通过动作就统计当前节点已通过人数全部通过才继续。这里有一个关键点count_approved 统计的是 approval_record 表里 form_id 加 node_id 相同且 action 为 approve 的去重人数同一节点同一人重复点击不能重复计数。这个版本是单机跑的逻辑。生产环境要考虑并发比如两个人同时点了同一个会签节点的通过count_approved 可能出现脏读。常见做法是在 approval_record 表上建唯一约束约束字段是 form_id、node_id、approver_id重复插入直接报错业务捕获异常提示“请勿重复审批”。状态机表的乐观锁也建议加上更新时带上 status 原值做条件更新行数为 0 说明状态已被其他请求修改。4.2 消息通知与超时提醒定时扫描加分级升级审批系统是否好用一半看消息通知是否及时。我的做法是事件驱动加定时兜底。事件驱动就是在 process_approval 里主动向下一步审批人推送待办定时兜底则是扫描那些超过 24 小时没人处理的记录。def scan_timeout_forms(): threshold now() - timedelta(hours24) pending_records find_pending_records(threshold) for record in pending_records: remind(record.approver_id, record.form_id)超时策略一般分三级24 小时未处理发普通提醒48 小时未处理再次提醒并抄送发起人72 小时未处理升级到管理员由管理员转交或者人工介入。等级和时间阈值我建议做成配置项不同公司节奏不一样机关单位可能春节后会积压大量单据固定死反而误事。定时任务部署上我习惯放一个独立服务每小时跑一次。用系统 crontab 就行。0 * * * * cd /opt/approval python3 scan_timeout.py logs/scan.log 21扫描逻辑只做两件事查 approval_record 表里最新的处理时间超过阈值就发消息。注意这里查的是“最新处理时间”不是“创建时间”因为一个人处理完之后下一节点的人可能会拖很久节点创建时间和处理时间差距很大。4.3 与主流企业IM工作台集成的三条路免登、H5、机器人审批小程序往往不是孤立运行的要嵌到企业办公平台上才有价值。我接触过三类集成方式按控制力排序列举。集成方式控制力数据存储适用场景复用平台自带审批模板低平台侧中小企业流程简单自建H5接入工作台高自己数据库需要合同、采购等多表单关联自建H5加机器人待办推送高自己数据库机关单位、数据不出内网第一种方式不适合标题里提到的复杂场景。平台自带模板能建请假和报销但合同审批要关联附件、采购审批要带明细列表平台模板很难满足。第二种方式是用免登接口换取用户身份审批页面完全自己开发工作台只负责入口。第三种方式最灵活纯自建只借用工作台的机器人接口做消息触达审批数据完全留在自己这边。我一般建议如果合同审批和采购审批是核心需求直接走第三条路。前期成本高但后期改流程、加统计、接财务系统都不用看平台脸色。机关单位往往也是这条路线因为审批数据涉及内部敏感信息。5. 办公审批小程序避坑指南五个最常见的流程翻车现场5.1 请假天数被自然日坑了周五到周一算成了 4 天现象某个员工周五请假到下周一系统自动算出 4 天实际工作日只有 1 天。员工投诉HR 手工改天数审批流形同虚设。原因计算请假天数时直接用 end_date - start_date 按自然日相减没有剔除周末和法定节假日。很多公司周五到周一这种情况非常普遍年中调休、法定假日补班让纯函数计算根本算不准。解决建立一张节假日表把周末、法定假日、调休补班都存进去。计算天数时遍历开始到结束每个自然日如果当天是休息日就跳过是工作日就加 1。补班日单独标记为工作日。这张表每年更新一次数据库里存两列date、is_workday。计算逻辑放在后端不要放在前端因为不同部门可能使用不同的假期规则。5.2 审批人离职或转岗后流程空转现象一张采购审批单卡在已离职员工名下管理员进入后台也找不到转交入口最后只能把数据库里的 current_node_id 手工改掉。原因流程定义里把审批人写成了具体员工ID。员工离职或转岗后这个ID在组织架构里已经失效但流程定义里还挂在节点上。解决流程定义一律存角色占位符比如部门主管、财务初审、合同管理员运行时通过接口解析成具体员工。会签节点也一样存角色列表解析后得到员工列表。如果解析结果为空直接走“该角色无匹配人员”的异常分支通知管理员配置审批人不要让它静默卡住。另外审批详情页一定要有转交功能当前节点处理人可以把单子转给其他人转交记录写进 approval_record。5.3 会签误配成或签合同单被单人直接通过现象合同审批本来要求法务、财务、总经理三人会签结果法务一个人点通过单子直接流转到下一步。原因建模时把节点 action 配成了 or。会签和或签在视觉上很像配置界面如果不加说明很容易选错。解决配置流程时把“会签”和“或签”直接放在节点类型里做成单选而不是让用户填写英文字段。同时做前置校验会签节点必须配置至少两个审批人或签节点允许配置一个或多个。我在代码里会把 or 的节点做成单独的标记流程定义校验器检测到 or 节点只有一个人时直接报警。合同和采购这两类单据默认走会签系统里做一个类型默认值降低误配概率。5.4 报销金额精度浮点运算和汇率冻结现象报销单金额乘以税率后出现 0.009 的尾巴或者有外币报销需求时汇率按审批当天汇率换算审批到一半汇率变了最终金额对不上。原因金额字段用了 Float 存储人民币换算外币时多次四舍五入。汇率如果从外部接口实时拉取每次打开详情页金额都可能变化。解决金额一律用分存储数据库字段类型用整数展示时除以 100 格式化。涉及税率时先乘再加不要在中间步骤做四舍五入。外币报销在发起表单时就冻结汇率把汇率快照存进 form_data审批过程中任何人打开详情页看到的都是同一个金额。财务付款时以冻结汇率为准汇率波动风险由公司承担或走补充审批流程这是业务决策系统层面只保证一致性。5.5 条件分支优先级写错大额采购走了普通通道现象一笔 8 万元的采购审批单直接走了“小于等于 5 万走财务初审”的分支财务初审通过了总裁办完全不知情。原因条件节点的表达式写成了金额范围但第一个条件只判断了“采购预算不为空”没有约束金额上限。按短路顺序这个宽泛条件先匹配成功后面更严格的条件根本没机会执行。解决条件节点按优先级从严格到宽松排列或者给每条条件加一个 priority 字段。每次保存流程定义时做静态检查同一层级如果有两条条件表达式对比它们的约束范围如果前一条是后一条的超集直接提示错误。条件表达式里我还会支持 whitelist 白名单只有白名单内的字段才能参与条件判断避免表单字段被随意引用。流程定义上线后做一次全量审批单模拟用历史数据跑一遍看有多少单子会走错分支比上了线再发现强得多。6. 进阶用表达式和回调钩子让审批流支持自定义6.1 把条件表达式配置化而不是写死在代码里前面的 YAML 已经能做到条件分支但表达式本身是字符串需要一套安全的解析器。我只开放白名单函数和字段不允许任意代码执行。比如金额判断def eval_condition(expr, form_data): if expr.startswith(amount ): limit float(expr.split()[1]) return form_data[amount] limit raise ValueError(funsupported expr: {expr})这个函数看起来简单但它指向一个原则条件引擎只支持系统预设的几种比较操作新增操作符要走代码评审不能靠动态执行用户输入的字符串。6.2 审批通过后的回调钩子单据通过以后往往需要触发其他系统动作这里我统一用一个回调钩子。def trigger_hook(form): if form[form_type] contract: write_contract_ledger(form) elif form[form_type] purchase: create_purchase_order(form) elif form[form_type] reimbursement: notify_finance_payment(form)审批引擎只负责流程状态机不关心业务动作。通过之后要写合同台账、要创建采购订单、要通知财务付款都挂在这个钩子里。钩子执行失败要记录日志并重试不要影响审批结果的展示。我一般把钩子做成异步任务审批通过状态先落库任务队列慢慢执行。做第一版审批系统时我把业务动作直接写在 controller 里结果每次加一种单据类型都要改动审批主流程后来抽了回调钩子才解决。现在我的习惯是任何通过之后的动作一律走钩子不在主流程里写具体的业务逻辑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑