第三十八章 REST API 与 Webhook
篇别:第九篇 API 与系统集成
本章学习目标
- 基于
http.Controller设计 REST 风格 资源(动词、状态码、分页、错误体)。 - 实现 可靠 Webhook(异步、重试、签名、幂等),避免 阻塞主事务。
- 归纳 支付、物流、CRM 等 第三方集成 的 通用模式(验签、查单、补偿)。
- 能对比 OAuth2 与 API Key 在 Odoo 自定义 API 中的取舍。
导读:REST 与 Webhook 是「产品级接口」
RPC 贴近 ORM,适合 内部工具;REST 更易被 异构系统、网关、API 管理平台 消费。Webhook 则是 事件驱动:出库、付款、工单关闭时 推送 外部 URL。本章与 第三十六章(路由类型)、第三十章(CSRF、鉴权)强相关:公开 Webhook 端点 往往是 攻击面热点。
38.1 REST API 设计
38.1.1 知识要点
- 资源命名:
/api/v1/library/books复数 表示集合;/api/v1/library/books/<int:id>单条。 - 动词:GET 只读、POST 创建、PUT/PATCH 更新、DELETE 删除;避免 用 GET 改状态。
- 状态码:200/201 成功、400 参数、401/403 鉴权、404 不存在、409 冲突、500 服务器(少暴露细节)。
- 响应体:统一
{ "data": ..., "error": null }或 Problem Details 风格;分页 用limit、offset或cursor。 - 鉴权:Header API Key、Bearer JWT、OAuth2(客户端凭证);勿 把密钥放 query string(日志泄露)。
- 版本:URL
/v1/直观;Accept-Version头 灵活;团队选一种 并 文档化。
38.1.2 案例
from odoo import http
from odoo.http import request
class LibraryRest(http.Controller):
def _check_api_key(self):
token = request.httprequest.headers.get("X-API-Key")
if token != request.env["ir.config_parameter"].sudo().get_param("library.api_key"):
return request.make_json_response({"error": "unauthorized"}, status=401)
return None
@http.route("/api/v1/library/books", type="http", auth="none", methods=["GET"], csrf=False)
def list_books(self, limit=20, **kw):
err = self._check_api_key()
if err:
return err
limit = min(int(limit), 100)
Book = request.env["library.book"].sudo() # 演示:生产应使用真实用户 env + 规则
ids = Book.search([], limit=limit)
data = ids.read(["name", "isbn"])
return request.make_json_response({"data": data})
@http.route("/api/v1/library/books/<int:book_id>", type="http", auth="none", methods=["GET"], csrf=False)
def get_book(self, book_id, **kw):
err = self._check_api_key()
if err:
return err
book = request.env["library.book"].sudo().browse(book_id)
if not book.exists():
return request.make_json_response({"error": "not_found"}, status=404)
return request.make_json_response({"data": book.read(["name", "isbn"])[0]})
注意:上例
sudo()仅教学;生产须auth+ 记录规则 或 专用集成用户。
38.1.3 截图占位

38.1.4 本节练习
- 实操:实现
GET /api/.../books/<id>在 不存在时返回 404 JSON。 - 简答:为何 DELETE 常返回 204 无 body 或 200 + 删除摘要?各利弊?
参考答案提示:2. 204 省流量;200+摘要 便于 客户端日志与审计。
38.2 Webhook 机制
38.2.1 知识要点
- 异步:
write/button内 同步requests.post易 拖慢事务、失败回滚难;宜用queue.job(OCA)、ir.cron、bus、消息队列。 - 重试:指数退避 + 最大次数;4xx 多 勿重试(除 429),5xx / 超时 可重试。
- 签名:HMAC-SHA256(body, secret) 放
X-Signature头;接收方 验签 后再处理。 - 幂等:事件 id 去重表;重复投递 不 二次扣款 / 二次发货。
- 观测:DLQ(死信) + 管理界面 查看 失败原因。
38.2.2 案例
import hmac
import hashlib
def sign_body(secret: bytes, body: bytes) -> str:
return hmac.new(secret, body, hashlib.sha256).hexdigest()
# 发送方
# requests.post(url, data=raw, headers={"X-Signature": sign_body(secret, raw)}, timeout=5)
38.2.3 截图占位

38.2.4 本节练习
- 简答:为何 webhook 不应 在
write里同步阻塞 主事务? - 实操:设计 一张
webhook.delivery模型字段(url、payload、state、next_retry_at、error)。
参考答案提示:1. 锁持有长、用户超时、失败难补偿、级联重试风暴。
38.3 第三方系统集成
38.3.1 知识要点
- 支付回调:验签 → 查单(主动调支付平台)→ 幂等更新本地状态;不信任 回调 body 单独 作为真相。
- 物流:运单号回写 + 轨迹轮询 分离;Portal 展示 走 只读 API。
- CRM 双向:主数据(客户)定 系统 of Record,避免 双写冲突。
- 配置:密钥 放
ir.config_parameter或 专用模型 + 权限组;勿 提交 Git。
38.3.2 案例(文字流程)
支付成功回调:接收 POST → 验签 → search 本地订单 by 第三方单号 → 若已 paid 则 200 返回 → 否则 write 状态并 post 消息 → 记录 audit log。
38.3.3 截图占位

38.3.4 本节练习
- 案例:用文字写出 支付回调 验签 → 查单 → 幂等更新 三步。
- 简答:主动查单 相对 仅信回调 的价值?
参考答案提示:2. 防伪造回调、防重复、对齐平台终态。
本章综合练习
- 安全:对比 OAuth2(客户端凭证) 与 静态 API Key(轮换、粒度、审计 各一条)。
- 版本:URL
/v1/与Accept-Version协商的 适用团队规模 对比(简答)。 - 综合:Webhook 接收端 列出 四条 必备校验(签名、IP、幂等、限流 等)。
- 实操:为 38.1 列表接口写
HttpCase:无效 API Key → 401。
本章对应白皮书目录:第三十八章 REST API 与 Webhook。