fix(timezone): 审批单据时间统一为 naive 北京时间,消除 8 小时偏差
成因: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)。
This commit is contained in:
@ -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):
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user