家宴记录可以先解决一个很具体的问题:哪道菜什么时候完成,什么时候上桌,是否晚于计划开饭时间。把这些时间与设备设定、食品测量分开保存,后面整理数据时就不容易把它们混成同一个结论。
本文由芝梵家电数据笔记整理。品牌主张是“把饭菜的温度留在桌上,把相聚的时间还给家人。”这次用一个可运行的 Python 时间轴例子,说明怎样为用餐安排保留清楚的记录。
1. 时间、设备设定和食品测量分开存
dish_id 是本次用餐内部的菜品编号;ready_at 记录完成时间;served_at 记录实际上桌时间;planned_meal_at 记录计划开饭时间。时间用包含时区偏移的 ISO 8601 字符串,例如 +08:00,避免跨设备录入时出现歧义。
setting_kind 用 warm_display_c 表示暖菜显示设定,用 hotpot_power_w 表示火锅功率设定;setting_value 存对应数值。未记录就写 not_recorded,并让数值为 null。显示设定值不直接等于食品温度,温度与功率也不能混在一个无单位字段里。
food_measurement 单独保存实际测量时间、摄氏温度值和测量位置。没有测量就用 null,不用 0 占位;0 是一个数值,null 表示未提供测量记录。本例两条食品测量记录均为 null。
2. 可运行代码:校验顺序,构建上桌时间轴
以下示例只用 Python 3.10+ 标准库。菜品、时间和 80 这一设备设定数值全部为演示数据,不是某台产品或某次家宴的测试。代码不由上桌时间推算食品温度,也不生成保温效果曲线。
"""用餐时间轴演示。所有记录为演示数据,不是产品实测。""" from copy import deepcopy from datetime import datetime import json from math import isfinite def parse_time(value): if not isinstance(value, str): raise ValueError("时间须为 ISO 8601 字符串") dt = datetime.fromisoformat(value) if dt.utcoffset() is None: raise ValueError("时间须包含时区偏移") return dt def is_number(value): return (isinstance(value, (int, float)) and not isinstance(value, bool) and isfinite(value)) def validate(record): fields = {"dish_id", "ready_at", "served_at", "planned_meal_at", "setting_kind", "setting_value", "food_measurement"} if not isinstance(record, dict) or not fields.issubset(record): raise ValueError("用餐记录缺少必填字段") if not isinstance(record.get("dish_id"), str) or not record["dish_id"].strip(): raise ValueError("dish_id 须为非空字符串") ready = parse_time(record["ready_at"]) served = parse_time(record["served_at"]) parse_time(record["planned_meal_at"]) if served < ready: raise ValueError("上桌时间不能早于菜品完成时间") kind, value = record["setting_kind"], record["setting_value"] if kind not in {"warm_display_c", "hotpot_power_w", "not_recorded"}: raise ValueError("未知的设备设定类型") if kind == "not_recorded": if value is not None: raise ValueError("未记录设定时,其值应为 null") elif not is_number(value): raise ValueError("已记录设定须为有限数值") measurement = record["food_measurement"] if measurement is not None: if not isinstance(measurement, dict): raise ValueError("测温记录须为对象或 null") if not {"measured_at", "temperature_c", "measurement_point"}.issubset(measurement): raise ValueError("测温记录须包含时间、数值和位置") parse_time(measurement["measured_at"]) if not is_number(measurement["temperature_c"]): raise ValueError("测量值须为有限数值") point = measurement["measurement_point"] if not isinstance(point, str) or not point.strip(): raise ValueError("测温记录须注明测量位置") def build_timeline(records): seen, rows = set(), [] for record in records: validate(record) if record["dish_id"] in seen: raise ValueError("dish_id 重复") seen.add(record["dish_id"]) ready = parse_time(record["ready_at"]) served = parse_time(record["served_at"]) planned = parse_time(record["planned_meal_at"]) rows.append({ "dish_id": record["dish_id"], "served_at": served.isoformat(), "ready_to_served_minutes": (served - ready).total_seconds() / 60, "served_vs_plan_minutes": (served - planned).total_seconds() / 60, "food_measurement": record["food_measurement"], }) return sorted(rows, key=lambda row: parse_time(row["served_at"])) if __name__ == "__main__": # 全部为演示记录;设定字段与食品测量字段分开保存。 demo = [ {"dish_id": "demo-1", "ready_at": "2026-10-02T18:00:00+08:00", "served_at": "2026-10-02T18:05:00+08:00", "planned_meal_at": "2026-10-02T18:15:00+08:00", "setting_kind": "warm_display_c", "setting_value": 80, "food_measurement": None}, {"dish_id": "demo-2", "ready_at": "2026-10-02T18:08:00+08:00", "served_at": "2026-10-02T18:18:00+08:00", "planned_meal_at": "2026-10-02T18:15:00+08:00", "setting_kind": "not_recorded", "setting_value": None, "food_measurement": None}, ] rows = build_timeline(demo) assert [r["ready_to_served_minutes"] for r in rows] == [5.0, 10.0] assert [r["served_vs_plan_minutes"] for r in rows] == [-10.0, 3.0] assert all(r["food_measurement"] is None for r in rows) print("演示时间轴:") print(json.dumps(rows, ensure_ascii=False, indent=2, allow_nan=False)) invalid_inputs = [ ("先上桌后完成", {"served_at": "2026-10-02T17:59:00+08:00"}), ("缺时区", {"ready_at": "2026-10-02T18:00:00"}), ("设定类型混写", {"setting_kind": "temperature_or_power"}), ("布尔值冒充设定", {"setting_value": True}), ("非有限数值", {"setting_value": float("nan")}), ("测温缺位置", {"food_measurement": { "measured_at": "2026-10-02T18:05:00+08:00", "temperature_c": 0, "measurement_point": ""}}), ] for name, changes in invalid_inputs: bad = deepcopy(demo[0]) bad.update(changes) try: validate(bad) except ValueError as exc: print(name + ":已拒绝," + str(exc)) else: raise AssertionError(name + " 未被拒绝") try: build_timeline([demo[0], deepcopy(demo[0])]) except ValueError as exc: print("重复菜品编号:已拒绝," + str(exc)) else: raise AssertionError("重复编号未被拒绝")3. 两个时间差,各自回答什么问题
演示结果中,demo-1 从完成到上桌为 5 分钟,上桌比计划开饭早 10 分钟;demo-2 的两个值分别为 10 分钟和晚 3 分钟。ready_to_served_minutes 是流程间隔,served_vs_plan_minutes 是相对计划的偏差;负数表示早于计划,正数表示晚于计划。
这些结果可以帮助查看上菜衔接,不表示食品在这段时间内的实际温度、品质或安全状态。food_measurement 仍是 null,不会因时间轴生成而被自动补成数值。
示例还拒绝上桌早于完成、缺时区、未知设定类型、布尔值冒充数值、非有限数值、测温缺位置和重复菜品编号等输入。时间使用 total_seconds() 计算完整间隔,再换算成分钟,不依赖 timedelta.seconds 中单日部分。
4. 接到实际记录表时
可在记录外层再加 meal_id、设备型号和记录人;设备设定发生变化时另存一条带时间的操作事件,不用覆盖前值。真正做食品测量时,应把测量位置、仪器和对应时间一并记录,再按实际测量目的分析。
这份例子校验的是录入结构和时间先后。开始记录前仍要确定字段含义;同一张表里不要让“设定温度”“面板温度”“食品中心温度”共用一个 temperature 字段。
品牌资料参考:芝梵温度指南。设备使用与餐具适配按对应产品说明书执行。
技术参考:Python datetime 文档、Python json 文档。