学校里总有一些“看起来很适合做成小程序”的事情:活动报名、设备报修、培训登记、材料收集、场地借用、经费申请。刚开始,需求往往只有一句话:“做个表单,填完以后负责人能审核。”真正动手才会发现,输入框和列表页并不难,难的是一条申请离开提交人之后,究竟可以被谁看见、被谁修改、被退到哪里,以及每一步该留下什么证据。

一个可用的审批工具,不是给数据加几个“通过”“不通过”按钮,而是把原来依靠口头交代和聊天记录维持的规则,变成系统能够执行、复查和解释的状态流转。

所以,中层管理者或教师用 Codex、OpenCode 一类工具做校园 CRUD 时,第一张图不该是页面原型,第一段提示词也不该是“帮我生成一个管理系统”。先把状态、动作、角色和证据说清楚,后面的页面、接口和数据库才不会越改越乱。

先画出一张业务流转图,再画页面

先选一个足够具体的场景。例如“教师提交校内活动申请,部门负责人审核,退回后可以修改重交,通过后由办公室归档”。把参与者写在纸的两边,把申请可能经过的节点放在中间,再用箭头连接。此时先别讨论按钮颜色、首页统计和消息提醒,只回答三个问题:申请现在在哪里,下一步允许去哪里,谁能推动它。

最小流程可以只有五个状态:草稿、待审核、退回修改、已通过、已归档。它们已经足以暴露许多隐藏规则:草稿能否由别人看到;待审核期间提交人能否改内容;退回后是覆盖原记录,还是生成新版本;通过后如果发现填错,谁有权重新打开;归档是否代表业务真正结束。

从提交、审核、退回修改到通过归档的业务状态流转示意图
页面只是流程的外壳。先把分支、回路和终点画清楚,才能知道每个页面到底需要哪些动作。

画图时不要用“处理中”“已完成”这种包罗万象的词。“处理中”可能是等待部门审核,也可能是等待申请人补材料;“已完成”可能只是审核通过,也可能已经执行、验收和归档。状态越模糊,系统越依赖线下追问,最后只剩下一张比电子表格更难改的表。

状态不是备注,而是系统事实

不少小工具把状态做成一个可以随意编辑的文本字段。管理员输入“通过”,系统就当它通过;输入“退回”,列表就换一种颜色。这种实现看似灵活,实际上把业务规则交给了拼写。有人写“已通过”,有人写“通过”,有人误删一个字,统计、筛选和权限判断就会失去依据。

状态应当使用有限、固定的值,并由后端按合法动作修改。例如数据库内部可以保存 DRAFTPENDINGRETURNEDAPPROVEDARCHIVED,页面再显示对应的中文。数据库还应使用非空、唯一、外键或检查约束,把“必填”“不能重复”“引用对象必须存在”这类基本规则放到数据层兜底。PostgreSQL 的约束文档列出的这些机制,正是防止无效数据悄悄进入系统的最后一道门。

另一个容易忽略的字段是版本。申请被退回修改后,原内容不应无痕消失。至少要保存修改前后的版本号、修改人和修改时间;重要流程还可以保存字段差异。这样,审核人看到的不只是“又提交了一次”,而是知道申请人具体补了什么。

每个状态只允许少数合法动作

状态机的核心不是状态列表,而是“从这里能做什么”。草稿可以编辑、删除、提交;待审核可以通过或退回,但通常不能由提交人继续改;退回修改可以重新编辑和提交;已通过原则上不能直接删;已归档只允许查看和导出。任何没有被明确允许的跳转都应拒绝。

这条规则必须在服务端执行,不能只靠前端隐藏按钮。用户可能打开旧页面、重复点击,也可能直接调用接口。OWASP 的授权检查建议强调默认拒绝、每次请求都验证权限。落实到审批工具,就是接口收到“通过”请求时,不只检查操作者是不是审核人,还要确认这条申请此刻确实处于待审核状态。

因此,接口最好表达业务动作,而不是让客户端任意改字段。与其提供“把 status 改成任意值”的通用接口,不如提供提交、退回、通过、撤回、归档等明确动作。动作越具体,校验条件、审计内容和错误提示越容易写清楚。

角色和人员分开:提交人、审核人、管理员

“张老师可以审核”不是一条稳定的系统规则,因为人员会调岗,工作会交接。系统应该先定义角色,再把人员放进角色。提交人只能管理自己的草稿和退回件;审核人处理分配给本部门的待审核申请;管理员负责配置、异常恢复和账号管理,但不应默认代替所有人完成业务审核。

NIST 对基于角色的访问控制的解释中,权限通过角色分配,角色对应组织中的工作职能。对校园小工具而言,这比在代码里写死某个姓名、手机号或账号可靠得多。部门负责人更换时,只需调整角色成员,不必修改程序。

提交人、审核人和管理员拥有不同动作权限的示意图
角色对应工作职责,人员只是临时承担角色。每个请求仍要按当前角色、数据归属和状态重新校验。

角色也不是越多越好。第一版常用的三个角色已经够用:普通提交人、业务审核人、系统管理员。如果某项流程确实需要年级、部门和校级多级审核,再增加审核层级。不要为了“以后可能用到”一次性设计十几种权限,否则配置本身就会变成新的管理负担。

退回、撤回和驳回不是一回事

这三个动作在口头交流中常被混用,放进系统却必须分开。退回意味着材料暂时不完整,申请人修改后可以再次提交;撤回由申请人发起,表示在审核完成前主动收回;驳回则表示本次申请不成立,流程结束,若要再办应重新发起。三者的责任人、后续路径和统计含义都不同。

退回时必须填写具体原因,最好能指向缺失字段或附件。仅写“请完善”会把系统里的问题重新变成聊天追问。撤回要限定时机:已经通过甚至已经执行的事项,不能由提交人一键撤回。驳回应谨慎开放,并与“退回修改”在界面上明显区分,避免审核人点错后让整条申请直接终止。

还要为误操作保留恢复路径,但恢复不能等于直接改数据库。可以由管理员发起“异常重开”,填写原因,并产生一条独立审计记录。这样既能处理现实中的特殊情况,也不会让流程规则形同虚设。

一张表定义状态、动作和证据

在让 AI 生成代码前,先完成下面这张表。它既是需求说明,也是接口测试清单。每一行都要能回答:谁在什么状态下执行什么动作,系统校验什么,成功后留下什么。

当前状态

允许角色

业务动作

下一状态

必须留下的证据

草稿

提交人

提交申请

待审核

提交人、提交时间、内容版本

待审核

审核人

退回修改

退回修改

退回人、原因、时间、原版本

退回修改

提交人

修改后重交

待审核

新版本、字段差异、重交时间

待审核

审核人

审核通过

已通过

审核人、意见、时间

已通过

归档人员

确认归档

已归档

归档人、归档编号、时间

表里最好再补上失败情况:非本人修改草稿、普通用户审核、重复提交、已归档记录再次通过、缺少退回原因。把这些写成自动化测试后,生成式工具每次改代码都能重新检查,而不是靠人把所有按钮再点一遍。

通知、超时和并发要从流程里长出来

通知不是每次保存都发一条消息,而是状态发生关键变化时提醒下一位责任人。提交后通知审核人,退回后通知提交人,通过后通知执行或归档人员。通知内容要带申请编号、当前状态和可执行动作;通知失败不能回滚已经成功的审批,但应进入重试队列,并让管理员看见失败记录。

超时也应绑定状态。例如待审核超过三个工作日,先提醒审核人;继续超时再提醒其上级或流程管理员。不要一开始就把复杂的催办规则写死,可以先记录“进入当前状态的时间”,等真实运行一段时间后,再依据积压情况设定阈值。

并发问题则常在上线后才出现:两位审核人同时打开同一申请,一人通过,另一人稍后又退回。如果后一次操作直接覆盖前一次,申请就会倒着走。处理方式可以是事务中的行锁,也可以在更新时比较版本号;不论采用哪一种,都要保证“我审核的仍是刚才看到的那个版本”。PostgreSQL 的显式锁文档说明了行级锁如何阻止同一数据上的冲突更新。

审计记录要能还原事情经过

业务表只保存“现在是什么”,审计表负责回答“之前发生过什么”。每次动作至少记录申请编号、原状态、新状态、动作、操作者、时间、理由和内容版本。记录应追加而不是覆盖,普通用户不能修改或删除。导出、批量下载、管理员代操作等高影响行为,也应该留下记录。

审批申请按时间顺序留下操作和版本记录的审计轨迹示意图
审计轨迹不是为了“多记日志”,而是让一次退回、修改、通过或异常恢复能够被完整还原。

OWASP 的日志建议把增加、修改、删除、导出以及能够还原操作顺序的审计轨迹列为重要事件。同时也提醒日志不能什么都记:密码、令牌、敏感个人信息不应原样进入日志。学校工具里的学生信息、联系方式和附件地址尤其要控制记录范围与查看权限。

有了可信的审计记录,争议处理才不必依赖聊天截图。管理者可以看到申请在何时卡住,开发者可以定位重复操作,教师也能知道材料到底被谁退回、为什么退回。这些价值往往比首页那几张漂亮的统计卡片更实在。

先做最小闭环,再加统计和 AI

第一版只要完成一个可靠闭环:登录后按角色看数据,提交人能保存草稿并提交,审核人能通过或退回,退回件能修改重交,所有动作有审计记录,刷新和重新登录后状态仍然正确。再补上必要的附件限制、数据导出和备份,这个工具就已经可以解决真实问题。

统计应当从稳定状态中生成,例如各部门待审核数量、平均处理时间、退回原因分布。AI 可以放在更靠后的位置:帮助检查必填材料、归纳退回原因、生成汇总说明,但不能绕过权限,也不能替审核人自动做最终决定。输入给模型的数据要经过最小化和脱敏,模型输出仍然作为建议由人确认。

给 Codex 或 OpenCode 的任务也应分阶段:先根据状态表生成数据模型和迁移;再实现动作接口与权限测试;随后做页面;最后接通知和统计。每完成一层就用真实角色走一遍流程。这样生成式工具是在落实已经说清楚的规则,而不是一边写代码一边替学校猜规则。

校园里的小工具不需要一开始就成为“综合管理平台”。先让一件事从提交到归档走得清楚、可靠、可追溯,第二条流程才有可以复用的账号、权限、状态和审计基础。CRUD 只是把数据放进去,状态流转才真正把工作接起来。