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
+15
View File
@@ -42,6 +42,21 @@ def to_utc_naive(value: datetime) -> datetime:
return aware.astimezone(UTC).replace(tzinfo=None)
def from_local(value: datetime) -> datetime:
"""把**客户端传来的时间参数**换算成库内格式(UTC naive)。
REST 的时间参数是裸 `datetime`(`api/schemas/risk.py:32-33`),不带时区信息。
对面向中国客户的业务系统,裸值按**本地(北京)时间**解释才符合填表人的预期:
当成 UTC 会让前端填的"今天 00:00"实际查到昨天 08:00 起的数据。
带时区的值仍按它自己声明的时区处理。
与 `to_utc_naive` 的区别就在裸值上:这个把裸值当**本地**,那个当 **UTC**。
取库里的值用后者,接客户端输入用这个。
"""
aware = value if value.tzinfo is not None else value.replace(tzinfo=local_zone())
return aware.astimezone(UTC).replace(tzinfo=None)
def local_hour(value: datetime) -> int:
"""库内时间对应的**本地**小时(0-23)。