Python 模型与 JSON:保存和恢复订单
今天从哪里开始
昨天已经把规则拆成可调用的函数,但函数收到的字典仍可能缺字段,程序关闭以后内存里的订单也会消失。再加一个业务要求:商品明天改价,今天已经成交的金额必须保持不变。
今天分两段完成:先用模型守住字段、数量与价格快照,再把有效模型转成 JSON 写入文件。最后重新读取,确认恢复后的对象仍符合规则,而不只是磁盘上多了一个文件。
flowchart LR A[昨日规则函数] --> B[明细对象与价格快照] B --> C[JSON保存并重新校验]
今天要学会什么
- 用类、实例、方法和 dataclass 表达订单明细
- 用特殊方法展示、比较和计数,用 StrEnum 集中状态名
- 比较组合、继承与拷贝,保持价格快照
- 用 Path、UTF-8、with 和 JSON 保存恢复并区分错误
课堂结束时,用自己的项目检查:
- 能做出来:修改商品当前价格,证明历史订单金额不变,并展示正常输入的结果。
- 能查出问题:针对“把坏文件当空数据、Decimal 转 float、bool 冒充数量”中的风险,说明输入、预期与实际差异;失败后核对数据没有被误改。
- 能解释修改:提交代码、测试和修改前后结果,讲清自己接受或拒绝了哪一处 AI 建议。
一条订单明细怎样拥有自己的数据和行为
昨天函数的输入是字典,字段写成 price 或 unit_price_yuan 会让调用者理解不一致。我用类给一条明细明确结构,用实例保存这次购买的数据。
from decimal import Decimal
class OrderLine:
def __init__(self, product_id, unit_price, quantity):
self.product_id = product_id
self.unit_price = unit_price
self.quantity = quantity
def total(self):
return self.unit_price * self.quantity
line = OrderLine("P1001", Decimal("129.00"), 2)
print(line.product_id, line.total())class 定义类型;OrderLine(…) 创建实例并调用初始化方法;self 指当前实例;self.quantity 是实例属性;total 是方法,line.total() 会把 line 作为 self 传入。__init__ 初始化实例,不返回金额。先建立第二条明细,修改第二条数量,确认第一条仍为2。
这里是结构最小例,尚未校验;接回项目时 total 复用昨天的 calculate_line_total_yuan,构造时检查商品编号非空、单价为 Decimal、数量为真正正整数。属性不是自动校验器。
魔法方法:让自己的对象接入 Python 已有操作
双下划线特殊方法规定了对象参与内置操作时的行为,不是任意取个特殊名字。商城调试、展示、快照核对与购物车计数分别需要不同含义。
| 操作 | 调用的协议 | 商城用途与边界 |
|---|---|---|
| 创建 OrderLine(…) | __init__ | 初始化这次购买的字段,不等于保存文件 |
repr(line) | __repr__ | 开发者看字段,排查价格与数量 |
str(line)、print(line) | __str__ | 展示“P1001 × 2:258.00 元” |
restored == line | __eq__ | 核对保存恢复后的值相等;不要求是同一个对象 |
len(cart) | __len__ | 约定为商品行数,不是总件数;返回非负整数 |
我先给刚才的类局部加入展示方法:
def __str__(self):
return f"{self.product_id} × {self.quantity}:{self.total():.2f} 元"这是放进类内部的方法片段。运行 print(line) 比较前后,再解释为什么不能在 str 中发起支付或修改库存。特殊方法只承担它约定的职责。
dataclass 减少重复写法,校验仍由我负责
先用普通类理解对象,再让标准库生成重复的初始化、开发展示与值比较。@dataclass 把类交给 dataclass 处理并绑定回来;暂时使用这个已有装饰器,明天再亲手写函数装饰器。
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class OrderLine:
product_id: str
unit_price: Decimal
quantity: int
def __post_init__(self):
if not isinstance(self.product_id, str) or not self.product_id.strip():
raise ValueError("商品编号不能为空")
if not isinstance(self.unit_price, Decimal):
raise ValueError("单价必须是Decimal")
if not self.unit_price.is_finite() or self.unit_price < 0:
raise ValueError("单价必须是有限非负数")
if self.unit_price != self.unit_price.quantize(Decimal("0.01")):
raise ValueError("单价最多两位小数")
if type(self.quantity) is not int or self.quantity < 1:
raise ValueError("数量必须是正整数")这是可独立运行的校验小例。自动 init 赋值后调用 post_init;抛异常会让本次构造失败。frozen 阻止常规字段重新赋值,不会使嵌套列表深度不可变;注解不会执行类型校验。完整项目仍复用金额校验函数,避免两处维护。
运行两个同值实例的 == 与 is:值相等可以为真,对象身份相同为假。再尝试数量True、0和单价NaN,说明拒绝发生在哪一行。repr、eq 可由 dataclass 生成,不必手写所有魔法方法。
订单状态只允许几个名字:StrEnum
把PAID在多处手写成PAIED会制造难查的状态。枚举把一组允许的值集中命名,Python 3.12的StrEnum也能作为字符串使用:
from enum import StrEnum
class OrderStatus(StrEnum):
PENDING_PAYMENT = "PENDING_PAYMENT"
PAID = "PAID"
CANCELLED = "CANCELLED"
status = OrderStatus("PAID")
print(status.value) # PAID
print(status == "PAID") # TrueOrderStatus继承StrEnum,成员名和值由上面的类体声明。未知字符串构造会抛ValueError。把外部字符串转成枚举只能保证名字属于允许集合,不能证明PAID允许取消;状态迁移仍交给昨天的业务函数。
练习:分别传入PAID与PAIED;再用合法的PAID尝试取消,确认“名称合法”和“动作合法”是两道检查。后续流式事件名复用这项语法,不再重新首讲。
购物车包含明细:组合、继承与拷贝的选择
购物车“有多条明细”,因此保存 OrderLine 列表,叫组合。购物车并不是一种订单明细,把 Cart 继承 OrderLine 会误导调用者。
class Cart:
def __init__(self, lines=None):
self.lines = [] if lines is None else list(lines)
def __len__(self):
return len(self.lines)
cart = Cart([OrderLine("P1001", Decimal("129.00"), 2)])
print(len(cart)) # 1行,2件我把每实例的 lines 放进 init。若写成类体里的 lines = [],所有购物车会共用同一列表。list(lines) 复制外层列表,本例明细只读;可变嵌套字典仍需继续观察。
from copy import deepcopy
original = {"items": [{"quantity": 2}]}
shallow = original.copy()
isolated = deepcopy(original)
shallow["items"][0]["quantity"] = 9
print(original["items"][0]["quantity"]) # 9
print(isolated["items"][0]["quantity"]) # 2赋值是别名,浅复制共享嵌套对象,深复制递归复制;价格快照优先明确选取字段,不把所有对象一律深复制。用同一实验解释改商品价格为什么不能改历史订单。
继承表示“是一种”,子类可继承并覆盖父类方法;super() 调用继承链上的下一份实现。先看可独立运行的物流规则小例,课程主项目不额外接物流服务:
class Delivery:
def label(self):
return "配送"
class ExpressDelivery(Delivery):
def label(self):
return super().label() + ":加急"
print(Delivery().label())
print(ExpressDelivery().label())调用方统一调用label,得到“配送”与“配送:加急”,子类覆盖方法并复用父类行为。今天主项目用组合;D05 的 OrderInputError(ValueError) 会实际使用异常继承。方法覆盖与组合是选择,不要求为了知识覆盖给商城制造多层继承。
订单为什么保存价格快照
用户在 9:00 以 129.00 元下单,商品 14:00 改成 139.00 元。历史订单应该显示多少?
我先用独立实验观察快照;下面需在已完成 OrderLine 的 total 方法后运行:
product = {"name": "轻羽无线鼠标", "unit_price_yuan": Decimal("129.00")}
line = OrderLine("P1001", product["unit_price_yuan"], 2)
product["unit_price_yuan"] = Decimal("139.00")
print(line.total()) # 258.00,未追随商品现价累计订单的 OrderItem 还保存 product_name_snapshot 与 unit_price_yuan;本日小例只缩小字段说明快照机制。
这是一种有业务意义的冗余:换取历史事实稳定。Service(服务层) 创建订单时生成快照;CLI 不能提交或覆盖金额计算字段。
客服复核昨天订单时应使用哪个单价?为什么?
使用下单时保存的 129.00 元快照。商品现价服务新交易,历史订单记录已发生的事实,不能随商品改价重算过去金额。
为什么订单保存下单时的单价,而不是随时查商品现价?
历史订单是一笔已发生的交易事实。商品之后改价不能重写过去的金额,因此明细保存单价与名称快照;商品目录和订单快照承担不同职责。
内部不变量和跨对象规则怎样划分
先看五张变化卡:
| 变化 | 应落在哪里 | 为什么 |
|---|---|---|
| 数量必须大于 0 | Model(模型) | 单个明细在任何场景都应成立 |
| 商品是否存在/在售 | Service | 需要查询商品集合 |
| 已支付订单不得取消 | Service | 是跨状态业务动作 |
| JSON 文件路径改变 | Storage/配置 | 是持久化事实 |
| CLI 提示改中文 | CLI | 是外部展示 |
如果只会背“高内聚低耦合”,请回到变化卡:修改一条规则时,哪些文件不应该被迫改变?
什么是持久化
到这里,对象在内存里已经满足规则。现在关掉程序:对象不会自动留在磁盘。接下来把“有效对象”送过文件边界,再用新的进程把它恢复回来。
持久化是把需要跨运行保留的事实写入进程之外的介质,并在下一次运行重新加载。
读图时先跟着蓝色主链走一遍,再看底部三种失败。文件不存在、JSON(结构化数据交换格式) 损坏和写入失败代表不同问题,后面的代码不能把它们一律处理成空列表。
Python 对象
→ 转成 JSON 可表达的数据
→ 写入 UTF-8 文件
→ 结束进程
→ 新进程读取文件
→ 恢复对象并重新校验文件出现了还不够。我们还要判断:
- 文本是否符合 JSON 语法;
- 顶层形状是否符合 当天约定的 Schema(数据结构约束);
- 恢复后的订单是否满足数量、金额、状态等业务不变量。
路径从哪里开始解释
下面两位同学都说“文件就在 data(数据) 目录”,结果一个能打开,一个报 FileNotFoundError。区别可能不在文件,而在当前工作目录(current working directory,cwd)。
from pathlib import Path
data_path = Path("data/orders.json")
print("cwd:", Path.cwd())
print("target:", data_path.resolve())
print("exists:", data_path.exists())把上面的独立代码保存为你的项目的 labs/d04_path_probe.py,从项目根运行:
python labs/d04_path_probe.py再进入 labs 后运行同一个脚本,比较 target。
判断题
Path("data/orders.json")是相对脚本文件,还是相对当前工作目录?- 为了让代码在同伴电脑上运行,应该硬编码某位同学的用户目录绝对路径吗?
- 文件不存在时,第一条证据应该是改代码,还是打印 cwd 和解析目标?
工程约定
课堂所有命令从项目根目录执行。代码使用相对路径,README(项目说明文档) 写清运行起点。
UTF-8、文本模式和 with
4.1 中文订单怎样进入文件:字符要先变成字节
运营导出的商品名称是中文。Python 中的 str(字符串)表达文字,磁盘保存的是字节。写入时把文字转换成字节叫编码,读回时把字节还原成文字叫解码。双方采用同一种规则,才能读回原来的名字。
"订单"(str) → encode("utf-8") → 字节(bytes) → decode("utf-8") → "订单"
写入文本文件时由 Python 编码 读取时由 Python 解码| 名称 | 是什么 | 企业里什么时候遇到 |
|---|---|---|
| ASCII | 主要表达英文字母、数字和基本符号的编码 | 英文协议、老数据;不能编码“订单” |
| Unicode(统一码) | 给字符分配码点的标准;不是单一文件编码 | 理解为什么同一个汉字能有不同字节表示 |
| UTF-8 | Unicode 的一种编码;ASCII 字符占 1 字节,常用汉字通常占 3 字节 | 本项目新建文本/JSON 文件统一采用它 |
| GBK | 支持大量汉字的传统编码 | 国内旧系统导出文件;先核实导出设置再读取 |
| UTF-16 | Unicode 的另一种编码,常见字符用 2 字节,也有需要 4 字节的字符 | 某些系统导出;按来源约定读取,不凭扩展名猜 |
下面是可单独运行的实验。len() 对字符串数的是码点,对字节串数的是字节;不能用文件字节数直接当中文字数。
text = "订单"
raw = text.encode("utf-8")
print("文字:", text, "长度:", len(text))
print("字节:", raw, "长度:", len(raw))
print("恢复:", raw.decode("utf-8"))
try:
text.encode("gbk").decode("utf-8")
except UnicodeDecodeError:
print("解码失败:写入规则和读取规则不一致。")编码用错可能报 UnicodeDecodeError(解码错误),也可能读出乱码而不报错。先查来源编码、保留原文件,再按正确编码读取;不要用 errors="ignore" 悄悄丢掉字符。encoding="utf-8" 是约定解码方式,不是自动探测或修复乱码。
4.2 文本模式与二进制模式:你要读文字,还是原始字节
文本模式 t 是默认值,"r" 相当于 "rt":读取结果为 str,写入也需要 str,Python 按 encoding 编解码。二进制模式 b 直接读写 bytes,不传 encoding;图片、PDF、文件哈希通常需要原始字节。.txt 后缀不会替你选择模式。
先运行下面的完整例子。mkdir() 创建上层目录;open() 只打开文件,不替你创建目录。此例使用演示文件,w 会覆盖它,不能指向已有订单数据。
from pathlib import Path
path = Path("data/note.txt")
path.parent.mkdir(parents=True, exist_ok=True) # open不会自动创建上层目录
with path.open("w", encoding="utf-8", newline="\n") as target:
target.write("轻羽无线鼠标\n")
with path.open("r", encoding="utf-8") as source:
print(source.read())source.read() 一次读取剩余文本,小文件便于观察;大文件可以 for line in source 逐行处理,避免一次装入内存。读到末尾再 read() 会得到空字符串;重新打开文件会从头读。
在上例之后运行,观察同一个文件的两种结果:
with path.open("rb") as source:
raw = source.read()
print("二进制读取类型:", type(raw))
print("按 UTF-8 解码:", raw.decode("utf-8"))文本模式还处理换行:默认读取会把常见的 \r\n、\r 转为 \n;示例写入指定 newline="\n",让演示文件统一使用 LF 换行。这和编码是两件事。
| 模式 | 行为 | 今天的风险 |
|---|---|---|
r | 读取;文件不存在时报错 | 要区分首次无文件与已有坏文件 |
w | 打开时先截断,再写 | 中断可能留下半文件 |
a | 追加到末尾 | 不能靠拼接多段文本维护一个完整 JSON 对象 |
x | 新建;已存在则报 FileExistsError(文件已存在) | 适合今天第一次保存,防止覆盖旧数据 |
w、a 在文件不存在时会新建;r 不会。w 在打开成功时就截断,哪怕后来没有执行 write()。r/w/a/x 决定读写行为,t/b 决定文字还是字节,可以组合,例如 rb、wb。今天不引入同时读写的 + 模式。
4.3 为什么要用 with:中途失败也要归还文件资源
打开文件会取得文件句柄。代码执行到一半报错时,普通的末尾 close() 可能根本走不到。文件缓冲也可能尚未提交给操作系统。不要依赖程序退出或垃圾回收替自己收尾。
# 接着前面的 path 定义运行:finally 无论成功或异常都会尝试清理。
source = path.open("r", encoding="utf-8")
try:
print(source.read())
finally:
source.close()对文件对象,常用写法可以缩短成 with(上下文管理语句):
with path.open("r", encoding="utf-8") as source:
print("块内关闭了吗:", source.closed) # False:还可以读取
print(source.read())
print("块外关闭了吗:", source.closed) # True:对象变量仍在,文件已关闭逐段读:path.open(...) 打开文件;as source 给文件对象起名;冒号后的缩进块是使用范围;离开块时自动关闭。with 不会让文件内容自动变成 JSON,也不负责校验订单。
现在故意中途失败。下面接着已有 path 运行,异常由外层 except 处理,文件关闭由 with 处理:
try:
with path.open("r", encoding="utf-8") as source:
raise RuntimeError("模拟读取后业务校验失败")
except RuntimeError as error:
print("捕获原因:", error)
print("异常后文件已关闭:", source.closed)记住两条边界:文件 with 不会吞掉异常;关闭文件不等于撤销已经写入的内容。本日使用x模式拒绝覆盖旧快照;临时文件替换留作后续扩展,不能只加一个with就宣称保存安全。
小结:保存中文订单前,先能回答这四问
- 文件给我的究竟是
str还是bytes?看文本/二进制模式。 - 中文按什么规则存、按什么规则读?看来源约定与
encoding。 - 当前操作会不会清空旧文件?看
w,不是看是否已经write()。 - 中途报错谁关闭文件、谁处理错误?分别是文件
with和外层异常处理。
中文订单写入文件时,UTF-8、文本模式和 with 各解决什么问题?
UTF-8 约定字符与字节的转换;文本模式读取和写入字符串;with 管理文件资源,正常结束或异常退出都会执行关闭。编码正确不等于 JSON 结构及业务规则正确。
先认识 JSON、序列化与反序列化
刚才write()接收文字,直接把订单字典交给它会报TypeError(类型错误)。我们需要一种文字格式保存字段和结构,下次才可以恢复。
*** JSON(JavaScript Object Notation,JavaScript对象表示法)是一种数据交换格式***,是一种开放标准的轻量级数据交换格式。对象转成JSON文本叫序列化,JSON文本恢复为对象叫反序列化。
import json
order = {"order_id": "O20260825002", "total_amount_yuan": "258.00"}
text = json.dumps(order, ensure_ascii=False) # 转成字符串,还没有写文件
restored = json.loads(text)
print(type(text), text)
print(type(restored), restored)先观察类型,再组合文件操作。下面保存和读取两段应分两次运行,证明新进程也能取回订单:
from pathlib import Path
import json
path = Path("data/d01_orders_demo.json")
path.parent.mkdir(parents=True, exist_ok=True)
order = {"order_id": "O20260825002", "total_amount_yuan": "258.00"}
# 第一段:x拒绝覆盖已有文件;重演换新文件名,保护旧订单。
with path.open("x", encoding="utf-8") as file:
json.dump([order], file, ensure_ascii=False, indent=2)
# 第二段:在新进程只运行必要导入、path定义和以下读取代码。
with path.open("r", encoding="utf-8") as file:
print(json.load(file))| 函数 | 做什么 |
|---|---|
| dumps / loads | 对象与JSON字符串互转,不操作文件 |
| dump / load | 对象与已经打开的文件交互 |
这一小例只取订单关键字段解释机制,不代替累计订单模型的完整约定。后半课再接回完整商品、客户、订单数据及校验。
Python 类型怎样进入 JSON
JSON 能表达对象、数组、字符串、数字、布尔和 null。Python 的 dict/list/str/int/float/bool/None 有常见对应关系,但 Decimal 不是 JSON 原生类型。
from decimal import Decimal
import json
payload = {"unit_price_yuan": Decimal("129.00")}
print(json.dumps(payload, ensure_ascii=False))先预测,再运行。你会看到:
TypeError: Object of type Decimal is not JSON serializable金额边界由我们明确转换:
from decimal import Decimal
amount = Decimal("129.00")
json_value = format(amount, ".2f")
restored = Decimal(json_value)
assert json_value == "129.00"
assert restored == amount图中有两次人工负责的转换:to_dict() 把 Decimal(十进制数值类型) 变成两位小数字符串,line_from_dict() 把字符串恢复成 Decimal,并再次校验格式。不要使用 float(浮点数类型) 作为中间桥梁。
最容易踩的坑
Decimal(129.00) # 参数先成为 float,可能已带入二进制近似
Decimal("129.00") # 从十进制字符串构造累计商城订单沿用 _yuan 字段;本日最小明细练习沿用 unit_price,单位同为元,迁入累计订单时显式映射。两种边界的JSON金额都必须是类似 "129.00" 的字符串。
明细对象怎样通过 JSON 边界
我给 OrderLine 增加 to_dict 方法,把 unit_price 明确转为两位小数字符串。恢复用模块函数 line_from_dict,先查字段再构造 OrderLine;不把未经校验的 JSON 直接当成对象。
# 放进 OrderLine 类内的方法片段
def to_dict(self):
return {"product_id": self.product_id,
"unit_price": format(self.unit_price, ".2f"),
"quantity": self.quantity}本日最小练习沿用 unit_price 字段,单位仍为元;接入累计订单时明确映射到订单项 unit_price_yuan,不改现有商城接口。恢复步骤:确认 dict、检查必填和额外键、确认价格为规范字符串、转 Decimal、调用已校验的构造函数。
# 本人实现 line_from_dict 后运行
restored = line_from_dict(line.to_dict())
assert restored == line
assert restored is not line这条往返检查同时验证字段和类型;只有“JSON 能读出”不能证明模型合法。日包 exercise.py 给出这两个方法/函数的 TODO,完成后合并到本人 src/models.py。
序列化与反序列化不是业务校验
序列化:把内存数据转换为可保存格式。反序列化:从外部格式恢复内存数据。
import json
text = '{"quantity": 0, "total_amount_yuan": "1.00"}'
payload = json.loads(text)
print(payload)JSON 可以成功解析,但数量 0 和错误总金额仍是无效订单。请把有效性分成三层:
| 层 | 典型失败 | 今天怎样观察 |
|---|---|---|
| JSON 语法 | 少括号、少引号 | JSONDecodeError 的行列 |
| Schema 形状 | 多未知字段、金额类型错误 | 恢复函数拒绝 |
| 业务对象 | 数量 0、金额不一致、未知状态 | 本课前半段的 OrderLine 重新校验 |
今天把前半段的模型校验接到文件恢复之后:读得出 JSON,只完成了第一层检查。
无文件、空文件和坏文件不能共用一条处理
先运行三个实验:
实验 A:首次无文件
path = Path("evidence/d01/missing.json")
print(path.exists())这表示尚未初始化,可以由调用方明确创建空数据集。
实验 B:空文件
path.write_text("", encoding="utf-8")
json.loads(path.read_text(encoding="utf-8"))实验 C:截断 JSON
path.write_text('{"orders": [', encoding="utf-8")
json.loads(path.read_text(encoding="utf-8"))空文件和截断文件都属于“已有文件损坏”。如果这样写:
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {"orders": []}下一次保存会把故障现场覆盖成空数据。这不是容错,而是数据丢失。
正确的项目语义
- 无文件:保留 FileNotFoundError,由用户决定是否首次初始化。
- 空文件、坏 JSON、错误编码、字段不合法:明确报错,原文件不写入。
- 本日 save 使用 x 模式创建新快照,已有目标拒绝覆盖;再次演示换新文件名。
- 今天沿用内置异常。明天用自定义 SnapshotError 统一向 CLI 报告,并保留异常链。
分别创建空文件和截断文件,记录读取前后内容,确认失败没有覆盖。新建测试目录要先存在;生产备份、原子替换、并发写入都不在本日实现范围。
基础文件存储的边界
我把“读写一份 JSON”作为数据库之前的过渡。缺少文件、JSON 损坏和业务字段非法必须区分;已有坏文件不能直接当成空订单后覆盖。临时文件安全替换、备份轮转的完整机制保留为扩展阅读,今天只观察一次直接覆盖失败的反例,不要求搭建文件数据库。
JSON 是可交换的文本格式;它不会自动保存 Decimal 或检查订单状态。模型构造负责运行时校验,序列化负责明确转换,存储函数负责路径和读写,CLI 只负责用户输入输出。
从语法小例合并成自己的订单快照
我按“普通类 → 校验 → 显示与比较 → 组合 → 文件”逐段接回 D03 项目。日包 exercise.py 只提供 TODO;先用 python check_model.py,不提前依赖明天才系统讲的 pytest。完成后将模型、保存恢复函数合并至 src/models.py,保留原有业务函数调用。
核心检查:129.00×2得到258.00,商品现价改成199.00后历史行金额不变;新进程读取快照后金额仍相同;损坏文件仍保留;两个 Cart 不共享列表。独立变化为不同商品/数量,说明 len(cart) 与总件数为何不同。
保存的是价格快照,不等于恢复了整笔订单状态。累计项目还需逐字段保存自己的订单编号、客户、状态和所有明细,并在读取后逐项验证;在日验收中分开登记最小模型与累计工程完成情况。
同一套转换扩展到完整订单
单条价格快照验证通过后,我把同一方法放到订单列表:保留order_id、customer_id、items、total_amount_yuan、order_status和created_at;每条items继续保存product_id、product_name_snapshot、unit_price_yuan、quantity、line_total_yuan。
在 src/order_snapshot.py 中分三个职责:restore_orders校验并返回新的订单列表;save_orders转为JSON可表达的数据并创建新文件;load_orders只读文件再交给恢复函数。金额字符串先转Decimal,再用已有模型重新计算行金额与订单总额,与文件声明值比较。字段缺失、未知状态、重复编号和金额不一致都要拒绝。
# 本人完成累计保存恢复后,从项目根运行这一段。
from pathlib import Path
from src.order_snapshot import save_orders, load_orders
path = Path("orders_snapshot.json")
save_orders(path, orders) # orders为本人已有订单列表;新文件名
restored_orders = load_orders(path)
assert restored_orders == orders先恢复到新变量,全部校验成功再替换当前内存列表,防止读到一半破坏旧状态。独立变化:手改文件总额为1.00,程序应拒绝;原文件内容与旧内存列表都保持原样。这是单进程文件练习,不作为多人共享数据库。
学生自己动手,AI 只能改你允许改的地方
我先保存当前可运行版本,再完成“修改商品当前价格,证明历史订单金额不变”。让 AI 一次只修改负责该行为的文件;修改前预测正常与失败结果,检查差异,再运行回归。每人需要解释一处接受或拒绝的建议。
错误与边界怎样验收
今天重点检查:把坏文件当空数据、Decimal 转 float、bool 冒充数量。保留输入、预期、实际输出和修复后的结果,不能只贴全绿截图。未验证的外部依赖如实记为待验证。
留给下一阶段
今天的完整链是“构造并校验 → 保存价格快照 → 转换成 JSON → 写文件 → 重新读取并校验”。明天把这条链与此前的订单规则接到命令行入口,并用自动测试保护,不能只留下一个孤立的保存示例。