关于 quality-code-review
这是面向 Frappe 应用审查者的代码评审清单,按正确性、安全性、性能、并发和可维护性组织检查要点。它强调识别根因、说明问题影响,并优先保护数据与事务完整性。
主要功能
- 01按风险顺序组织审查
- 02检查事务与数据完整性
- 03覆盖注入与访问控制风险
- 04检查性能和数据库查询
- 05审视并发与代码可维护性
适用场景
- 01评审 Frappe 应用变更
- 02检查账务或库存代码的状态风险
- 03审查认证、权限和输入处理
- 04识别慢查询与并发问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ frappe/ skills --skill quality-code-review安装后在 Claude Code 中输入/quality-code-review确认已加载。
详细介绍
代码质量审查
供 Frappe 应用审查者使用的检查清单。按后果严重程度依次保障正确性、安全性和未来的维护者。以下各节也按此顺序排列——请将大部分注意力放在 §1 和 §2。优先修复根本原因,而非采用权宜之计,并说明问题为何重要(会导致什么故障、影响哪些人)。
1. 正确性与稳定性(后果最严重)
最严重的 Bug 是悄无声息的状态数据损坏——过账的分类账/库存数据错误,却没有报错。将有状态代码以及法律、会计和合规相关代码视为**“绝不能失败”**的代码。
- 尽早、大声地失败。 使用断言检查内部不变量:
assert total_credit == total_debit.(断言用于代码保证成立的不变量,而非面向用户的验证。) - 设想代码会如何出错。 对每项更改都要问:可能在哪里出错?别人会如何误用? 编写不易出错的代码。扩展和覆盖尤其应考虑处理各种失败情况。
- 禁止部分提交。 在事务中途意外调用
frappe.db.commit()/db.rollback()会结束事务并暴露部分状态——发现任何此类调用都要标记。绕过验证进行提交/保存(验证失败时仍发布了没有 SL/GL 分录的文档)属于严重 Bug。 - 不要为了方便而牺牲原子性(例如,添加
autocommit来修复引导过程,会让事务失去原子性)。autocommit只应用于创建架构。 - 维护不变量优先于用户体验。 “可以牺牲用户体验,但必须保证正确性。”不要为了迁就有问题的代码而破坏原本正常工作的代码。
- 修复前先验证问题。 有时正确的做法是“不要修复”。先找出根本原因。
- 注意筛选条件为空或为
None时的破坏性数据库 API。set_value("Site", None, ...)/ 不带筛选条件的db.delete会更新/删除所有行。这些操作必须报错,而不能悄悄对整张表执行。对于任何set_value/delete/get_value,只要名称/筛选条件可能为None、为空或由攻击者控制,就应标记。 - 检查条件中的类型是否匹配。 类型不匹配的比较(字符串与
datetime、字符串与 int)会悄悄地始终不匹配或始终为真——应在边界处显式转换(cint/flt)。 - 确认返回值的形状。 对结果进行索引前,先确认它是否可能为
None/[None, None];注意区分as_dict=1(字典列表)与标量。 - 不要悄悄改变长期沿用的语义。 调用方长期依赖的行为就是一种契约——即使签名没有改变,修改这种行为也会伪装成非破坏性更改,实则造成破坏性变更。
2. 安全
避免漏洞远比安全地修复漏洞容易。应格外严格地审计安全关键代码(身份验证、授权、权限、用户管理)。
注入
- 绝不通过字符串拼接或 f-string 构造 SQL。应使用 ORM 或查询构造器。如果无法避免原始 SQL,应使用参数替换(
frappe.db.sql("... where name = %s", (user,)))——绝不要自行插入参数。更好的做法是,完全避免引入新的原始 SQL:除了注入风险,它还会将代码绑定到单一数据库,而框架旨在保持数据库无关(支持 Postgres)。优先使用frappe.qb。 - 即使使用 ORM,类型混淆也是一种注入途径。 Frappe 接受复杂类型,因此预期为字符串的参数可能以筛选列表的形式传入:将
{"key": ["!=", ""]}传给db.get_value,就能绕过密钥检查。在信任边界验证输入的类型——显式使用isinstance(key, str)。审计每个@frappe.whitelist方法。 - 绝不要自行
eval/exec任何内容。仅使用safe_eval/safe_exec,且要严格限制使用量;并且“safe_exec 并非万能”。绝不要接受客户端提供的方法路径并执行。
沙箱与信任边界
- 沙箱执行(RestrictedPython/
safe_exec)并不可靠安全——应假定存在逃逸方式。安全开关必须位于正确的信任边界:服务器脚本的启用配置必须在 bench 级别,绝不能在站点级别(租户可能启用它并控制整台服务器)。 - 优先使用允许列表,而非禁止列表——禁止列表可以被绕过。不要默认开放所有内容。
访问控制
- “在设置
allow_guest=True之前三思”——这不是绕过真正身份验证/授权的捷径。网页必须先应用权限,再读取或共享数据。优先使用get_list/get_all,而非手写查询。 - 精确限定放宽范围。 验证速率限制/权限例外恰好针对预期主体,而不是例如所有非访客用户。
路径遍历/文件系统
- 优先使用 File doctype API。如果用户输入进入路径,确保它无法遍历(
/../../)到站点文件夹之外。
加密/密钥
- 绝不要自行实现加密;应复用现有实现。验证访客/Webhook 请求的真实性(HMAC)。
- 签名/一次性 URL:使用真正保密的签名值;同时通过时间和首次使用来过期;仅使用 URL 验证,不要使用合并后的表单数据(Frappe 会合并 URL 和表单数据 → 攻击者可利用一个有效签名发起重放攻击)。
- 将密钥存储在密码字段中;绝不能以纯文本存储,也绝不能在日志或错误消息中泄露密钥。
XSS 与 OWASP 其他风险
- 不要将用户输入注入 DOM。即使 XSS 看似微不足道,也应视为严重问题——HTML/JS 注入通常会导致账户被劫持。
- 不要通过清理并丢弃特殊字符来修复 XSS。优先在将值注入 DOM 前进行转义。
3. 性能就是正确性
在代码审查阶段修复性能问题的成本最低;合入的慢代码可能多年都未被发现。性能是一项功能(多尔蒂阈值;人类能感知约 100ms 的延迟)。
- **性能预算:**常见读取 < 100ms;读取 < 1s;大多数写入 < 5s;绝不超过约 10s。高频只读请求的 P99 应约为 1s。缓慢的同步请求会阻塞工作进程(队头阻塞)。
- **复杂度规则:**高频调用的端点必须执行 O(1) 或 O(log N) 的工作量——最坏情况下允许较大的常数因子,但绝不能是 O(N)。统计行数是 O(N),不是 O(1);对大型或经过筛选的表执行
COUNT(*)开销很大。为无界扫描设定上限(例如最近 3 个月,或使用“1000+”哨兵值),不要扫描所有数据。 - 索引就是代码。标记任何针对无索引列的
WHERE/联接/筛选。索引(包括自定义索引)必须提交到代码中,不能临时应用——否则迁移时会丢失。加载表单时,如果要拉取评论/版本/分配信息,这些查询都必须有索引;其中一个查询没有索引,就会导致整个操作变慢。 - 循环中不要调用数据库。“不要编写会在循环中调用数据库的校验。”标记 N+1 模式。对稳定值(UOM、docstatus、status)进行缓存,而不是重新查询。这在后台任务中可以接受,但绝不能用于请求。
- **慢代码的“移除 → 减少 → 复用”阶梯:**如果无法修复慢代码,就移除它、减少调用次数,或进行记忆化。选择合适的缓存范围(
@redis_cache、@request_cache、@site_cache——最后一种如果过度使用,会大量占用内存)。不要在frappe.local或frappe.flags中手动实现缓存:“这样只会引入全新的缓存失效问题。”不要缓存成本微乎其微的工作。 - **内存:**不要把无用内容塞进共享的模块级文件 /
__init__.py/ 类级状态中——它们会一直驻留在内存中。移除未使用的模块级导入(将其移到使用它的函数中)。留意内存泄漏。 - **调整条件判断顺序,让数据库调用最后执行。**在布尔表达式中,先放开销低的内存检查,以便短路求值时完全跳过查询。
- **在 SQL 中聚合,而不是在 Python 中聚合。**在查询中使用
SUM()/COUNT(),而不是先获取所有行再在内存中汇总;将筛选条件推入子查询,使其在联接之前生效。 - **不要为了获取一个值而读取整个文档。**获取单列时,使用
get_value/get_single_value/set_value,而不是get_doc().save();使用frappe.delete_doc,而不是get_doc().delete()(后者只为删除文档就先读取了整个文档)。 - **热路径中不要使用 MyISAM 表。**读取 MyISAM 表会隐式锁定整个表——请求路径中绝不能访问这类表。
- **将耗时操作移至后台队列。**耗时操作应放入
enqueue(..., queue="long"),而不是放在会阻塞工作进程的同步请求中。
4. 并发
- 先检查再操作会产生竞态。
if not frappe.db.exists(...): insert()——两个工作进程都可能看到“不存在”,然后都插入。优先使用数据库级唯一约束;“将完整性保障交给数据库。” - **锁相关陷阱:**对无索引查询执行
SELECT ... FOR UPDATE会锁定所有扫描到的行(以及间隙)——务必确保筛选条件使用索引,否则就会锁住整张表。锁定父项却不锁定其子项,会产生“变异体”文档。 - 全局可变状态 / 类属性在 Python 中都是全局的——共享的引擎/类属性若将查询状态泄漏到并发请求之间,就会产生垃圾结果。构建查询时应保持无状态。不要“在
frappe.local里搞些奇怪的东西”——local用于存放变量,不用于存放静态状态。
5. 可读性与可维护性
约 50% 的开发时间都花在阅读代码上;糟糕的代码最终会迫使团队重写。
- 能保持函数纯粹时就尽量保持纯粹——易于阅读和测试。
- 不要传递可变对象,指望别人填充它们(“组装”代码)——应返回新值。传递一个将被修改的可变对象,会迫使读者打开两个文件才能弄明白一件事。
- 修改输入的函数必须使用恰当的名称。
- 优先选择朴素的写法。 虽然函数式的 map-reduce 单行代码看起来很漂亮,但请直接写一个 4 行的 for 循环。可调试性优先于炫技。
- 一致性优先于个人风格。 一个代码库不应混杂 10 种风格。遵循周围的格式、命名和导入约定;发现变更破坏这些约定时应提出。
- 品味: 重构代码,让边缘情况成为常见情况,从而移除特殊分支。想想:能否更简单?代码能否更少?if-else 是否嵌套过深、杂乱无章?
- 优先扩展共享组件,而不是复制粘贴后各自演变。 同一功能分裂成 3–4 个实现,会拖慢长期开发速度。避免模块间紧密耦合;通过清晰且有文档说明的公共 API 集成。
- 使用文档字符串为公共模块、类和函数编写文档;优先使用类型注解,而不是用文字描述类型(“类型提示好 10 倍”);类型检查器能发现不明显的错误。
- 文档字符串只应提及重要内容。保持简短、切中要点。不要解释从函数名称就能轻易理解的内容。重点说明“为什么”。
- 将无关变更拆分为不同的提交/PR,以便评审聚焦,并保持
git blame和回滚记录清晰。
6. API 设计与向后兼容性
- 最小意外原则: API 的名称和签名应能传达约 90% 的意图;用户不应对其行为感到意外。
- 拒绝接受多种互不相关类型的宽泛/重载参数(字符串/字典/列表/None)。优先使用各司其职的独立函数。谨防隐式回退;使用明确的变体。“正确使用方式依赖圈内知识的 API 是一种负担。”
- 为扩展而设计,而不是为了覆盖。 提供钩子;绝不要在运行时对核心进行猴子补丁(“糟糕透顶”——会妨碍未来的修复),也绝不要复制整个核心文件只改几行(修复不会同步)。
- 对成熟的/公共 API,向后兼容是一项义务。遵循语义化版本;次要版本 = 零项破坏性变更。破坏性变更包括:移除公共函数/字段、调整参数顺序、新增必填参数、更改业务逻辑、移动/重命名文件(导致导入失效)、升级共享依赖。重命名但不保留旧名称作为别名,是一种不必要的破坏。每项破坏性变更都必须附带弃用警告和文档。
- 留意架构变更中的破坏性隐患: 在现有站点添加必填字段、将长期存在的字段设为唯一(需要数据补丁)、更改字段类型但不提供补丁、移除字段。
- 会静默跳过现有站点的架构变更需要数据补丁。 单例 DocType 不会将新字段的默认值同步到现有站点;更改字段类型(例如 text→int)也不会转换现有值——这两种情况都需要明确的补丁,并在已填充数据的站点上进行测试。
- 新参数应放在末尾,作为带安全默认值的关键字参数(使用
None,而不是""),以免破坏现有的位置参数调用。重命名时,应将旧名称保留为转接函数:def old_name(...): return new_name(...)。 - 补丁规范。 数据补丁必须幂等(可安全重复运行)、顺序正确(在所读取的字段/doctype 创建后运行),并位于正确的应用中(框架变更应在框架中添加补丁,而不是在下游应用中)。
- 修改现有测试是个危险信号。 如果要让变更通过测试就必须修改现有测试的断言,那你很可能破坏了真实工作流——应明确说明理由,而不是迁就测试。
7. 测试
- 每个 PR 都需要足够的测试覆盖率——关注差异部分的补丁覆盖率,而不只是项目覆盖率。(Frappe 的目标:差异部分中有 85% 的代码行得到覆盖。)测试应覆盖最常用的业务场景。
- **为每个修复添加回归测试。**如果修复 Bug 时没有添加能捕获该问题的测试,就可能导致回归。对于后果极其严重的代码(有状态/合规性相关),不要止步于示例测试——应使用基于属性的测试(Hypothesis)。
- **指出缺少迁移/数据补丁覆盖的情况。**架构变更和数据补丁是风险最高、测试最少的部分;如果变更涉及字段调整或数据迁移,就需要在数据已填充的真实站点上测试补丁(空表总能“成功迁移”,即使变更无效)。
- **测试必须具有确定性且相互独立。**不要使用
random(会导致测试不稳定);不要依赖其他测试遗留的状态(避免顺序依赖);对于依赖时间的逻辑,请使用freeze_time。
8. 错误消息、日志记录与可观测性
- **错误消息质量是合理的审查项目。**标题必须具体且便于通过 Google 搜索(绝不能用“Message”/“Error”)。将字段名作为字段引用。说明发生了什么变化:哪一行、哪个字段,以及变化前后的值(
1 → 2)。用户必须知道“数量从多少变成了多少?” - 向受影响方说明故障——“只有当用户知道邮件设置已损坏时,邮件设置出问题才是用户的问题。”
- **记录日志。**保留回溯信息/异常上下文(能让调试容易几个数量级)。记录具有明确责任主体的破坏性/管理员操作(谁、何时、从哪里操作),并将日志持久化到临时容器之外。
评论
支持 Markdown