From 8755837fe820d2361628f2cafef256affbe7063c Mon Sep 17 00:00:00 2001 From: yueli Date: Thu, 10 Sep 2026 10:14:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(timezone):=20=E5=AE=A1=E6=89=B9=E5=8D=95?= =?UTF-8?q?=E6=8D=AE=E6=97=B6=E9=97=B4=E7=BB=9F=E4=B8=80=E4=B8=BA=20naive?= =?UTF-8?q?=20=E5=8C=97=E4=BA=AC=E6=97=B6=E9=97=B4=EF=BC=8C=E6=B6=88?= =?UTF-8?q?=E9=99=A4=208=20=E5=B0=8F=E6=97=B6=E5=81=8F=E5=B7=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 成因:approved_at / executed_at 列是 timestamp without time zone,而赋值用了 带时区的 datetime.now(timezone(timedelta(hours=8)))。psycopg2 对 naive 列不会 剥掉 tzinfo,而是转成 naive UTC 再写入,结果比同为北京时间的 created_at 早 8 小时。 (代码里并无 datetime.utcnow(),真实成因是 aware 值被驱动的隐式 UTC 转换。) 改动: · outbound_service / borrow_service 的审批分支 datetime.now(beijing_tz).replace(tzinfo=None) → beijing_time() (免审批分支已在上一轮改为 beijing_time,此处补齐审批分支); · purchase_service 审批/完结分支同源缺陷一并修复; · scrap_approval / scrap_approval_service 的 _beijing()、_beijing_now() 由 tz-aware 改为 naive —— 配合上述迁移把列类型改为 naive, 若仍返回 aware 值,timestamptz→timestamp 后会反向早 8 小时。 实测四表(scrap/outbound/borrow/purchase)created_at 与 approved_at 差值 均在同一秒内(-1ms ~ -12ms)。 --- inventory-backend/app/models/scrap_approval.py | 12 +++++++++--- inventory-backend/app/services/borrow_service.py | 11 ++++++----- inventory-backend/app/services/outbound_service.py | 11 ++++++----- inventory-backend/app/services/purchase_service.py | 6 +++--- .../app/services/scrap_approval_service.py | 11 +++++++++-- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/inventory-backend/app/models/scrap_approval.py b/inventory-backend/app/models/scrap_approval.py index e834bca..c379c69 100644 --- a/inventory-backend/app/models/scrap_approval.py +++ b/inventory-backend/app/models/scrap_approval.py @@ -1,10 +1,16 @@ -from app.extensions import db +from app.extensions import db, beijing_time import json def _beijing_now(): - from datetime import datetime, timezone, timedelta - return datetime.now(timezone(timedelta(hours=8))) + """ + 统一时间口径:naive 北京时间,与 created_at / OutboundApproval / BorrowApproval 一致。 + + ★ 原先返回 tz-aware 时间,配合 timestamptz 列才正确;现已将该表列类型统一为 + timestamp without time zone(见 db_migrations/unify_approval_timezone.sql), + 故此处必须返回 naive 值,否则存库会被当 UTC 处理而早 8 小时。 + """ + return beijing_time() class ScrapApproval(db.Model): diff --git a/inventory-backend/app/services/borrow_service.py b/inventory-backend/app/services/borrow_service.py index f580c9a..f3eb1a3 100644 --- a/inventory-backend/app/services/borrow_service.py +++ b/inventory-backend/app/services/borrow_service.py @@ -1,6 +1,6 @@ from datetime import datetime import pytz -from app.extensions import db +from app.extensions import db, beijing_time from app.models.borrow import BorrowApproval from app.models.system import SysUser @@ -139,7 +139,8 @@ class BorrowApprovalService: ) else: # 默认不审批:创建即已通过(status=1),直接进入“待库管执行” - from datetime import datetime, timezone, timedelta + # ★ approved_at 必须与 created_at(beijing_time) 同为 naive 本地时间: + # approved_at 列是 timestamp without time zone,传 aware 时间会被驱动转成 UTC 存库,导致早 8 小时 approval = BorrowApproval( request_no=request_no, applicant_id=applicant_id, @@ -147,7 +148,7 @@ class BorrowApprovalService: borrower_name=borrower_name, status=1, actual_approver_id=applicant_id, - approved_at=datetime.now(timezone(timedelta(hours=8))), + approved_at=beijing_time(), ) approval.set_items(items) @@ -388,8 +389,8 @@ class BorrowApprovalService: Returns: (success: bool, message: str, approval: BorrowApproval or None) """ - beijing_tz = pytz.timezone('Asia/Shanghai') - current_time = datetime.now(beijing_tz).replace(tzinfo=None) + # ★ 统一取 naive 北京时间,与 created_at(beijing_time) 同口径 + current_time = beijing_time() approval = BorrowApproval.query.get(request_id) if not approval: diff --git a/inventory-backend/app/services/outbound_service.py b/inventory-backend/app/services/outbound_service.py index 9966e6b..6d07532 100644 --- a/inventory-backend/app/services/outbound_service.py +++ b/inventory-backend/app/services/outbound_service.py @@ -2,7 +2,7 @@ import uuid # .material -> .base refactor checked from datetime import datetime, timezone, timedelta from sqlalchemy import or_, func, desc, and_ from sqlalchemy.orm import joinedload -from app.extensions import db +from app.extensions import db, beijing_time from app.models.outbound import TransOutbound, OutboundApproval # 引入所有库存模型以进行查询 @@ -816,7 +816,8 @@ class OutboundApprovalService: ) else: # 默认不审批:创建即已通过(status=1),直接进入“待库管执行” - from datetime import datetime, timezone, timedelta + # ★ approved_at 必须与 created_at(beijing_time) 同为 naive 本地时间: + # approved_at 列是 timestamp without time zone,传 aware 时间会被驱动转成 UTC 存库,导致早 8 小时 approval = OutboundApproval( request_no=request_no, applicant_id=applicant_id, @@ -824,7 +825,7 @@ class OutboundApprovalService: outbound_type=outbound_type, status=1, actual_approver_id=applicant_id, - approved_at=datetime.now(timezone(timedelta(hours=8))), + approved_at=beijing_time(), ) # 直接存储前端传来的物料信息快照,不查询/不关联具体库存记录 @@ -1006,8 +1007,8 @@ class OutboundApprovalService: """ from app.models.outbound import OutboundApproval - beijing_tz = timezone(timedelta(hours=8)) - current_time = datetime.now(beijing_tz).replace(tzinfo=None) + # ★ 统一取 naive 北京时间,与 created_at(beijing_time) 同口径 + current_time = beijing_time() approval = OutboundApproval.query.get(request_id) if not approval: diff --git a/inventory-backend/app/services/purchase_service.py b/inventory-backend/app/services/purchase_service.py index 36dfa2e..bcf2372 100644 --- a/inventory-backend/app/services/purchase_service.py +++ b/inventory-backend/app/services/purchase_service.py @@ -1,7 +1,7 @@ import json from datetime import datetime, timezone, timedelta, date from sqlalchemy import func -from app.extensions import db +from app.extensions import db, beijing_time from app.models.purchase import PurchaseRequest from app.models.base import MaterialBase @@ -145,8 +145,8 @@ class PurchaseService: if not purchase: raise ValueError("采购申请不存在") - beijing_tz = timezone(timedelta(hours=8)) - now = datetime.now(beijing_tz) + # ★ approved_at 与 created_at(beijing_time) 同为 naive 本地时间,避免存库时被转成 UTC 早 8 小时 + now = beijing_time() # ★ 完结:库管将「已通过(1)」的申请单置为「已完结(4)」(仿出库审批) if action == 'close': diff --git a/inventory-backend/app/services/scrap_approval_service.py b/inventory-backend/app/services/scrap_approval_service.py index 23b69c0..731cccf 100644 --- a/inventory-backend/app/services/scrap_approval_service.py +++ b/inventory-backend/app/services/scrap_approval_service.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone, timedelta from sqlalchemy import func -from app.extensions import db +from app.extensions import db, beijing_time from app.models.scrap_approval import ScrapApproval from app.models.transaction import TransScrap @@ -11,7 +11,14 @@ logger = logging.getLogger(__name__) def _beijing(): - return datetime.now(timezone(timedelta(hours=8))) + """ + 统一时间口径:naive 北京时间。 + + ★ scrap_approval 的时间列已统一为 timestamp without time zone + (见 db_migrations/unify_approval_timezone.sql),必须返回 naive 值, + 否则 aware 值会被驱动转成 UTC 存库,比 created_at 早 8 小时。 + """ + return beijing_time() STOCK_MODELS = {}