feat: add tenant-scoped rule and permission management
This commit is contained in:
@@ -24,12 +24,17 @@ from fastapi_modules.fastapi_leaudit.domian.vo.usageStatsVo import (
|
||||
UsageStatsUserItemVO,
|
||||
UsageStatsUserPageVO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ssoUserCompat import SsoUserCompat
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolver
|
||||
from fastapi_modules.fastapi_leaudit.services.usageStatsService import IUsageStatsService
|
||||
|
||||
|
||||
class UsageStatsServiceImpl(IUsageStatsService):
|
||||
"""系统使用统计服务实现。"""
|
||||
|
||||
def __init__(self, TenantResolverService: TenantResolver | None = None) -> None:
|
||||
self.TenantResolver = TenantResolverService or TenantResolver()
|
||||
|
||||
async def RecordLoginEvent(
|
||||
self,
|
||||
*,
|
||||
@@ -51,7 +56,16 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
ou_id = str((UserInfo or {}).get("ou_id") or "") or None
|
||||
ou_name = str((UserInfo or {}).get("ou_name") or "") or None
|
||||
area = str((UserInfo or {}).get("area") or "") or None
|
||||
tenant_code = str((UserInfo or {}).get("tenant_code") or "") or None
|
||||
tenant_name = str((UserInfo or {}).get("tenant_name") or "") or None
|
||||
client_type = self._detect_client_type(UserAgent)
|
||||
resolved_tenant = await self.TenantResolver.ResolveUserContext(
|
||||
Area=area,
|
||||
TenantCode=tenant_code,
|
||||
TenantName=tenant_name,
|
||||
Source="usage_login_event",
|
||||
)
|
||||
normalized_failure_reason = self._normalize_failure_reason(FailureReason)
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
@@ -59,12 +73,14 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
INSERT INTO usage_login_events (
|
||||
user_id, sub, username_snapshot, nick_name_snapshot,
|
||||
department_name_snapshot, ou_id_snapshot, ou_name_snapshot,
|
||||
tenant_code_snapshot, tenant_name_snapshot,
|
||||
area_snapshot, login_time, login_result, login_type,
|
||||
ip_address, user_agent, client_type, token_jti, failure_reason,
|
||||
extra, created_at, updated_at, deleted_at
|
||||
) VALUES (
|
||||
:user_id, :sub, :username_snapshot, :nick_name_snapshot,
|
||||
:department_name_snapshot, :ou_id_snapshot, :ou_name_snapshot,
|
||||
:tenant_code_snapshot, :tenant_name_snapshot,
|
||||
:area_snapshot, NOW(), :login_result, :login_type,
|
||||
:ip_address, :user_agent, :client_type, :token_jti, :failure_reason,
|
||||
CAST(:extra AS jsonb), NOW(), NOW(), NULL
|
||||
@@ -79,14 +95,16 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
"department_name_snapshot": dep_name,
|
||||
"ou_id_snapshot": ou_id,
|
||||
"ou_name_snapshot": ou_name,
|
||||
"area_snapshot": area,
|
||||
"tenant_code_snapshot": resolved_tenant.tenant_code or tenant_code,
|
||||
"tenant_name_snapshot": resolved_tenant.tenant_name or tenant_name or area,
|
||||
"area_snapshot": resolved_tenant.tenant_name or area,
|
||||
"login_result": LoginResult,
|
||||
"login_type": LoginType,
|
||||
"ip_address": IpAddress,
|
||||
"user_agent": UserAgent,
|
||||
"client_type": client_type,
|
||||
"token_jti": TokenJti,
|
||||
"failure_reason": FailureReason,
|
||||
"failure_reason": normalized_failure_reason,
|
||||
"extra": "{}",
|
||||
},
|
||||
)
|
||||
@@ -288,88 +306,115 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
context = await self._get_current_user_context(CurrentUserId)
|
||||
self._assert_stats_access(context)
|
||||
page, page_size, offset = self._pagination(Filters)
|
||||
area_condition, params = self._build_user_scope_condition(context, Filters, user_alias="u")
|
||||
if Filters.get("keyword"):
|
||||
params["keyword"] = f"%{str(Filters['keyword']).strip()}%"
|
||||
area_condition += " AND (u.username ILIKE :keyword OR u.nick_name ILIKE :keyword)"
|
||||
if Filters.get("departmentName"):
|
||||
params["department_name"] = str(Filters["departmentName"]).strip()
|
||||
area_condition += " AND COALESCE(u.dep_name, '') = :department_name"
|
||||
if Filters.get("userId") is not None:
|
||||
params["requested_user_id"] = int(Filters["userId"])
|
||||
area_condition += " AND u.id = :requested_user_id"
|
||||
|
||||
login_date_clause, login_date_params = self._build_range_clause("e.login_time", Filters, prefix="login")
|
||||
upload_date_clause, upload_date_params = self._build_range_clause("f.created_at", Filters, prefix="upload")
|
||||
audit_date_clause, audit_date_params = self._build_range_clause("COALESCE(ar.started_at, ar.created_at)", Filters, prefix="audit")
|
||||
|
||||
query = text(
|
||||
f"""
|
||||
WITH base_users AS (
|
||||
SELECT u.id, u.username, COALESCE(u.nick_name, '') AS nick_name,
|
||||
COALESCE(u.dep_name, '') AS department_name,
|
||||
COALESCE(u.area, '') AS area,
|
||||
u.last_login_at
|
||||
FROM sso_users u
|
||||
WHERE u.deleted_at IS NULL AND u.status = 0 AND {area_condition}
|
||||
),
|
||||
login_stats AS (
|
||||
SELECT e.user_id,
|
||||
COUNT(*)::int AS login_count,
|
||||
MAX(e.login_time) AS last_login_time
|
||||
FROM usage_login_events e
|
||||
WHERE e.login_result = 'success' AND e.user_id IS NOT NULL {login_date_clause}
|
||||
GROUP BY e.user_id
|
||||
),
|
||||
upload_stats AS (
|
||||
SELECT f.created_by AS user_id,
|
||||
COUNT(*) FILTER (WHERE f.file_role = 'primary')::int AS upload_document_count,
|
||||
COUNT(*) FILTER (WHERE f.file_role = 'attachment')::int AS upload_attachment_count
|
||||
FROM leaudit_document_files f
|
||||
JOIN leaudit_documents d ON d.id = f.document_id
|
||||
LEFT JOIN leaudit_document_types dt ON dt.id = d.type_id
|
||||
LEFT JOIN leaudit_entry_modules em ON em.id = dt.entry_module_id
|
||||
WHERE f.created_by IS NOT NULL AND f.deleted_at IS NULL {upload_date_clause}
|
||||
GROUP BY f.created_by
|
||||
),
|
||||
audit_stats AS (
|
||||
SELECT ar.trigger_user_id AS user_id,
|
||||
COUNT(*)::int AS audit_run_count,
|
||||
COUNT(*) FILTER (WHERE ar.status = 'completed')::int AS audit_completed_count,
|
||||
COUNT(*) FILTER (WHERE ar.status = 'failed')::int AS audit_failed_count
|
||||
FROM leaudit_audit_runs ar
|
||||
JOIN leaudit_documents d ON d.id = ar.document_id
|
||||
LEFT JOIN leaudit_document_types dt ON dt.id = d.type_id
|
||||
LEFT JOIN leaudit_entry_modules em ON em.id = dt.entry_module_id
|
||||
WHERE ar.trigger_user_id IS NOT NULL {audit_date_clause}
|
||||
GROUP BY ar.trigger_user_id
|
||||
)
|
||||
SELECT
|
||||
b.id AS user_id,
|
||||
b.username,
|
||||
b.nick_name,
|
||||
b.department_name,
|
||||
b.area,
|
||||
COALESCE(ls.login_count, 0) AS login_count,
|
||||
COALESCE(us.upload_document_count, 0) AS upload_document_count,
|
||||
COALESCE(us.upload_attachment_count, 0) AS upload_attachment_count,
|
||||
COALESCE(aus.audit_run_count, 0) AS audit_run_count,
|
||||
COALESCE(aus.audit_completed_count, 0) AS audit_completed_count,
|
||||
COALESCE(aus.audit_failed_count, 0) AS audit_failed_count,
|
||||
COALESCE(ls.last_login_time, b.last_login_at) AS last_login_time
|
||||
FROM base_users b
|
||||
LEFT JOIN login_stats ls ON ls.user_id = b.id
|
||||
LEFT JOIN upload_stats us ON us.user_id = b.id
|
||||
LEFT JOIN audit_stats aus ON aus.user_id = b.id
|
||||
ORDER BY b.id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
)
|
||||
count_query = text(f"SELECT COUNT(*)::int AS total FROM sso_users u WHERE u.deleted_at IS NULL AND u.status = 0 AND {area_condition}")
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
await self._ensure_usage_stats_schema(session)
|
||||
merged_params = {**params, **login_date_params, **upload_date_params, **audit_date_params, "limit": page_size, "offset": offset}
|
||||
sso_user_columns = await SsoUserCompat.get_columns(session)
|
||||
tenant_code_select = SsoUserCompat.optional_coalesce_as(
|
||||
sso_user_columns,
|
||||
alias="u",
|
||||
column="tenant_code",
|
||||
fallback_sql="''",
|
||||
)
|
||||
tenant_name_select = SsoUserCompat.optional_coalesce_as(
|
||||
sso_user_columns,
|
||||
alias="u",
|
||||
column="tenant_name",
|
||||
fallback_sql="''",
|
||||
)
|
||||
area_condition, params = self._build_user_scope_condition(context, Filters, user_alias="u")
|
||||
if Filters.get("keyword"):
|
||||
params["keyword"] = f"%{str(Filters['keyword']).strip()}%"
|
||||
area_condition += " AND (u.username ILIKE :keyword OR u.nick_name ILIKE :keyword)"
|
||||
if Filters.get("departmentName"):
|
||||
params["department_name"] = str(Filters["departmentName"]).strip()
|
||||
area_condition += " AND COALESCE(u.dep_name, '') = :department_name"
|
||||
if Filters.get("userId") is not None:
|
||||
params["requested_user_id"] = int(Filters["userId"])
|
||||
area_condition += " AND u.id = :requested_user_id"
|
||||
|
||||
login_where, login_params = self._build_login_filters(context, Filters)
|
||||
upload_where, upload_params = self._build_document_filters(context, Filters, alias_prefix="d", file_alias="f", user_alias="u")
|
||||
audit_where, audit_params = self._build_audit_filters(context, Filters)
|
||||
|
||||
query = text(
|
||||
f"""
|
||||
WITH base_users AS (
|
||||
SELECT u.id, u.username, COALESCE(u.nick_name, '') AS nick_name,
|
||||
COALESCE(u.dep_name, '') AS department_name,
|
||||
COALESCE(u.area, '') AS area,
|
||||
{tenant_code_select},
|
||||
{tenant_name_select},
|
||||
u.last_login_at
|
||||
FROM sso_users u
|
||||
WHERE u.deleted_at IS NULL AND u.status = 0 AND {area_condition}
|
||||
),
|
||||
login_stats AS (
|
||||
SELECT e.user_id,
|
||||
COUNT(*)::int AS login_count,
|
||||
MAX(e.login_time) AS last_login_time
|
||||
FROM usage_login_events e
|
||||
WHERE {login_where} AND e.login_result = 'success' AND e.user_id IS NOT NULL
|
||||
GROUP BY e.user_id
|
||||
),
|
||||
upload_stats AS (
|
||||
SELECT f.created_by AS user_id,
|
||||
COUNT(*) FILTER (WHERE f.file_role = 'primary')::int AS upload_document_count,
|
||||
COUNT(*) FILTER (WHERE f.file_role = 'attachment')::int AS upload_attachment_count
|
||||
FROM leaudit_document_files f
|
||||
JOIN leaudit_documents d ON d.id = f.document_id
|
||||
LEFT JOIN sso_users u ON u.id = f.created_by
|
||||
LEFT JOIN leaudit_document_types dt ON dt.id = d.type_id
|
||||
LEFT JOIN leaudit_entry_modules em ON em.id = dt.entry_module_id
|
||||
WHERE {upload_where} AND f.created_by IS NOT NULL
|
||||
GROUP BY f.created_by
|
||||
),
|
||||
audit_stats AS (
|
||||
SELECT ar.trigger_user_id AS user_id,
|
||||
COUNT(*)::int AS audit_run_count,
|
||||
COUNT(*) FILTER (WHERE ar.status = 'completed')::int AS audit_completed_count,
|
||||
COUNT(*) FILTER (WHERE ar.status = 'failed')::int AS audit_failed_count
|
||||
FROM leaudit_audit_runs ar
|
||||
JOIN leaudit_documents d ON d.id = ar.document_id
|
||||
LEFT JOIN sso_users u ON u.id = ar.trigger_user_id
|
||||
LEFT JOIN leaudit_document_types dt ON dt.id = d.type_id
|
||||
LEFT JOIN leaudit_entry_modules em ON em.id = dt.entry_module_id
|
||||
WHERE {audit_where} AND ar.trigger_user_id IS NOT NULL
|
||||
GROUP BY ar.trigger_user_id
|
||||
)
|
||||
SELECT
|
||||
b.id AS user_id,
|
||||
b.username,
|
||||
b.nick_name,
|
||||
b.department_name,
|
||||
b.area,
|
||||
b.tenant_code,
|
||||
b.tenant_name,
|
||||
COALESCE(ls.login_count, 0) AS login_count,
|
||||
COALESCE(us.upload_document_count, 0) AS upload_document_count,
|
||||
COALESCE(us.upload_attachment_count, 0) AS upload_attachment_count,
|
||||
COALESCE(aus.audit_run_count, 0) AS audit_run_count,
|
||||
COALESCE(aus.audit_completed_count, 0) AS audit_completed_count,
|
||||
COALESCE(aus.audit_failed_count, 0) AS audit_failed_count,
|
||||
COALESCE(ls.last_login_time, b.last_login_at) AS last_login_time
|
||||
FROM base_users b
|
||||
LEFT JOIN login_stats ls ON ls.user_id = b.id
|
||||
LEFT JOIN upload_stats us ON us.user_id = b.id
|
||||
LEFT JOIN audit_stats aus ON aus.user_id = b.id
|
||||
ORDER BY b.id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
)
|
||||
count_query = text(
|
||||
f"SELECT COUNT(*)::int AS total FROM sso_users u WHERE u.deleted_at IS NULL AND u.status = 0 AND {area_condition}"
|
||||
)
|
||||
merged_params = {
|
||||
**params,
|
||||
**login_params,
|
||||
**upload_params,
|
||||
**audit_params,
|
||||
"limit": page_size,
|
||||
"offset": offset,
|
||||
}
|
||||
total = int((await session.execute(count_query, params)).scalar_one() or 0)
|
||||
rows = (await session.execute(query, merged_params)).mappings().all()
|
||||
items = [
|
||||
@@ -379,6 +424,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
nickName=str(row["nick_name"] or ""),
|
||||
departmentName=row["department_name"] or None,
|
||||
area=row["area"] or None,
|
||||
tenantCode=row.get("tenant_code") or None,
|
||||
tenantName=row.get("tenant_name") or row.get("area") or None,
|
||||
loginCount=int(row["login_count"] or 0),
|
||||
uploadDocumentCount=int(row["upload_document_count"] or 0),
|
||||
uploadAttachmentCount=int(row["upload_attachment_count"] or 0),
|
||||
@@ -435,13 +482,36 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
await self._ensure_usage_stats_schema(session)
|
||||
sso_user_columns = await SsoUserCompat.get_columns(session)
|
||||
user_tenant_code_expr = SsoUserCompat.raw_optional_column(
|
||||
sso_user_columns,
|
||||
alias="u",
|
||||
column="tenant_code",
|
||||
)
|
||||
user_tenant_name_expr = SsoUserCompat.raw_optional_column(
|
||||
sso_user_columns,
|
||||
alias="u",
|
||||
column="tenant_name",
|
||||
)
|
||||
login_where, login_params = self._build_login_filters(context, Filters)
|
||||
if area_scope == "document":
|
||||
upload_area_expr = "COALESCE(d.region, '')"
|
||||
audit_area_expr = "COALESCE(d.region, '')"
|
||||
upload_tenant_code_expr = "COALESCE(NULLIF(BTRIM(d.tenant_code), ''), NULL)"
|
||||
upload_tenant_name_expr = self._document_tenant_name_sql("d")
|
||||
audit_tenant_code_expr = "COALESCE(NULLIF(BTRIM(d.tenant_code), ''), NULL)"
|
||||
audit_tenant_name_expr = self._document_tenant_name_sql("d")
|
||||
else:
|
||||
upload_area_expr = "COALESCE(u.area, '')"
|
||||
audit_area_expr = "COALESCE(u.area, '')"
|
||||
upload_tenant_code_expr = f"COALESCE(NULLIF(BTRIM({user_tenant_code_expr}), ''), NULL)"
|
||||
upload_tenant_name_expr = (
|
||||
f"COALESCE(NULLIF(BTRIM({user_tenant_name_expr}), ''), NULLIF(BTRIM({upload_area_expr}), ''), '未分配地区')"
|
||||
)
|
||||
audit_tenant_code_expr = f"COALESCE(NULLIF(BTRIM({user_tenant_code_expr}), ''), NULL)"
|
||||
audit_tenant_name_expr = (
|
||||
f"COALESCE(NULLIF(BTRIM({user_tenant_name_expr}), ''), NULLIF(BTRIM({audit_area_expr}), ''), '未分配地区')"
|
||||
)
|
||||
doc_where, doc_params = self._build_document_filters(context, Filters, alias_prefix="d", file_alias="f", user_alias="u")
|
||||
audit_where, audit_params = self._build_audit_filters(context, Filters)
|
||||
|
||||
@@ -450,11 +520,13 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
text(
|
||||
f"""
|
||||
SELECT COALESCE(e.area_snapshot, '未分配地区') AS area,
|
||||
COALESCE(NULLIF(BTRIM(e.tenant_code_snapshot), ''), NULL) AS tenant_code,
|
||||
COALESCE(NULLIF(BTRIM(e.tenant_name_snapshot), ''), COALESCE(e.area_snapshot, '未分配地区')) AS tenant_name,
|
||||
COUNT(*)::int AS login_count,
|
||||
COUNT(DISTINCT e.user_id)::int AS login_user_count
|
||||
FROM usage_login_events e
|
||||
WHERE {login_where} AND e.login_result = 'success'
|
||||
GROUP BY 1
|
||||
GROUP BY 1, 2, 3
|
||||
"""
|
||||
),
|
||||
login_params,
|
||||
@@ -465,6 +537,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
text(
|
||||
f"""
|
||||
SELECT {upload_area_expr} AS area,
|
||||
{upload_tenant_code_expr} AS tenant_code,
|
||||
{upload_tenant_name_expr} AS tenant_name,
|
||||
COUNT(*) FILTER (WHERE f.file_role = 'primary')::int AS upload_document_count,
|
||||
COUNT(*) FILTER (WHERE f.file_role = 'attachment')::int AS upload_attachment_count
|
||||
FROM leaudit_document_files f
|
||||
@@ -473,7 +547,7 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
LEFT JOIN leaudit_document_types dt ON dt.id = d.type_id
|
||||
LEFT JOIN leaudit_entry_modules em ON em.id = dt.entry_module_id
|
||||
WHERE {doc_where}
|
||||
GROUP BY 1
|
||||
GROUP BY 1, 2, 3
|
||||
"""
|
||||
),
|
||||
doc_params,
|
||||
@@ -484,6 +558,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
text(
|
||||
f"""
|
||||
SELECT {audit_area_expr} AS area,
|
||||
{audit_tenant_code_expr} AS tenant_code,
|
||||
{audit_tenant_name_expr} AS tenant_name,
|
||||
COUNT(*)::int AS audit_run_count,
|
||||
COUNT(*) FILTER (WHERE ar.status = 'completed')::int AS audit_completed_count,
|
||||
COUNT(*) FILTER (WHERE ar.status = 'failed')::int AS audit_failed_count
|
||||
@@ -493,30 +569,45 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
LEFT JOIN leaudit_document_types dt ON dt.id = d.type_id
|
||||
LEFT JOIN leaudit_entry_modules em ON em.id = dt.entry_module_id
|
||||
WHERE {audit_where}
|
||||
GROUP BY 1
|
||||
GROUP BY 1, 2, 3
|
||||
"""
|
||||
),
|
||||
audit_params,
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
grouped: dict[str, UsageStatsAreaItemVO] = {}
|
||||
grouped: dict[tuple[str | None, str], UsageStatsAreaItemVO] = {}
|
||||
for row in login_rows:
|
||||
area = str(row["area"] or "未分配地区")
|
||||
grouped.setdefault(area, UsageStatsAreaItemVO(area=area))
|
||||
grouped[area].loginCount += int(row["login_count"] or 0)
|
||||
grouped[area].loginUserCount += int(row["login_user_count"] or 0)
|
||||
tenant_code = row.get("tenant_code") or None
|
||||
group_key = (tenant_code, area)
|
||||
grouped.setdefault(
|
||||
group_key,
|
||||
UsageStatsAreaItemVO(area=area, tenantCode=tenant_code, tenantName=row.get("tenant_name") or area),
|
||||
)
|
||||
grouped[group_key].loginCount += int(row["login_count"] or 0)
|
||||
grouped[group_key].loginUserCount += int(row["login_user_count"] or 0)
|
||||
for row in upload_rows:
|
||||
area = str(row["area"] or "未分配地区")
|
||||
grouped.setdefault(area, UsageStatsAreaItemVO(area=area))
|
||||
grouped[area].uploadDocumentCount += int(row["upload_document_count"] or 0)
|
||||
grouped[area].uploadAttachmentCount += int(row["upload_attachment_count"] or 0)
|
||||
tenant_code = row.get("tenant_code") or None
|
||||
group_key = (tenant_code, area)
|
||||
grouped.setdefault(
|
||||
group_key,
|
||||
UsageStatsAreaItemVO(area=area, tenantCode=tenant_code, tenantName=row.get("tenant_name") or area),
|
||||
)
|
||||
grouped[group_key].uploadDocumentCount += int(row["upload_document_count"] or 0)
|
||||
grouped[group_key].uploadAttachmentCount += int(row["upload_attachment_count"] or 0)
|
||||
for row in audit_rows:
|
||||
area = str(row["area"] or "未分配地区")
|
||||
grouped.setdefault(area, UsageStatsAreaItemVO(area=area))
|
||||
grouped[area].auditRunCount += int(row["audit_run_count"] or 0)
|
||||
grouped[area].auditCompletedCount += int(row["audit_completed_count"] or 0)
|
||||
grouped[area].auditFailedCount += int(row["audit_failed_count"] or 0)
|
||||
tenant_code = row.get("tenant_code") or None
|
||||
group_key = (tenant_code, area)
|
||||
grouped.setdefault(
|
||||
group_key,
|
||||
UsageStatsAreaItemVO(area=area, tenantCode=tenant_code, tenantName=row.get("tenant_name") or area),
|
||||
)
|
||||
grouped[group_key].auditRunCount += int(row["audit_run_count"] or 0)
|
||||
grouped[group_key].auditCompletedCount += int(row["audit_completed_count"] or 0)
|
||||
grouped[group_key].auditFailedCount += int(row["audit_failed_count"] or 0)
|
||||
items = list(grouped.values())
|
||||
items.sort(key=lambda item: (item.auditRunCount, item.uploadDocumentCount, item.loginCount), reverse=True)
|
||||
total = len(items)
|
||||
@@ -529,16 +620,40 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
data_type = str(Filters.get("dataType") or "audit").strip().lower()
|
||||
if data_type not in {"login", "upload", "audit"}:
|
||||
data_type = "audit"
|
||||
area_scope = str(Filters.get("areaScope") or "user").strip().lower()
|
||||
if area_scope not in {"user", "document"}:
|
||||
area_scope = "user"
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
await self._ensure_usage_stats_schema(session)
|
||||
sso_user_columns = await SsoUserCompat.get_columns(session)
|
||||
user_tenant_code_expr = SsoUserCompat.raw_optional_column(
|
||||
sso_user_columns,
|
||||
alias="u",
|
||||
column="tenant_code",
|
||||
)
|
||||
user_tenant_name_expr = SsoUserCompat.raw_optional_column(
|
||||
sso_user_columns,
|
||||
alias="u",
|
||||
column="tenant_name",
|
||||
)
|
||||
if area_scope == "document":
|
||||
detail_area_expr = "COALESCE(d.region, '')"
|
||||
detail_tenant_code_select = "COALESCE(NULLIF(BTRIM(d.tenant_code), ''), NULL) AS tenant_code"
|
||||
detail_tenant_name_select = f"{self._document_tenant_name_sql('d')} AS tenant_name"
|
||||
else:
|
||||
detail_area_expr = "COALESCE(u.area, '')"
|
||||
detail_tenant_code_select = f"COALESCE(NULLIF(BTRIM({user_tenant_code_expr}), ''), NULL) AS tenant_code"
|
||||
detail_tenant_name_select = (
|
||||
f"COALESCE(NULLIF(BTRIM({user_tenant_name_expr}), ''), NULLIF(BTRIM(COALESCE(u.area, '')), ''), NULL) AS tenant_name"
|
||||
)
|
||||
if data_type == "login":
|
||||
where_clause, params = self._build_login_filters(context, Filters)
|
||||
count_sql = text(f"SELECT COUNT(*)::int FROM usage_login_events e WHERE {where_clause}")
|
||||
list_sql = text(
|
||||
f"""
|
||||
SELECT e.login_time, e.user_id, e.username_snapshot, e.nick_name_snapshot,
|
||||
e.department_name_snapshot, e.area_snapshot, e.login_result,
|
||||
e.department_name_snapshot, e.area_snapshot, e.tenant_code_snapshot, e.tenant_name_snapshot, e.login_result,
|
||||
e.failure_reason, e.login_type
|
||||
FROM usage_login_events e
|
||||
WHERE {where_clause}
|
||||
@@ -557,6 +672,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
nickName=str(row.get("nick_name_snapshot") or ""),
|
||||
departmentName=row.get("department_name_snapshot") or None,
|
||||
area=row.get("area_snapshot") or None,
|
||||
tenantCode=row.get("tenant_code_snapshot") or None,
|
||||
tenantName=row.get("tenant_name_snapshot") or row.get("area_snapshot") or None,
|
||||
status=str(row.get("login_result") or ""),
|
||||
extra={"failureReason": row.get("failure_reason"), "loginType": row.get("login_type")},
|
||||
)
|
||||
@@ -579,7 +696,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
f"""
|
||||
SELECT f.created_at, f.created_by, COALESCE(u.username, '') AS username,
|
||||
COALESCE(u.nick_name, '') AS nick_name, COALESCE(u.dep_name, '') AS department_name,
|
||||
COALESCE(u.area, '') AS area, d.id AS document_id, f.file_name,
|
||||
{detail_area_expr} AS area, {detail_tenant_code_select},
|
||||
{detail_tenant_name_select}, d.id AS document_id, f.file_name,
|
||||
dt.id AS document_type_id, dt.name AS document_type_name,
|
||||
em.id AS entry_module_id, em.name AS entry_module_name,
|
||||
f.file_role
|
||||
@@ -604,6 +722,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
nickName=str(row.get("nick_name") or ""),
|
||||
departmentName=row.get("department_name") or None,
|
||||
area=row.get("area") or None,
|
||||
tenantCode=row.get("tenant_code") or None,
|
||||
tenantName=row.get("tenant_name") or row.get("area") or None,
|
||||
documentId=self._to_int(row.get("document_id")),
|
||||
documentName=row.get("file_name"),
|
||||
documentTypeId=self._to_int(row.get("document_type_id")),
|
||||
@@ -633,7 +753,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
SELECT COALESCE(ar.started_at, ar.created_at) AS event_time,
|
||||
ar.trigger_user_id, COALESCE(u.username, '') AS username,
|
||||
COALESCE(u.nick_name, '') AS nick_name, COALESCE(u.dep_name, '') AS department_name,
|
||||
COALESCE(u.area, '') AS area, d.id AS document_id,
|
||||
{detail_area_expr} AS area, {detail_tenant_code_select},
|
||||
{detail_tenant_name_select}, d.id AS document_id,
|
||||
COALESCE(f.file_name, d.normalized_name, '') AS document_name,
|
||||
dt.id AS document_type_id, dt.name AS document_type_name,
|
||||
em.id AS entry_module_id, em.name AS entry_module_name,
|
||||
@@ -660,6 +781,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
nickName=str(row.get("nick_name") or ""),
|
||||
departmentName=row.get("department_name") or None,
|
||||
area=row.get("area") or None,
|
||||
tenantCode=row.get("tenant_code") or None,
|
||||
tenantName=row.get("tenant_name") or row.get("area") or None,
|
||||
documentId=self._to_int(row.get("document_id")),
|
||||
documentName=row.get("document_name"),
|
||||
documentTypeId=self._to_int(row.get("document_type_id")),
|
||||
@@ -687,6 +810,8 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
department_name_snapshot VARCHAR(255) NULL,
|
||||
ou_id_snapshot VARCHAR(128) NULL,
|
||||
ou_name_snapshot VARCHAR(255) NULL,
|
||||
tenant_code_snapshot VARCHAR(64) NULL,
|
||||
tenant_name_snapshot VARCHAR(128) NULL,
|
||||
area_snapshot VARCHAR(64) NULL,
|
||||
login_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
login_result VARCHAR(16) NOT NULL,
|
||||
@@ -704,20 +829,38 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
"""
|
||||
)
|
||||
)
|
||||
await session.execute(text("ALTER TABLE usage_login_events ADD COLUMN IF NOT EXISTS tenant_code_snapshot VARCHAR(64) NULL"))
|
||||
await session.execute(text("ALTER TABLE usage_login_events ADD COLUMN IF NOT EXISTS tenant_name_snapshot VARCHAR(128) NULL"))
|
||||
await session.execute(text("CREATE INDEX IF NOT EXISTS idx_usage_login_events_login_time ON usage_login_events(login_time DESC)"))
|
||||
await session.execute(text("CREATE INDEX IF NOT EXISTS idx_usage_login_events_user_id ON usage_login_events(user_id, login_time DESC)"))
|
||||
await session.execute(text("CREATE INDEX IF NOT EXISTS idx_usage_login_events_department ON usage_login_events(department_name_snapshot, login_time DESC)"))
|
||||
await session.execute(text("CREATE INDEX IF NOT EXISTS idx_usage_login_events_area ON usage_login_events(area_snapshot, login_time DESC)"))
|
||||
await session.execute(text("CREATE INDEX IF NOT EXISTS idx_usage_login_events_tenant_code ON usage_login_events(tenant_code_snapshot, login_time DESC)"))
|
||||
|
||||
async def _get_current_user_context(self, current_user_id: int) -> dict[str, Any]:
|
||||
async with GetAsyncSession() as session:
|
||||
sso_user_columns = await SsoUserCompat.get_columns(session)
|
||||
tenant_code_select = SsoUserCompat.optional_coalesce_as(
|
||||
sso_user_columns,
|
||||
alias="u",
|
||||
column="tenant_code",
|
||||
fallback_sql="''",
|
||||
)
|
||||
tenant_name_select = SsoUserCompat.optional_coalesce_as(
|
||||
sso_user_columns,
|
||||
alias="u",
|
||||
column="tenant_name",
|
||||
fallback_sql="''",
|
||||
)
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
f"""
|
||||
SELECT
|
||||
u.id,
|
||||
COALESCE(u.area, '') AS area,
|
||||
{tenant_code_select},
|
||||
{tenant_name_select},
|
||||
COALESCE(bool_or(r.role_key IN ('super_admin', 'provincial_admin')), FALSE) AS is_global,
|
||||
COALESCE(bool_or(r.role_key IN ('super_admin', 'provincial_admin', 'admin')), FALSE) AS can_manage,
|
||||
COALESCE(bool_or(r.role_key = 'super_admin'), FALSE) AS is_super_admin
|
||||
@@ -733,7 +876,21 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
).mappings().first()
|
||||
if not row:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "当前用户不存在")
|
||||
return {"area": str(row["area"] or ""), "is_global": bool(row["is_global"]), "can_manage": bool(row["can_manage"]), "is_super_admin": bool(row["is_super_admin"])}
|
||||
tenant = await self.TenantResolver.ResolveUserContext(
|
||||
Area=str(row["area"] or ""),
|
||||
TenantCode=str(row["tenant_code"] or "") or None,
|
||||
TenantName=str(row["tenant_name"] or "") or None,
|
||||
Source="usage_stats_user_context",
|
||||
)
|
||||
return {
|
||||
"area": tenant.tenant_name or tenant.normalized_value or str(row["area"] or ""),
|
||||
"tenant_code": tenant.tenant_code or str(row["tenant_code"] or "") or None,
|
||||
"tenant_name": tenant.tenant_name or str(row["tenant_name"] or "") or str(row["area"] or "") or None,
|
||||
"tenant_scope_value": tenant.tenant_name or tenant.normalized_value or str(row["area"] or ""),
|
||||
"is_global": bool(row["is_global"]),
|
||||
"can_manage": bool(row["can_manage"]),
|
||||
"is_super_admin": bool(row["is_super_admin"]),
|
||||
}
|
||||
|
||||
def _assert_stats_access(self, context: dict[str, Any]) -> None:
|
||||
if context["is_global"] or context["can_manage"]:
|
||||
@@ -744,25 +901,60 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
conditions = ["1 = 1"]
|
||||
params: dict[str, Any] = {}
|
||||
requested_area = str(filters.get("area") or "").strip()
|
||||
requested_tenant_code = str(filters.get("tenantCode") or filters.get("tenant_code") or "").strip()
|
||||
requested_tenant_name = self._resolve_requested_tenant_name(requested_tenant_code, requested_area, context)
|
||||
if context["is_global"]:
|
||||
if requested_area:
|
||||
if requested_tenant_code:
|
||||
conditions.extend(self._user_tenant_filter_sql(user_alias, params, requested_tenant_code, requested_tenant_name, "requested"))
|
||||
elif requested_tenant_name:
|
||||
conditions.append(f"COALESCE({user_alias}.area, '') = :requested_area")
|
||||
params["requested_area"] = requested_area
|
||||
params["requested_area"] = requested_tenant_name
|
||||
else:
|
||||
if not context["area"]:
|
||||
scope_value = str(context.get("tenant_scope_value") or context.get("area") or "").strip()
|
||||
scope_tenant_code = str(context.get("tenant_code") or "").strip()
|
||||
if not scope_value and not scope_tenant_code:
|
||||
conditions.append("1 = 0")
|
||||
elif requested_area and requested_area != context["area"]:
|
||||
elif requested_tenant_code:
|
||||
if scope_tenant_code and requested_tenant_code != scope_tenant_code:
|
||||
conditions.append("1 = 0")
|
||||
elif not scope_tenant_code and requested_tenant_name and requested_tenant_name != scope_value:
|
||||
conditions.append("1 = 0")
|
||||
else:
|
||||
conditions.extend(self._user_tenant_filter_sql(user_alias, params, requested_tenant_code, requested_tenant_name, "requested"))
|
||||
elif requested_tenant_name and requested_tenant_name != scope_value:
|
||||
conditions.append("1 = 0")
|
||||
else:
|
||||
conditions.append(f"COALESCE({user_alias}.area, '') = :scope_area")
|
||||
params["scope_area"] = context["area"]
|
||||
conditions.extend(self._user_tenant_filter_sql(user_alias, params, scope_tenant_code or None, scope_value or None, "scope"))
|
||||
return " AND ".join(conditions), params
|
||||
|
||||
def _build_login_filters(self, context: dict[str, Any], filters: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
||||
conditions = ["e.deleted_at IS NULL"]
|
||||
scope_cond, params = self._build_user_scope_condition(context, filters, user_alias="e")
|
||||
scope_cond = scope_cond.replace("e.area", "e.area_snapshot")
|
||||
conditions.append(scope_cond)
|
||||
params: dict[str, Any] = {}
|
||||
requested_area = str(filters.get("area") or "").strip()
|
||||
requested_tenant_code = str(filters.get("tenantCode") or filters.get("tenant_code") or "").strip()
|
||||
requested_tenant_name = self._resolve_requested_tenant_name(requested_tenant_code, requested_area, context)
|
||||
scope_tenant_code = str(context.get("tenant_code") or "").strip()
|
||||
scope_tenant_name = str(context.get("tenant_scope_value") or context.get("area") or "").strip()
|
||||
if context["is_global"]:
|
||||
if requested_tenant_code:
|
||||
conditions.extend(self._login_tenant_filter_sql(params, requested_tenant_code, requested_tenant_name, "requested"))
|
||||
elif requested_tenant_name:
|
||||
conditions.append("COALESCE(e.area_snapshot, '') = :requested_area")
|
||||
params["requested_area"] = requested_tenant_name
|
||||
else:
|
||||
if not scope_tenant_code and not scope_tenant_name:
|
||||
conditions.append("1 = 0")
|
||||
elif requested_tenant_code:
|
||||
if scope_tenant_code and requested_tenant_code != scope_tenant_code:
|
||||
conditions.append("1 = 0")
|
||||
elif not scope_tenant_code and requested_tenant_name and requested_tenant_name != scope_tenant_name:
|
||||
conditions.append("1 = 0")
|
||||
else:
|
||||
conditions.extend(self._login_tenant_filter_sql(params, requested_tenant_code, requested_tenant_name, "requested"))
|
||||
elif requested_tenant_name and requested_tenant_name != scope_tenant_name:
|
||||
conditions.append("1 = 0")
|
||||
else:
|
||||
conditions.extend(self._login_tenant_filter_sql(params, scope_tenant_code or None, scope_tenant_name or None, "scope"))
|
||||
if filters.get("userId") is not None:
|
||||
conditions.append("e.user_id = :user_id")
|
||||
params["user_id"] = int(filters["userId"])
|
||||
@@ -782,19 +974,31 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
params: dict[str, Any] = {}
|
||||
area_scope = str(filters.get("areaScope") or "user").strip().lower()
|
||||
requested_area = str(filters.get("area") or "").strip()
|
||||
requested_tenant_code = str(filters.get("tenantCode") or filters.get("tenant_code") or "").strip()
|
||||
requested_tenant_name = self._resolve_requested_tenant_name(requested_tenant_code, requested_area, context)
|
||||
if area_scope == "document":
|
||||
if context["is_global"]:
|
||||
if requested_area:
|
||||
if requested_tenant_code:
|
||||
conditions.extend(self._document_tenant_filter_sql(alias_prefix, params, requested_tenant_code, requested_tenant_name, "requested"))
|
||||
elif requested_tenant_name:
|
||||
conditions.append(f"COALESCE({alias_prefix}.region, '') = :requested_area")
|
||||
params["requested_area"] = requested_area
|
||||
params["requested_area"] = requested_tenant_name
|
||||
else:
|
||||
if not context["area"]:
|
||||
scope_value = str(context.get("tenant_scope_value") or context.get("area") or "").strip()
|
||||
scope_tenant_code = str(context.get("tenant_code") or "").strip()
|
||||
if not scope_value and not scope_tenant_code:
|
||||
conditions.append("1 = 0")
|
||||
elif requested_area and requested_area != context["area"]:
|
||||
elif requested_tenant_code:
|
||||
if scope_tenant_code and requested_tenant_code != scope_tenant_code:
|
||||
conditions.append("1 = 0")
|
||||
elif not scope_tenant_code and requested_tenant_name and requested_tenant_name != scope_value:
|
||||
conditions.append("1 = 0")
|
||||
else:
|
||||
conditions.extend(self._document_tenant_filter_sql(alias_prefix, params, requested_tenant_code, requested_tenant_name, "requested"))
|
||||
elif requested_tenant_name and requested_tenant_name != scope_value:
|
||||
conditions.append("1 = 0")
|
||||
else:
|
||||
conditions.append(f"COALESCE({alias_prefix}.region, '') = :scope_area")
|
||||
params["scope_area"] = context["area"]
|
||||
conditions.extend(self._document_tenant_filter_sql(alias_prefix, params, scope_tenant_code or None, scope_value or None, "scope"))
|
||||
else:
|
||||
user_scope_cond, user_scope_params = self._build_user_scope_condition(context, filters, user_alias=user_alias)
|
||||
conditions.append(user_scope_cond)
|
||||
@@ -825,19 +1029,31 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
params: dict[str, Any] = {}
|
||||
area_scope = str(filters.get("areaScope") or "user").strip().lower()
|
||||
requested_area = str(filters.get("area") or "").strip()
|
||||
requested_tenant_code = str(filters.get("tenantCode") or filters.get("tenant_code") or "").strip()
|
||||
requested_tenant_name = self._resolve_requested_tenant_name(requested_tenant_code, requested_area, context)
|
||||
if area_scope == "document":
|
||||
if context["is_global"]:
|
||||
if requested_area:
|
||||
if requested_tenant_code:
|
||||
conditions.extend(self._document_tenant_filter_sql("d", params, requested_tenant_code, requested_tenant_name, "requested"))
|
||||
elif requested_tenant_name:
|
||||
conditions.append("COALESCE(d.region, '') = :requested_area")
|
||||
params["requested_area"] = requested_area
|
||||
params["requested_area"] = requested_tenant_name
|
||||
else:
|
||||
if not context["area"]:
|
||||
scope_value = str(context.get("tenant_scope_value") or context.get("area") or "").strip()
|
||||
scope_tenant_code = str(context.get("tenant_code") or "").strip()
|
||||
if not scope_value and not scope_tenant_code:
|
||||
conditions.append("1 = 0")
|
||||
elif requested_area and requested_area != context["area"]:
|
||||
elif requested_tenant_code:
|
||||
if scope_tenant_code and requested_tenant_code != scope_tenant_code:
|
||||
conditions.append("1 = 0")
|
||||
elif not scope_tenant_code and requested_tenant_name and requested_tenant_name != scope_value:
|
||||
conditions.append("1 = 0")
|
||||
else:
|
||||
conditions.extend(self._document_tenant_filter_sql("d", params, requested_tenant_code, requested_tenant_name, "requested"))
|
||||
elif requested_tenant_name and requested_tenant_name != scope_value:
|
||||
conditions.append("1 = 0")
|
||||
else:
|
||||
conditions.append("COALESCE(d.region, '') = :scope_area")
|
||||
params["scope_area"] = context["area"]
|
||||
conditions.extend(self._document_tenant_filter_sql("d", params, scope_tenant_code or None, scope_value or None, "scope"))
|
||||
else:
|
||||
user_scope_cond, user_scope_params = self._build_user_scope_condition(context, filters, user_alias="u")
|
||||
conditions.append(user_scope_cond)
|
||||
@@ -874,6 +1090,85 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
params[f"{prefix}_date_to"] = self._normalize_date(filters["dateTo"])
|
||||
return "".join(clauses), params
|
||||
|
||||
def _resolve_requested_tenant_name(
|
||||
self,
|
||||
requested_tenant_code: str | None,
|
||||
requested_area: str | None,
|
||||
context: dict[str, Any],
|
||||
) -> str:
|
||||
tenant_code = str(requested_tenant_code or "").strip()
|
||||
if tenant_code:
|
||||
if tenant_code == str(context.get("tenant_code") or "").strip():
|
||||
return str(context.get("tenant_scope_value") or context.get("tenant_name") or context.get("area") or "").strip()
|
||||
if tenant_code == "PUBLIC":
|
||||
return "公共"
|
||||
if tenant_code == "PROVINCIAL":
|
||||
return "省级"
|
||||
return str(requested_area or "").strip()
|
||||
|
||||
@staticmethod
|
||||
def _document_tenant_name_sql(document_alias: str) -> str:
|
||||
return (
|
||||
"CASE "
|
||||
f"WHEN NULLIF(BTRIM({document_alias}.tenant_code), '') = 'PUBLIC' THEN '公共' "
|
||||
f"WHEN NULLIF(BTRIM({document_alias}.tenant_code), '') = 'PROVINCIAL' THEN '省级' "
|
||||
f"ELSE COALESCE(NULLIF(BTRIM({document_alias}.region), ''), '未分配地区') "
|
||||
"END"
|
||||
)
|
||||
|
||||
def _user_tenant_filter_sql(
|
||||
self,
|
||||
user_alias: str,
|
||||
params: dict[str, Any],
|
||||
tenant_code: str | None,
|
||||
tenant_name: str | None,
|
||||
prefix: str,
|
||||
) -> list[str]:
|
||||
normalized_tenant_code = str(tenant_code or "").strip()
|
||||
normalized_tenant_name = str(tenant_name or "").strip()
|
||||
if normalized_tenant_code:
|
||||
params[f"{prefix}_tenant_code"] = normalized_tenant_code
|
||||
return [f"{user_alias}.tenant_code = :{prefix}_tenant_code"]
|
||||
if normalized_tenant_name:
|
||||
params[f"{prefix}_tenant_name"] = normalized_tenant_name
|
||||
return [f"COALESCE({user_alias}.area, '') = :{prefix}_tenant_name"]
|
||||
return ["1 = 0"]
|
||||
|
||||
def _login_tenant_filter_sql(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
tenant_code: str | None,
|
||||
tenant_name: str | None,
|
||||
prefix: str,
|
||||
) -> list[str]:
|
||||
normalized_tenant_code = str(tenant_code or "").strip()
|
||||
normalized_tenant_name = str(tenant_name or "").strip()
|
||||
if normalized_tenant_code:
|
||||
params[f"{prefix}_tenant_code"] = normalized_tenant_code
|
||||
return [f"e.tenant_code_snapshot = :{prefix}_tenant_code"]
|
||||
if normalized_tenant_name:
|
||||
params[f"{prefix}_tenant_name"] = normalized_tenant_name
|
||||
return [f"COALESCE(e.area_snapshot, '') = :{prefix}_tenant_name"]
|
||||
return ["1 = 0"]
|
||||
|
||||
def _document_tenant_filter_sql(
|
||||
self,
|
||||
alias_prefix: str,
|
||||
params: dict[str, Any],
|
||||
tenant_code: str | None,
|
||||
tenant_name: str | None,
|
||||
prefix: str,
|
||||
) -> list[str]:
|
||||
normalized_tenant_code = str(tenant_code or "").strip()
|
||||
normalized_tenant_name = str(tenant_name or "").strip()
|
||||
if normalized_tenant_code:
|
||||
params[f"{prefix}_tenant_code"] = normalized_tenant_code
|
||||
return [f"{alias_prefix}.tenant_code = :{prefix}_tenant_code"]
|
||||
if normalized_tenant_name:
|
||||
params[f"{prefix}_tenant_name"] = normalized_tenant_name
|
||||
return [f"COALESCE({alias_prefix}.region, '') = :{prefix}_tenant_name"]
|
||||
return ["1 = 0"]
|
||||
|
||||
@staticmethod
|
||||
def _normalize_date(value: Any):
|
||||
from datetime import date as date_type
|
||||
@@ -890,6 +1185,20 @@ class UsageStatsServiceImpl(IUsageStatsService):
|
||||
return "pc"
|
||||
return "other"
|
||||
|
||||
@staticmethod
|
||||
def _normalize_failure_reason(value: str | None, limit: int = 255) -> str | None:
|
||||
"""裁剪登录失败原因,避免审计表二次写入失败。"""
|
||||
if value is None:
|
||||
return None
|
||||
normalized = " ".join(str(value).split()).strip()
|
||||
if not normalized:
|
||||
return None
|
||||
if len(normalized) <= limit:
|
||||
return normalized
|
||||
if limit <= 3:
|
||||
return normalized[:limit]
|
||||
return f"{normalized[: limit - 3]}..."
|
||||
|
||||
@staticmethod
|
||||
def _to_int(value: Any) -> int | None:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user