备注接口返回成功,数据库却没变:怎样限定一次修复的范围?

AI SaaS 工程诊断实战(二)

备注保存接口回了 success: true,重新读取订单,备注却还是空的。这类问题不难造出来,也很容易被一次"接口正常"的检查放过去。

我用一个 Python 与 SQLite 的最小程序,把失败基线、修复位置和相关回归放在一起。本文是教学构造案例,故意省略了写入语句,不是客户生产故障,也不代表某个 AI Coding 工具实际生成过这段错误代码。

先确认失败发生在哪一步

订单 A 已经确认,订单 B 是另一条不应受影响的数据。我们只想给 A 保存备注。故障函数直接把输入拼回响应:

python 复制代码
def broken_save(db, order_id, note):
    return {'success': True, 'note': note}

看接口返回,备注存在;看数据库,备注没有变化。响应里的 note 来自本次输入,不是保存后的重新读取,因此它不能当作持久状态的证据。

这时我会先固定一组失败基线:确认 A 两次,保存一次备注,再直接查询 orders 和 events。先知道哪些原有行为成立,再决定补丁应该保护什么。否则"顺手重构一下确认逻辑"很容易把一次写入修复扩大成另一项任务。

补丁要回答三个问题

第一,更新哪一行?条件是订单 ID,不能省掉 WHERE。

第二,值怎样传进去?使用参数占位符。示例特意使用带单引号的备注,避免只拿简单英文测试。

第三,找不到订单怎么办?这里的教学规则是抛出 LookupError,由调用方转换成合适的错误响应,不假装保存成功。

python 复制代码
def save(db, order_id, note):
    with db:
        changed = db.execute(
            'UPDATE orders SET note=? WHERE id=?',
            (note, order_id)).rowcount
        if changed != 1:
            raise LookupError(order_id)
    return {'success': True, 'note': note}

成功响应放在事务块之后。订单 ID 是主键,本例以受影响行数检查目标是否存在。这里没有把空字符串视为错误:本例允许清空备注;如果你的业务不允许,先改规则,再改检查。

这次补丁只负责写入 note,不改变状态机,不增加确认事件,不重命名接口字段。真实项目若还存在权限、审计或输入长度要求,应将它们纳入明确范围,不能因为这个示例没写,就默认它们不重要。

回归为什么选这七项

检查 保护的行为
首次写入后读取 原失败是否消失
再次保存覆盖原值 更新不是仅首次插入有效
空字符串清空 本例允许的边界输入
未知订单拒绝 没有目标时不报成功
确认状态保留 备注修改不倒退业务状态
确认事件仍为一条 没有引入重复副作用
B 订单保持原样 更新条件没有误伤其他对象

这七项不是通用的固定测试清单,而是从这次改动及相邻行为推出来的。更换业务规则后,这张表也需要变。

实际执行结果是:故障基线的首次写入、覆盖和未知订单拒绝未通过;修复版七项通过。注意基线中的"空字符串清空"可能碰巧通过,因为原值本来就是空的。一个通过结果,必须连同前置状态一起读。

完整演示

保存为 demo.py,用 Python 3.12 执行 python demo.py。仅使用标准库。程序分别运行故障版和修复版,打印逐项结果,并用断言检查关键预期。下方代码与本次运行材料一致。

python 复制代码
import json
import sqlite3


def new_db():
    db = sqlite3.connect(':memory:')
    db.executescript('''
        CREATE TABLE orders(id TEXT PRIMARY KEY, status TEXT, note TEXT);
        CREATE TABLE events(order_id TEXT, action TEXT);
        INSERT INTO orders VALUES ('A', 'draft', '');
        INSERT INTO orders VALUES ('B', 'draft', 'keep');
    ''')
    return db


def confirm(db, order_id):
    with db:
        changed = db.execute(
            "UPDATE orders SET status='confirmed' WHERE id=? AND status='draft'",
            (order_id,)).rowcount
        if changed:
            db.execute("INSERT INTO events VALUES (?, 'confirm')", (order_id,))


def broken_save(db, order_id, note):
    return {'success': True, 'note': note}  # Deliberate teaching defect.


def save(db, order_id, note):
    with db:
        changed = db.execute(
            'UPDATE orders SET note=? WHERE id=?', (note, order_id)).rowcount
        if changed != 1:
            raise LookupError(order_id)
    return {'success': True, 'note': note}


def run_checks(writer):
    db = new_db()
    try:
        confirm(db, 'A')
        confirm(db, 'A')
        response = writer(db, 'A', "O'Reilly 地址")
        first = db.execute("SELECT note FROM orders WHERE id='A'").fetchone()[0]
        writer(db, 'A', 'updated')
        second = db.execute("SELECT note FROM orders WHERE id='A'").fetchone()[0]
        writer(db, 'A', '')
        empty = db.execute("SELECT note FROM orders WHERE id='A'").fetchone()[0]
        try:
            writer(db, 'missing', 'x')
            unknown_rejected = False
        except LookupError:
            unknown_rejected = True
        checks = {
            'first_write': first == "O'Reilly 地址",
            'overwrite': second == 'updated',
            'clear_allowed': empty == '',
            'unknown_rejected': unknown_rejected,
            'status_kept': db.execute("SELECT status FROM orders WHERE id='A'").fetchone()[0] == 'confirmed',
            'one_confirm_event': db.execute("SELECT COUNT(*) FROM events WHERE order_id='A'").fetchone()[0] == 1,
            'other_order_kept': db.execute("SELECT status,note FROM orders WHERE id='B'").fetchone() == ('draft', 'keep'),
        }
        return {'response': response, 'checks': checks}
    finally:
        db.close()


if __name__ == '__main__':
    baseline, repaired = run_checks(broken_save), run_checks(save)
    assert not baseline['checks']['first_write']
    assert all(repaired['checks'].values())
    print(json.dumps({'baseline': baseline, 'repaired': repaired}, ensure_ascii=False, indent=2))

这次通过还不能说明什么

程序使用同进程内存数据库,没有页面和 HTTP 层,也没有鉴权、并发、崩溃恢复及重启持久化验证。它支持的结论是"在这些输入与检查下,备注写入修复有效且相关样例没有出现回归",还不是"整个订单系统可以上线"。

给 AI Coding 工具交任务时,可以把范围写得很具体:"修复备注未写入;保留确认状态和事件行为;给出修改前失败、修改后通过的存储读取结果;说明未覆盖项。"真正执行完,再核对差异与运行记录。提示词只是任务约定,不会自动保证工具遵守范围。

关于参数绑定和事务上下文的具体行为,可查 Python sqlite3 官方文档。

------ Linksass|AI SaaS 独立开发者。本文由我确定选题与交付范围,使用 AI 辅助整理和制作演示;以上程序已在本地执行,构造案例与验证边界已说明。

相关推荐
Dumbness44 分钟前
用 Python + UI Automation 读写微信 PC 版:一个群消息自动回复程序的实现与踩坑
python
艺杯羹1 小时前
Windows跨平台编码地雷全解:从GBK静默崩溃到内核级原子写入防御
人工智能·python·桌面客户端
用户8356290780511 小时前
使用 Python 在 Excel 中识别并高亮重复数据与唯一数据
后端·python
CCblandy1 小时前
Agent 工具调用怎么编排?本机实测:并行只到 2.63 倍,慢的先发省 30ms,CPU 活加 worker 无效
人工智能·后端·python·性能优化
用户8356290780511 小时前
使用 Python 在 Excel 中查找、替换数据并高亮显示
后端·python
GPU实战笔记1 小时前
PyCharm 接云 GPU:远端项目连接与远程解释器别混为一谈
python·pycharm·ssh·远程开发·云gpu
范中勤1 小时前
Python 协程(Coroutine)通俗讲解与技术原理:从 await 挂起到 Agent 高并发
python·agent·协程·并发编程·asyncio
2601_966949652 小时前
如何判断实时行情数据是否已经更新?从时间戳、交易时段到行情有效性
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
MoRanzhi12032 小时前
106.正则问题(分治)
python·算法·正则表达式·蓝桥杯·递归·分治算法·字符串处理