首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python 接口兼容示例:字段默认值、版本校验与幂等回放

Python 接口兼容示例:字段默认值、版本校验与幂等回放

原创
作者头像
微订技术团队
修改于 2026-10-19 21:58:08
修改于 2026-10-19 21:58:08
570
举报

一个楼栋分配接口最初只接收楼栋编号和对象版本。后来需要增加备注字段,又不能要求所有旧客户端同时升级。字段能解析只是第一步:旧请求重试时,新版服务还需要判断它是不是同一次业务,并返回正确的历史结果。

本文用一个只依赖Python标准库的示例,讨论请求归一化、API版本、对象版本和幂等回放如何配合。代码使用单进程内存存储,目的在于复现接口行为,不提供重启后恢复或多进程一致性保证。

1. 三种标识不能混用

字段

它回答的问题

示例处理

api_version

这份请求按哪套字段规则解析

不支持的版本直接报错

expected_version

操作是否仍基于当前对象

首次执行时不匹配则冲突

request_id

这次业务操作以前是否处理过

同键同内容回放原结果

API版本改变不一定代表业务含义改变。例如v2新增remark,可约定旧客户端未传时按空字符串处理。只有当“未传”和“空字符串”在业务上含义一致时,才适合做这样的兼容。

对象版本则用于检查写入时机。两次不同操作即使使用同一API版本,也不能同时覆盖同一个旧对象版本。

2. 先归一化,再计算请求摘要

示例将v1的默认输入:

代码语言:json
复制
{"building_id": "B2", "expected_version": 1}

与v2的显式默认输入:

代码语言:json
复制
{"api_version": 2, "building_id": "B2", "expected_version": 1, "remark": ""}

归一化成相同的业务对象。字段排序和紧凑JSON编码只是稳定序列化的方法,真正决定是否等价的是字段默认值及业务语义。这里刻意不把api_version放入摘要,因为两个受支持适配器得到的是同一个操作;如果版本升级改变了语义,就不应沿用这个规则。

未知字段会报错,避免把拼错的字段静默丢弃。例如客户端把remark写成remarks,不能假装已经保存了备注。

3. 一份可运行的接口核心示例

保存下面的完整代码为demo.py,运行python demo.py即可。actor模拟服务端认证上下文,不是允许客户端填写的请求字段。示例只处理校区C1的一项分配对象。

代码语言:python
复制
import copy
import hashlib
import json
from threading import RLock

class Demo:
    def __init__(self):
        self.lock = RLock()
        self.version = 1
        self.saved = {}
        self.applied = 0

    def call(self, actor, request_id, body):
        # actor is server-side authentication context, not request JSON.
        if not actor["allowed"] or actor["campus"] != "C1":
            raise PermissionError("scope denied")
        if not isinstance(request_id, str) or not request_id.strip():
            raise ValueError("request_id required")
        api = body.get("api_version", 1)
        if type(api) is not int or api not in (1, 2):
            raise ValueError("unsupported API version")
        allowed = {"api_version", "building_id", "expected_version"}
        if api == 2:
            allowed.add("remark")
        if set(body) - allowed:
            raise ValueError("unknown field")
        building = body.get("building_id")
        expected = body.get("expected_version")
        remark = body.get("remark", "")
        if not isinstance(building, str) or not building.strip():
            raise ValueError("building_id required")
        if type(expected) is not int or expected < 1:
            raise ValueError("expected_version required")
        if not isinstance(remark, str):
            raise ValueError("remark must be string")
        semantic = {"building_id": building.strip(),
                    "expected_version": expected, "remark": remark}
        digest = hashlib.sha256(json.dumps(
            semantic, ensure_ascii=False, sort_keys=True,
            separators=(",", ":")
        ).encode("utf-8")).hexdigest()
        key = (actor["campus"], actor["id"], request_id)
        with self.lock:
            prior = self.saved.get(key)
            if prior:
                if prior["digest"] != digest:
                    raise ValueError("idempotency key conflict")
                result = copy.deepcopy(prior["result"])
            else:
                if expected != self.version:
                    raise ValueError("object version conflict")
                self.version += 1
                self.applied += 1
                result = dict(semantic, applied_version=self.version)
                self.saved[key] = {
                    "digest": digest, "result": copy.deepcopy(result)}
            return {"result": result, "current_version": self.version}

def expect_error(error, action):
    try:
        action()
    except error:
        return
    raise AssertionError("expected " + error.__name__)

def main():
    demo = Demo()
    actor = {"id": 7, "campus": "C1", "allowed": True}
    old = {"building_id": "B2", "expected_version": 1}
    first = demo.call(actor, "req-1", old)
    compatible = dict(old, api_version=2, remark="")
    assert demo.call(actor, "req-1", compatible) == first
    demo.call(actor, "req-2", {
        "api_version": 2, "building_id": "B3",
        "expected_version": 2, "remark": "east entrance"})
    replay = demo.call(actor, "req-1", old)
    assert replay["result"]["applied_version"] == 2
    assert replay["current_version"] == 3
    expect_error(ValueError, lambda: demo.call(
        actor, "req-1", dict(old, building_id="B9")))
    expect_error(ValueError, lambda: demo.call(
        actor, "req-1", dict(old, api_version=99)))
    expect_error(PermissionError, lambda: demo.call(
        dict(actor, allowed=False), "req-1", old))
    assert demo.applied == 2
    print("compatible replay: same saved result")
    print("saved/current version:", 2, 3)
    print("changed payload: rejected")
    print("unsupported API version: rejected")
    print("revoked permission: rejected")
    print("applied operations:", demo.applied)

if __name__ == "__main__":
    main()

幂等键由校区、操作者和请求编号组成。同一个请求编号在不同作用域下不应串用结果;真实接口仍需验证操作者是否有权访问具体分配对象,不能仅凭这三个字段就认定授权完成。

RLock让这个单进程例子中的“查历史、检查版本、写结果”不可被其他线程插入。它不是分布式锁,也不能代替数据库持久化事务。部署为多个进程或多个节点时,需要把业务变更和请求结果放进同一个持久化原子边界。

4. 为什么先查历史结果,再比较当前对象版本

首次req-1把对象从版本1更新到版本2,并保存applied_version=2。接着req-2合法地把对象更新到版本3。

此时旧客户端再次发送req-1,原请求里的expected_version仍为1。如果服务端先拿它和当前版本3比较,就会把一次成功操作的重试误判成新的写入冲突。

示例先在幂等记录中查找相同作用域和请求键:参数摘要一致就回放原result,不再执行第二次更新;同时用current_version告诉客户端当前对象已经到版本3。两个版本有不同含义,不能用最新状态覆盖历史结果。

返回前对保存结果做深拷贝,防止调用方修改响应对象后连带污染内存中的历史记录。

5. 本地实际验证的六种行为

本次在Windows环境使用Python 3.10.11运行,得到:

代码语言:txt
复制
compatible replay: same saved result
saved/current version: 2 3
changed payload: rejected
unsupported API version: rejected
revoked permission: rejected
applied operations: 2

其中包含以下断言:v1请求和带默认备注的v2请求可回放同一结果;后续对象版本变化不会改变旧请求保存的applied_version;同键更换楼栋会被拒绝;未知API版本会被拒绝;权限撤销后即使有历史结果,也不能继续读到它;两次独立有效操作只产生两次业务变更。

权限检查放在读取幂等结果前面,避免“有缓存就跳过授权”。同样,API版本和字段解析也要先通过,不能让任意格式的请求只凭一个旧request_id取得结果。

6. 持久化时要补齐的部分

内存记录会在进程退出后消失,因此这份演示不能直接用于需要可靠重试的生产接口。持久化实现至少需要为作用域与请求键设置唯一约束,并把业务更新、参数摘要和保存结果放在同一事务内。

历史结果保留期限要覆盖实际重试窗口。若准备清理记录,应明确过期请求如何识别和处理;只删除幂等记录而保留可再次执行的旧业务入口,可能使迟到请求被当成新操作。

外部学校服务调用也不在本例的原子边界中。若一次分配需要外部系统确认,还要增加调用意图、外部幂等键或结果查询,并区分“明确失败”和“结果尚未确认”。

兼容改造应验证具体请求和具体结果。这份示例只证明上述本地接口行为,不将其推广成线上稳定性、性能或完整权限体系已经通过验证。

作者:微订(上海逊柯计算机科技有限公司)。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 1. 三种标识不能混用
  • 2. 先归一化,再计算请求摘要
  • 3. 一份可运行的接口核心示例
  • 4. 为什么先查历史结果,再比较当前对象版本
  • 5. 本地实际验证的六种行为
  • 6. 持久化时要补齐的部分
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档