fix(api): query 接口 Decimal 输出去掉存储尺度尾零
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 is contained in:
+16
-1
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user