第三十八章 REST API 与 Webhook

篇别:第九篇 API 与系统集成

本章学习目标

  • 基于 http.Controller 设计 REST 风格 资源(动词、状态码、分页、错误体)。
  • 实现 可靠 Webhook异步、重试、签名、幂等),避免 阻塞主事务
  • 归纳 支付、物流、CRM第三方集成通用模式(验签、查单、补偿)。
  • 能对比 OAuth2API KeyOdoo 自定义 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 风格;分页limitoffsetcursor
  • 鉴权Header API KeyBearer JWTOAuth2客户端凭证); 把密钥放 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 OpenAPI/Swagger 或 README 端点表

38.1.4 本节练习

  1. 实操:实现 GET /api/.../books/<id>不存在时返回 404 JSON
  2. 简答:为何 DELETE 常返回 204 无 body200 + 删除摘要?各利弊?

参考答案提示: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 Webhook 重试队列表(自绘或后台截图)

38.2.4 本节练习

  1. 简答:为何 webhook 不应write 里同步阻塞 主事务?
  2. 实操:设计 一张 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 集成架构示意(Odoo ↔ 网关 ↔ 第三方)

38.3.4 本节练习

  1. 案例:用文字写出 支付回调 验签 → 查单 → 幂等更新 三步。
  2. 简答主动查单 相对 仅信回调 的价值?

参考答案提示:2. 防伪造回调、防重复、对齐平台终态


本章综合练习

  1. 安全:对比 OAuth2(客户端凭证)静态 API Key轮换、粒度、审计 各一条)。
  2. 版本URL /v1/Accept-Version 协商的 适用团队规模 对比(简答)。
  3. 综合Webhook 接收端 列出 四条 必备校验(签名、IP、幂等、限流 等)。
  4. 实操:为 38.1 列表接口写 HttpCase无效 API Key → 401

本章对应白皮书目录:第三十八章 REST API 与 Webhook。