From 95112218071208d626c995b88a2349b595344aab Mon Sep 17 00:00:00 2001 From: wxg0103 <727495428@qq.com> Date: Thu, 12 Nov 2026 17:28:16 +0800 Subject: [PATCH] refactor: add number formatting utilities for improved data presentation --- apps/common/utils/common.py | 39 +++++++++++++++++++++++ apps/homepage/serializers/homepage.py | 45 ++++++++++++++++++--------- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 73765407a5f..40174be14fc 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -11,12 +11,14 @@ import hashlib import io import json +import math import mimetypes import pickle import re import secrets import shutil import uuid +from decimal import Decimal, ROUND_HALF_UP from functools import lru_cache, reduce from typing import Dict, List @@ -34,6 +36,43 @@ from ..exception.app_exception import AppApiException +_NUMBER_UNITS = ["", "K", "M", "B", "T"] + + +def _to_fixed(value, digits): + if value is None: + value = 0 + dec = Decimal(str(value)).quantize(Decimal("1").scaleb(-digits), rounding=ROUND_HALF_UP) + return f"{dec:.{digits}f}" + + +def _to_thousands(text): + return re.sub(r"\B(?=(\d{3})+(?!\d))", ",", text) + + +def number_format(value): + """Replicate the frontend numberFormat util (thousands separators / 'k' abbreviation).""" + if value is None: + value = 0 + if value < 1000: + return _to_thousands(str(value)) + return f"{_to_thousands(_to_fixed(value / 1000, 1))}k" + + +def format_token_number(value): + """Replicate the frontend formatTokenNumber util (K/M/B/T abbreviation).""" + if value is None or not isinstance(value, (int, float)) or not math.isfinite(value): + return "-" + normalized = float(value) + unit_index = 0 + while abs(normalized) >= 1000 and unit_index < len(_NUMBER_UNITS) - 1: + normalized /= 1000 + unit_index += 1 + if unit_index == 0: + return str(math.floor(normalized + 0.5)) + return f"{_to_fixed(normalized, 1)}{_NUMBER_UNITS[unit_index]}" + + def _legacy_md5_hash(row_password): """ Legacy MD5 hashing — used only to detect old hashes during migration. diff --git a/apps/homepage/serializers/homepage.py b/apps/homepage/serializers/homepage.py index db50e2aea69..d0698db715b 100644 --- a/apps/homepage/serializers/homepage.py +++ b/apps/homepage/serializers/homepage.py @@ -9,6 +9,7 @@ import datetime import os +from decimal import Decimal, ROUND_HALF_UP from typing import List, Dict import openpyxl @@ -37,7 +38,7 @@ from common.auth.constants.role_constants import RoleConstants from common.auth.struct.permission import Permission, Role from common.db.search import native_search, get_dynamics_model, page_search -from common.utils.common import get_file_content +from common.utils.common import format_token_number, get_file_content, number_format from knowledge.models import Knowledge from maxkb.conf import PROJECT_DIR from models_provider.base_model_provider import ModelTypeConst @@ -50,6 +51,22 @@ TOKEN_EXPR = F("chatrecord__message_tokens") + F("chatrecord__answer_tokens") +def _round_ratio(value, divisor): + if divisor <= 0: + return 0 + dec = (Decimal(value) / Decimal(divisor)).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) + result = float(dec) + return int(result) if result.is_integer() else result + + +def _proportion_percentage(value, total): + if not total: + return 0 + dec = (Decimal(value) / Decimal(total) * 100).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) + result = min(100.0, max(0.0, float(dec))) + return int(result) if result.is_integer() else result + + def hasPermission(auth, permission): if "USER" in auth.roles: return True @@ -305,10 +322,10 @@ def export(self, auth, with_valid=True): row = [ index, username, - total_tokens, - total_tokens / token_count if token_count else 0, - chat_record_count, - total_tokens / chat_record_count if chat_record_count else 0, + format_token_number(total_tokens), + _proportion_percentage(total_tokens, token_count), + number_format(chat_record_count), + format_token_number(_round_ratio(total_tokens, chat_record_count)), ] worksheet.append(row) response = HttpResponse(content_type="application/vnd.ms-excel") @@ -476,10 +493,10 @@ def export(self, auth, with_valid=True): row = [ index, item.name, - item.chat_record_count_total, - item.chat_record_count_total / chat_record_number if chat_record_number != 0 else 0, - item.chat_user_count, - item.chat_user_count / item.chat_record_count_total if item.chat_record_count_total != 0 else 0, + number_format(item.chat_record_count_total), + _proportion_percentage(item.chat_record_count_total, chat_record_number), + number_format(item.chat_user_count), + number_format(_round_ratio(item.chat_record_count_total, item.chat_user_count)), ] worksheet.append(row) response = HttpResponse(content_type="application/vnd.ms-excel") @@ -605,11 +622,11 @@ def export(self, auth, with_valid=True): row = [ index, item.name, - total_tokens, - total_tokens / tokens_total if tokens_total else 0, - item.chat_user_count, - chat_record_count_total, - total_tokens / chat_record_count_total if chat_record_count_total else 0, + format_token_number(total_tokens), + _proportion_percentage(total_tokens, tokens_total), + number_format(chat_record_count_total), + number_format(item.chat_user_count), + format_token_number(_round_ratio(total_tokens, chat_record_count_total)), ] worksheet.append(row) response = HttpResponse(content_type="application/vnd.ms-excel")