From 43d141ed95ba240ae4b387134abfe0af6ead463e Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sun, 1 Nov 2026 10:57:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20query=20=E6=8E=A5=E5=8F=A3=20Decima?= =?UTF-8?q?l=20=E8=BE=93=E5=87=BA=E5=8E=BB=E6=8E=89=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E5=B0=BA=E5=BA=A6=E5=B0=BE=E9=9B=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fact 金额字段是 DecimalField(max_digits=44, decimal_places=24),PostgreSQL 把 NUMERIC(44,24) 原样读回,/api/query 的 JsonResponse 对 Decimal 直接 str(),把 24 位存储尺度尾零(如 1000.000000000000000000000000)暴露给 前端与 e2e;SQLite 无此尺度所以本地从未复现(formula-export e2e 在 run 1959/1961 失败的直接原因)。 复用 evidence 接口既有的 amount_text 归一化,新增 AmountJSONEncoder 只覆盖 Decimal 分支;assistant 等内部消费 analyze() 的路径仍拿到 Decimal,不受影响。前端所有数值都经 Number()/format() 解析,格式变化 透明。新增单测锁定 encoder 行为及 /api/query 汇总值输出。 --- finance/views.py | 17 ++++++++++++++++- tests/test_finance.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/finance/views.py b/finance/views.py index 5918efb..49dcf8d 100644 --- a/finance/views.py +++ b/finance/views.py @@ -7,6 +7,7 @@ import math import tempfile import uuid from datetime import timedelta +from decimal import Decimal from functools import wraps from pathlib import Path @@ -15,6 +16,7 @@ from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError +from django.core.serializers.json import DjangoJSONEncoder from django.db import IntegrityError, connection, transaction from django.http import FileResponse, HttpResponse, JsonResponse from django.middleware.csrf import get_token @@ -294,7 +296,7 @@ def query(request, dataset_id): ds = dataset(request, dataset_id, True) pref, _ = preferences(request, ds) limit(f"analysis:{request.user.pk}", 120, 60) - return JsonResponse(analyze(ds, request.member, body(request), pref)) + return JsonResponse(analyze(ds, request.member, body(request), pref), encoder=AmountJSONEncoder) def amount_text(value): @@ -305,6 +307,19 @@ def amount_text(value): return "0" if text == "-0" else text +class AmountJSONEncoder(DjangoJSONEncoder): + """分析接口的 Decimal 统一经 amount_text 输出。 + + PostgreSQL 把 NUMERIC(44,24) 原样读回,DjangoJSONEncoder 默认 str() 会把 + 存储尺度尾零直接暴露给前端;SQLite 无此尺度,两库表现需一致。 + """ + + def default(self, o): + if isinstance(o, Decimal): + return amount_text(o) + return super().default(o) + + @endpoint(("POST",)) def evidence(request, dataset_id): ds = dataset(request, dataset_id, True) diff --git a/tests/test_finance.py b/tests/test_finance.py index fafdeb3..3048467 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -192,6 +192,34 @@ def test_evidence_amounts_trim_storage_scale(workspace): assert row["gross"] == "50" +def test_query_amounts_trim_storage_scale(workspace): + from decimal import Decimal + + from finance.views import AmountJSONEncoder + + assert ( + AmountJSONEncoder().encode({"current": Decimal("1000.000000000000000000000000")}) + == '{"current": "1000"}' + ) + + ds, _, client, *_ = workspace + response = client.post( + f"/api/datasets/{ds.pk}/query", + data={ + "start": "2026-08", + "end": "2026-08", + "comparison": "yoy", + "group": "department", + "metric": "revenue", + }, + content_type="application/json", + ) + assert response.status_code == 200 + summary = response.json()["summary"] + assert summary["revenue"]["current"] == "90" + assert summary["revenue"]["baseline"] == "150" + + def test_settings_revision_and_invalid_metric_are_rejected(workspace): ds, _, client, *_ = workspace url = f"/api/datasets/{ds.pk}/settings"