feat: 新增个人效能统计接口 /dashboard/my-stats(支持自选时段)

移动端「个人中心 → 工作统计」的数据源。现有接口拿不到这份数据:
/dashboard/people-history 支持 assignee_id 但只返回 WIP/PENDING/COMPLETED,
不含 REJECTED;/dashboard/rejected-tasks 则根本没有 assignee_id 参数 ——
「今日被驳回数」按现有接口无论如何都过滤不到个人。故补一个薄桥接端点。

服务端 get_my_stats(db, assignee_id, since, until) 返回两组指标:

生产战绩(按 Task.assignee_id 归因)
- tasks_completed / tasks_rejected:status 判定 + completed_at 落在区间,
  与 get_dashboard_stats 的 t_done_q 同一口径,只多了 assignee_id 过滤
- products_touched:按 product_id 去重,同一台设备做多道工序只算一台

操作统计(与 PC /dashboard/user-operations 严格同口径)
- receive/transfer:task_logs 的 receive / complete,按 operator_id 归因
- record:task_records 按 Task.assignee_id 归因,排除 '[' 开头的系统自动备注
- 前两项归因 operator_id、第三项归因 assignee_id 是 PC 端既有口径,
  此处刻意保持一致,便于工人自查的数与主管看到的面板对得上

端点侧新增 _parse_bound():裸时间字符串(无时区偏移)按北京时间解释,
否则会被当作服务器本地时间,边界整体偏 8 小时,出现「选了今日却统计到
昨天下午」。since/until 缺省为「本月 1 日 ~ 此刻」。
This commit is contained in:
2026-09-15 15:51:34 +08:00
parent 242a6d5463
commit 44ca09ae22
2 changed files with 167 additions and 0 deletions

View File

@ -5,8 +5,10 @@ from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.time_utils import BEIJING_TZ
from app.services.dashboard_service import (
get_dashboard_stats, DashboardStats,
get_my_stats, MyStats,
get_wip_tasks, WipTask,
get_completed_tasks, CompletedTask,
get_rejected_tasks, RejectedTask,
@ -22,6 +24,18 @@ from app.services.dashboard_service import (
router = APIRouter(prefix="/dashboard", tags=["管理看板"])
def _parse_bound(value: str | None) -> datetime | None:
"""解析 ISO 时间边界。
裸时间(无时区偏移)按**北京时间**解释 —— 否则会被当作服务器本地时间,
在边界上整体偏移 8 小时,出现「选了今日却统计到昨天下午」这类错位。
"""
if not value:
return None
dt = datetime.fromisoformat(value)
return dt.replace(tzinfo=BEIJING_TZ) if dt.tzinfo is None else dt
@router.get("/stats", response_model=DashboardStats)
async def dashboard_stats(
since: str | None = Query(None, description="起始日期 ISO 如 2026-08-01T00:00:00"),
@ -39,6 +53,26 @@ async def dashboard_stats(
return await get_dashboard_stats(db, since=since_dt, until=until_dt)
@router.get("/my-stats", response_model=MyStats)
async def my_stats(
assignee_id: str = Query(..., description="负责人ID移动端传当前登录用户 username"),
since: str | None = Query(None, description="起始时间 ISO含时区偏移如 2026-09-01T00:00:00+08:00缺省=本月 1 日"),
until: str | None = Query(None, description="截止时间 ISO含时区偏移缺省=此刻"),
db: AsyncSession = Depends(get_db),
):
"""
一线工人个人效能 — 移动端「工作统计」页,支持自选时段。
- 生产战绩(完成 / 被驳回 / 参与产品)按本人名下任务归因;
- 操作统计(接收 / 转交 / 上传备注)与 PC /dashboard/user-operations 严格同口径,
工人自查的数与主管看到的面板对得上。
⚠️ since/until 必须带时区偏移。移动端发的是北京时间 (+08:00)
不带偏移的裸字符串会被当成"本地时间"导致边界偏移 8 小时。
"""
return await get_my_stats(db, assignee_id, since=_parse_bound(since), until=_parse_bound(until))
@router.get("/wip-tasks", response_model=list[WipTask])
async def wip_tasks(
limit: int = Query(20, ge=1, le=100),