fix(时长): 所有时长计算排除非工作日 + 修复两处时区多算8小时
- get_wip_tasks/get_people_workload/get_people_history/product active_duration_hours/analytics 均改用 working_duration_hours - 修复 get_wip_tasks 与 active_duration_hours 的 naive 时间误标北京时间 bug(DB实存UTC,原多算8小时) - 每处读取 holidays 表排除配置的放假日期
This commit is contained in:
@ -114,11 +114,12 @@ def _to_bj(dt: datetime | None) -> datetime | None:
|
||||
return dt
|
||||
|
||||
|
||||
def _duration_hours(start: datetime | None, end: datetime) -> float:
|
||||
"""计算单台耗时(小时),无开始时间返回 0。"""
|
||||
def _duration_hours(start: datetime | None, end: datetime, holidays: set = None) -> float:
|
||||
"""计算单台耗时(工作小时,排除周末/节假日),无开始时间返回 0。"""
|
||||
if not start:
|
||||
return 0.0
|
||||
return (end - start).total_seconds() / 3600
|
||||
from app.core.time_utils import working_duration_hours
|
||||
return working_duration_hours(start, end, holidays)
|
||||
|
||||
|
||||
# ============================================================
|
||||
@ -142,10 +143,15 @@ async def get_capability_profile(
|
||||
Task, TASK_STATUS_WIP, TASK_STATUS_PENDING, TASK_STATUS_COMPLETED,
|
||||
)
|
||||
from app.models.product import Product
|
||||
from app.models.holiday import Holiday
|
||||
from app.core.time_utils import get_beijing_time
|
||||
|
||||
now = get_beijing_time()
|
||||
|
||||
# 读取节假日(排除非工作日)
|
||||
hres = await db.execute(select(Holiday.day))
|
||||
holidays = {r[0] for r in hres}
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
Task.assignee_id, Task.status,
|
||||
@ -196,7 +202,7 @@ async def get_capability_profile(
|
||||
"hours": 0.0, "prio": 9,
|
||||
"first_start": None, "last_completed": None,
|
||||
})
|
||||
entry["hours"] += _duration_hours(start, end)
|
||||
entry["hours"] += _duration_hours(start, end, holidays)
|
||||
if start and (entry["first_start"] is None or start < entry["first_start"]):
|
||||
entry["first_start"] = start
|
||||
completed_dt = _to_bj(row[4]) if row[4] else None
|
||||
|
||||
@ -209,7 +209,12 @@ async def get_dashboard_stats(
|
||||
async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]:
|
||||
from app.models.task import Task, TASK_STATUS_PENDING, TASK_STATUS_WIP
|
||||
from app.models.product import Product
|
||||
from app.core.time_utils import get_beijing_time, BEIJING_TZ
|
||||
from app.models.holiday import Holiday
|
||||
from app.core.time_utils import get_beijing_time, to_beijing, working_duration_hours
|
||||
|
||||
# 读取节假日(排除非工作日)
|
||||
hres = await db.execute(select(Holiday.day))
|
||||
holidays = {r[0] for r in hres}
|
||||
|
||||
stmt = (
|
||||
select(Task, Product.serial_number, Product.external_serial, Product.material_name, Product.spec_model)
|
||||
@ -231,12 +236,9 @@ async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]:
|
||||
for task, product_sn, ext_sn, mat_name, spec in rows:
|
||||
start = task.received_at or task.created_at
|
||||
if start:
|
||||
if start.tzinfo is None:
|
||||
start = start.replace(tzinfo=BEIJING_TZ)
|
||||
else:
|
||||
start = start.astimezone(BEIJING_TZ)
|
||||
hours = round((now - start).total_seconds() / 3600, 1)
|
||||
recv_str = start.strftime("%m-%d %H:%M")
|
||||
start_bj = to_beijing(start) # 🚀 naive 按 UTC 转北京时间(修复多算8小时)
|
||||
hours = working_duration_hours(start_bj, now, holidays)
|
||||
recv_str = start_bj.strftime("%m-%d %H:%M")
|
||||
else:
|
||||
hours = 0
|
||||
recv_str = ""
|
||||
@ -683,7 +685,12 @@ async def get_people_workload(db: AsyncSession) -> list[PersonWorkload]:
|
||||
"""上帝视角 — 按负责人聚合当前在制品设备(WIP/PENDING 任务,product 去重,含滞留时长)。"""
|
||||
from app.models.task import Task, TASK_STATUS_PENDING, TASK_STATUS_WIP
|
||||
from app.models.product import Product
|
||||
from app.core.time_utils import get_beijing_time, BEIJING_TZ
|
||||
from app.models.holiday import Holiday
|
||||
from app.core.time_utils import get_beijing_time, to_beijing, working_duration_hours
|
||||
|
||||
# 读取节假日(排除非工作日)
|
||||
hres = await db.execute(select(Holiday.day))
|
||||
holidays = {r[0] for r in hres}
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
@ -749,7 +756,7 @@ async def get_people_workload(db: AsyncSession) -> list[PersonWorkload]:
|
||||
devices: list[PersonDevice] = []
|
||||
for product_id, e in prods.items():
|
||||
dt = e["earliest_dt"]
|
||||
hours = round((now - dt).total_seconds() / 3600, 1) if dt else 0.0
|
||||
hours = working_duration_hours(dt, now, holidays) if dt else 0.0
|
||||
received_str = dt.strftime("%m-%d %H:%M") if dt else None
|
||||
devices.append(PersonDevice(
|
||||
product_id=product_id,
|
||||
@ -787,11 +794,16 @@ async def get_people_history(
|
||||
"""上帝视角 — 人员效能与工时台账(平铺 Task 明细,含 WIP/PENDING/COMPLETED)。"""
|
||||
from app.models.task import Task, TASK_STATUS_WIP, TASK_STATUS_PENDING, TASK_STATUS_COMPLETED
|
||||
from app.models.product import Product
|
||||
from app.core.time_utils import get_beijing_time, BEIJING_TZ
|
||||
from app.core.time_utils import get_beijing_time, BEIJING_TZ, to_beijing, working_duration_hours
|
||||
from app.models.holiday import Holiday
|
||||
from sqlalchemy import func
|
||||
|
||||
now = get_beijing_time()
|
||||
|
||||
# 读取节假日(排除非工作日)
|
||||
hres = await db.execute(select(Holiday.day))
|
||||
holidays = {r[0] for r in hres}
|
||||
|
||||
# ── 关联 TaskRecord:最新有效备注 + 记录总数 ──
|
||||
from app.models.task import TaskRecord
|
||||
from sqlalchemy import and_, or_
|
||||
@ -915,7 +927,7 @@ async def get_people_history(
|
||||
else:
|
||||
end_dt = now
|
||||
completed_str = None
|
||||
hours = round((end_dt - received_dt).total_seconds() / 3600, 1) if received_dt else 0.0
|
||||
hours = working_duration_hours(received_dt, end_dt, holidays) if received_dt else 0.0
|
||||
records.append(PersonHistoryRecord(
|
||||
task_id=str(row[0]),
|
||||
task_name=row[1] or "",
|
||||
|
||||
@ -541,11 +541,14 @@ async def get_all_products(
|
||||
has_img = bool(row[3] and row[3] != "[]" and row[3] != "null")
|
||||
latest_record_map[row[0]] = (row[1], row[2], has_img, row[4])
|
||||
|
||||
# 🔧 当前人滞留时长:每个产品活跃任务(WIP/PENDING)最早接手时间 → 小时
|
||||
# 🔧 当前人滞留时长:每个产品活跃任务(WIP/PENDING)最早接手时间 → 小时(排除非工作日)
|
||||
active_duration_map: dict[uuid.UUID, float] = {}
|
||||
if product_ids:
|
||||
from sqlalchemy import func as sa_func
|
||||
from app.core.time_utils import get_beijing_time, BEIJING_TZ
|
||||
from app.core.time_utils import get_beijing_time, to_beijing, working_duration_hours
|
||||
from app.models.holiday import Holiday
|
||||
hres = await db.execute(select(Holiday.day))
|
||||
holidays = {r[0] for r in hres}
|
||||
start_stmt = (
|
||||
select(
|
||||
Task.product_id,
|
||||
@ -563,11 +566,8 @@ async def get_all_products(
|
||||
start = row[1]
|
||||
if start is None:
|
||||
continue
|
||||
if start.tzinfo is None:
|
||||
start = start.replace(tzinfo=BEIJING_TZ)
|
||||
else:
|
||||
start = start.astimezone(BEIJING_TZ)
|
||||
active_duration_map[row[0]] = round((now - start).total_seconds() / 3600, 1)
|
||||
start_bj = to_beijing(start) # 🚀 naive 按 UTC 转北京时间(修复多算8小时)
|
||||
active_duration_map[row[0]] = working_duration_hours(start_bj, now, holidays)
|
||||
|
||||
return [
|
||||
ProductResponse(
|
||||
|
||||
Reference in New Issue
Block a user