From 7f7b69030744e20a3a721830c97fbccd537b6720 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 6 Oct 2026 17:04:42 +0800 Subject: [PATCH] =?UTF-8?q?upload=5Fdates=20=E7=9B=B4=E6=8E=A5=E5=8F=96?= =?UTF-8?q?=E7=8E=B0=E8=A1=8C=E9=85=8D=E6=96=B9=EF=BC=88current=5Frecipe?= =?UTF-8?q?=5Fid=EF=BC=89=EF=BC=9A=E5=8F=91=E5=B8=83=E3=80=81=E5=90=AF?= =?UTF-8?q?=E7=94=A8=E6=97=A7=E7=89=88=E6=9C=AC=E6=97=B6=E5=AE=83=E5=92=8C?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E5=BF=AB=E7=85=A7=E3=80=81=E8=A1=A8=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E4=B8=80=E8=B5=B7=E6=8D=A2=EF=BC=8C=E4=B8=8D=E5=BF=85?= =?UTF-8?q?=E5=86=8D=E7=BB=8F=E5=BF=AB=E7=85=A7=E5=92=8C=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E4=B8=80=E6=9C=9F=E5=AF=BC=E5=85=A5=E5=8E=BB=E6=89=BE=EF=BC=9B?= =?UTF-8?q?=E8=A1=A5=E6=B5=8B=E8=AF=95=EF=BC=9A=E4=BF=AE=E6=94=B9=E9=85=8D?= =?UTF-8?q?=E6=96=B9=E6=8A=8A=E6=94=AF=E4=BB=98=E6=97=A5=E6=9C=9F=E6=94=B9?= =?UTF-8?q?=E6=88=90=E6=96=87=E5=AD=97=E5=90=8E=E5=8F=AA=E5=89=A9=E4=B8=8B?= =?UTF-8?q?=E5=8D=95=E6=97=A5=E6=9C=9F=EF=BC=8C=E5=9B=9E=E6=BB=9A=E5=88=B0?= =?UTF-8?q?=E6=97=A7=E7=89=88=E6=9C=AC=E4=B8=A4=E5=88=97=E5=8F=88=E9=83=BD?= =?UTF-8?q?=E6=98=AF=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/engine/dual_check_facts.py | 20 ++++------ backend/tests/test_dual_check_upload.py | 50 +++++++++++++++++++------ 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/backend/app/engine/dual_check_facts.py b/backend/app/engine/dual_check_facts.py index 09a11808..048d47a2 100644 --- a/backend/app/engine/dual_check_facts.py +++ b/backend/app/engine/dual_check_facts.py @@ -309,27 +309,21 @@ def recipe_dates(recipe: Any) -> dict[str, list[str]]: async def upload_dates(session: Any, row: DataSource | None) -> dict[str, list[str]]: """上传表格建的数据源当前版本里,导入时认作日期的列:{表名: [列名]}(recipe_dates)。只读,不碰导入流程。 - 按当前快照找它的配方(source_versions.snapshot_recipe_id:快照记着按哪一版配方建的表,期 3 之前的快照取最后一期导入 - 的):数据源的表结构(schema_cache)也出自当前快照,两者是同一个版本。读不出来的一律给空——手工登记的源、简单导入 - (没有配方,回执里只有存储类型)、快照或配方记录不在了、配方解析不了:业务日期照旧不记,原因说按文字保存。""" + 配方取现行的那一版(current_recipe_id):发布、启用旧版本时它和当前快照、表结构(schema_cache)一起换 + (table_versions.activate_snapshot),配方和表结构出自同一个版本。读不出来的一律给空——手工登记的源、简单导入 + (没有配方,构建回执里只有存储类型)、配方记录不在了、配方解析不了:业务日期照旧不记,原因说按文字保存。""" from app.data.recipe_types import Recipe - from app.data.source_versions import snapshot_recipe_id - from app.db.models import SourceSnapshot, TableImport, TableRecipe + from app.db.models import TableRecipe - if row is None or (row.origin or "manual") != "upload" or not row.current_snapshot_id: + if row is None or (row.origin or "manual") != "upload" or not row.current_recipe_id: return {} - snap = await session.get(SourceSnapshot, row.current_snapshot_id) - if snap is None or snap.source_id != row.id: - return {} - last = await session.get(TableImport, str(snap.imports[-1])) if snap.imports else None - recipe_id = snapshot_recipe_id(snap, {last.id: last} if last is not None else {}) - record = await session.get(TableRecipe, recipe_id) if recipe_id else None + record = await session.get(TableRecipe, row.current_recipe_id) if record is None or record.source_id != row.id: return {} try: return recipe_dates(Recipe.model_validate(record.recipe)) except ValueError: - logger.warning("上传表格的配方记录解析不了,业务日期按文字保存处理:source=%s recipe=%s", row.id, recipe_id) + logger.warning("上传表格的配方记录解析不了,业务日期按文字保存处理:source=%s recipe=%s", row.id, record.id) return {} diff --git a/backend/tests/test_dual_check_upload.py b/backend/tests/test_dual_check_upload.py index a538a646..c5f8caa8 100644 --- a/backend/tests/test_dual_check_upload.py +++ b/backend/tests/test_dual_check_upload.py @@ -92,13 +92,8 @@ def list_recipe(*, ordered: str = "DATE", paid: str = "DATE") -> dict: "tables": [{"name": TABLE, "grain": ["订单号"]}]} -async def recipe_import(client, recipe: dict) -> tuple[str, str]: - """按配方导入:暂存 → 换成给定的配方 → 试运行 → 勾全部确认项提交。返回 (数据源名, id)。""" - name = f"dc_upload_{uuid.uuid4().hex[:8]}" - r = await client.post(f"{API}/imports/stage", files={"file": (f"{SHEET}_2026-09.xlsx", workbook(), XLSX)}, - data={"name": name}) - assert r.status_code == 201, r.text - staging = r.json()["id"] +async def commit_recipe(client, staging: str, recipe: dict) -> dict: + """暂存区换成给定的配方 → 试运行 → 勾全部确认项提交。返回提交的响应。""" r = await client.put(f"{API}/imports/{staging}/recipe", json={"recipe": recipe}) assert r.status_code == 200 and r.json()["recipe_problems"] == [], r.text r = await client.post(f"{API}/imports/{staging}/trial", json={}) @@ -108,7 +103,17 @@ async def recipe_import(client, recipe: dict) -> tuple[str, str]: r = await client.post(f"{API}/imports/{staging}/commit", json={ "trial_id": trial["trial_id"], "confirmations": [i["id"] for i in trial.get("confirm_items") or []]}) assert r.status_code == 201, r.text - return name, r.json()["source"]["id"] + return r.json() + + +async def recipe_import(client, recipe: dict) -> tuple[str, str]: + """按配方导入:暂存 → 换成给定的配方 → 试运行 → 提交。返回 (数据源名, id)。""" + name = f"dc_upload_{uuid.uuid4().hex[:8]}" + r = await client.post(f"{API}/imports/stage", files={"file": (f"{SHEET}_2026-09.xlsx", workbook(), XLSX)}, + data={"name": name}) + assert r.status_code == 201, r.text + out = await commit_recipe(client, r.json()["id"], recipe) + return name, out["source"]["id"] async def simple_import(client) -> tuple[str, str]: @@ -127,11 +132,11 @@ async def dates_of(source_id: str) -> dict: async def break_recipe(source_id: str) -> None: - """把数据源当前版本的配方记录改坏(造「配方读不出来」)。""" + """把数据源当前版本的配方记录改坏(造「配方读不出来」)。当前快照记的就是这一版。""" async with SessionLocal() as session: row = await session.get(DataSource, source_id) - snap = await session.get(SourceSnapshot, row.current_snapshot_id) - record = await session.get(TableRecipe, snap.recipe_id) + assert (await session.get(SourceSnapshot, row.current_snapshot_id)).recipe_id == row.current_recipe_id + record = await session.get(TableRecipe, row.current_recipe_id) record.recipe = {"recipe_format": "agentlab-recipe/0"} await session.commit() @@ -193,6 +198,29 @@ async def test_an_upload_source_reads_the_date_columns_from_its_current_recipe(c assert await dates_of(paid_as_text[1]) == {TABLE: ["下单日期"]} +async def current_snapshot(source_id: str) -> str: + async with SessionLocal() as session: + return (await session.get(DataSource, source_id)).current_snapshot_id + + +async def test_the_dates_follow_the_current_version_of_the_source(client, dated): + """只认数据源当前版本的配方(表结构出自同一个版本):修改配方把支付日期改成「文字」、提交之后只剩下单日期;回滚到 + 旧版本,两列又都是日期。""" + _, source_id = dated + first = await current_snapshot(source_id) + r = await client.post(f"{API}/{source_id}/redraft", json={}) + assert r.status_code == 201, r.text + await commit_recipe(client, r.json()["id"], list_recipe(paid="TEXT")) + second = await current_snapshot(source_id) + assert second != first + assert await dates_of(source_id) == {TABLE: ["下单日期"]} + r = await client.post(f"{API}/{source_id}/snapshots/{first}/activate", + json={"confirm": True, "expected_current_snapshot_id": second}) + assert r.status_code == 200, r.text + assert await current_snapshot(source_id) == first + assert await dates_of(source_id) == {TABLE: ["下单日期", "支付日期"]} + + async def test_without_a_readable_recipe_no_column_is_taken_as_a_date(client, dated, tmp_path): """读不出配方的都不认:简单导入没有配方,配方记录坏了,手工登记的库,不存在的数据源。""" name, source_id = await simple_import(client)