fix(api): query 接口 Decimal 输出去掉存储尺度尾零
Verify / verify (push) Successful in 4m24s

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 汇总值输出。
This commit was merged in pull request #12.
This commit is contained in:
Jayway Liu
2026-11-01 04:32:26 +00:00
committed by jianwel
parent b7bed0e35d
commit 43d141ed95
2 changed files with 44 additions and 1 deletions
+16 -1
View File
@@ -7,6 +7,7 @@ import math
import tempfile import tempfile
import uuid import uuid
from datetime import timedelta from datetime import timedelta
from decimal import Decimal
from functools import wraps from functools import wraps
from pathlib import Path 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.models import User
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.db import IntegrityError, connection, transaction from django.db import IntegrityError, connection, transaction
from django.http import FileResponse, HttpResponse, JsonResponse from django.http import FileResponse, HttpResponse, JsonResponse
from django.middleware.csrf import get_token from django.middleware.csrf import get_token
@@ -294,7 +296,7 @@ def query(request, dataset_id):
ds = dataset(request, dataset_id, True) ds = dataset(request, dataset_id, True)
pref, _ = preferences(request, ds) pref, _ = preferences(request, ds)
limit(f"analysis:{request.user.pk}", 120, 60) 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): def amount_text(value):
@@ -305,6 +307,19 @@ def amount_text(value):
return "0" if text == "-0" else text 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",)) @endpoint(("POST",))
def evidence(request, dataset_id): def evidence(request, dataset_id):
ds = dataset(request, dataset_id, True) ds = dataset(request, dataset_id, True)
+28
View File
@@ -192,6 +192,34 @@ def test_evidence_amounts_trim_storage_scale(workspace):
assert row["gross"] == "50" 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): def test_settings_revision_and_invalid_metric_are_rejected(workspace):
ds, _, client, *_ = workspace ds, _, client, *_ = workspace
url = f"/api/datasets/{ds.pk}/settings" url = f"/api/datasets/{ds.pk}/settings"