fix(risk): 收敛剩余的时区口径(REST 时间参数、年龄、日报日期字段)

接 b3da1b6。上一条只改了凌晨规则与日报日界,剩下几处一并收掉:

1. risk_query_service.py:77-78:REST 的 start_time/end_time 是**裸 datetime**,
   原先原样透传去比库内 UTC 列,而 Agent 路径本来就带时区
   (risk_natural_language.py:117)——同一条筛选条件在界面与对话里会查出不同结果。
   timeutil 新增 from_local:裸值按**北京时间**解释(面向中国客户的业务系统,
   填表人的预期就是本地时间),带时区的按其自身时区处理。它与 to_utc_naive 的区别
   正在裸值上:取库里的值用后者,接客户端输入用这个。
2. risk_scan_service.py 与 risk_judgement_service.py 的 _age:一处用 UTC 日期、
   一处用服务器 date.today(),生日边界上同一客户会差一岁、65 岁阈值可能翻面。
   统一走 local_date(北京时间)。
3. risk_daily_report_service.py:122 的 report_date 与 :244 的 created_today:
   原先取 UTC 日期,北京 08:00 之前会把"今天新增的预警"算成昨天。

ruff / mypy(135 文件) / 603 unit+contract 全绿。
This commit is contained in:
2026-09-11 12:55:00 +08:00
parent 554fbbcaab
commit 8ac0b794ff
5 changed files with 38 additions and 10 deletions
+6 -3
View File
@@ -7,11 +7,11 @@
from __future__ import annotations
import re
from datetime import date, datetime
from datetime import UTC, date, datetime
from decimal import Decimal, InvalidOperation
from typing import Any
from app.core.timeutil import local_hour
from app.core.timeutil import local_date, local_hour
VERDICT_RELEASE = "可考虑放行"
VERDICT_SUSPECTED_FALSE_POSITIVE = "疑似误报"
@@ -383,7 +383,10 @@ def _age(value: Any) -> int | None:
birth_date = _date(value)
if birth_date is None:
return None
today = date.today()
# 用**北京时间**的今天。原先这里用服务器 date.today()(东八区的机器上是北京日期,
# 但不保证),而扫描侧用 UTC 日期——两处口径不一致会让同一客户在生日边界上差一岁
# (docs/25 P1 #6)。统一走 timeutil。
today = local_date(datetime.now(UTC))
return today.year - birth_date.year - (
(today.month, today.day) < (birth_date.month, birth_date.day)
)