跳转至

BUG-P3B-02 测试报告:/auth/me 未返回师傅真实 display_name 和 worker_id

DWLG 编号:DWLG-20260811-06 日期:2026-08-11 报告人:DT(听写) 关联文档DWLG-20260811-05 全面业务测试过程 关联测试项:W3(/auth/me 返回师傅信息)


一、问题描述

1.1 现象

师傅端使用 worker_uat_0001 / worker123 登录后,调用 GET /api/v1/auth/me 获取当前用户信息,返回的 display_nameUSR-WKR-UAT0000001(user_id),而非数据库中存储的 張師傅。同时,响应体中缺少 worker_id 字段。

1.2 预期行为

{
  "user_id": "USR-WKR-UAT0000001",
  "roles": ["FL"],
  "permissions": ["cpt:list:self"],
  "display_name": "張師傅",
  "worker_id": "WKR0UAT0000001"
}

1.3 实际行为

{
  "user_id": "USR-WKR-UAT0000001",
  "roles": ["FL"],
  "permissions": ["cpt:list:self"],
  "display_name": "USR-WKR-UAT0000001"
}

1.4 影响分析

维度 影响
用户体验 师傅端 App 冷启动 hydrate 时,顶部显示"USR-WKR-UAT0000001"而非"張師傅",用户体验差
功能完整性 师傅端无法从 /auth/me 获取 worker_id,需从 JWT token 中解析(前端可 workaround)
安全性 无安全风险(user_id 本身不是敏感信息,但暴露了内部 ID 格式)
数据一致性 /auth/login 返回的 user.display_name 是正确的"張師傅",但 /auth/me 返回错误值,两端不一致

二、根因分析

2.1 问题代码位置

文件app/interfaces/api/auth.py 行号:206-214

@router.get("/me", response_model=UserMeResponse)
async def current_user(user: CurrentUser = Depends(get_current_user)) -> UserMeResponse:
    """当前登录用户信息(roles 为数组)."""
    return UserMeResponse(
        user_id=user.user_id,
        roles=[user.role] if user.role else [],
        permissions=user.permissions,
        display_name="开发管理员" if user.role == "TL" else user.user_id,  # ← 问题行
    )

2.2 根因

  1. display_name 硬编码:第 213 行对非 TL 角色直接用 user.user_id 作为 display_name,没有查询 sys_users 表获取真实的 display_name 字段
  2. worker_id 未返回UserMeResponse 模型没有 worker_id 字段,虽然 CurrentUser 对象中已包含 worker_id 属性(从 JWT claims 中提取)

2.3 对比:/auth/login 的正确实现

/auth/login 端点(auth.py:65-179)从数据库查询 sys_users 表,正确返回了 display_name

# auth.py:106-116 — 从数据库查询
row = (
    await db.execute(
        text(
            "SELECT u.user_id, u.username, u.password_hash, u.display_name,"
            " u.worker_id, u.status,"
            " ARRAY_AGG(r.role_id) FILTER (WHERE r.role_id IS NOT NULL) AS roles"
            " FROM sys_users u"
            " LEFT JOIN sys_user_roles r ON r.user_id = u.user_id"
            " WHERE u.username = :uname AND u.deleted_at IS NULL"
            " GROUP BY u.user_id, u.username, u.password_hash,"
            " u.display_name, u.worker_id, u.status"
        ),
        {"uname": req.username},
    )
).first()

# auth.py:176 — 正确使用 row.display_name
display_name=str(row.display_name),

2.4 CurrentUser 已有 worker_id

文件app/interfaces/dependencies.py 行号:18-33, 74-82

class CurrentUser:
    def __init__(
        self,
        user_id: str,
        role: str = "",
        permissions: list[str] | None = None,
        worker_id: str | None = None,    # ← 已有 worker_id
        customer_id: str | None = None,
    ) -> None:
        self.user_id = user_id
        self.worker_id = worker_id        # ← 已赋值
        ...

# 从 JWT payload 中提取
worker_id: str | None = payload.get("worker_id")    # ← 已从 JWT 提取

三、修复方案

3.1 方案 A(推荐):从 CurrentUser 直接获取

优势:无需查库,JWT 中已有 worker_id;display_name 可从 sys_users 查询或缓存

修改 1app/application/dtos/auth.py — UserMeResponse 增加 worker_id 字段

class UserMeResponse(BaseModel):
    user_id: str
    roles: list[str]
    permissions: list[str]
    display_name: str
    worker_id: str | None = None    # ← 新增
    customer_id: str | None = None  # ← 新增(可选,客户端场景)

修改 2app/interfaces/api/auth.py — current_user 函数

@router.get("/me", response_model=UserMeResponse)
async def current_user(
    user: CurrentUser = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> UserMeResponse:
    """当前登录用户信息(roles 为数组).

    师傅(FL)返回 worker_id;客户(CST)返回 customer_id。
    display_name 从 sys_users 表查询(TL 固定为"开发管理员")。
    """
    display_name = "开发管理员" if user.role == "TL" else user.user_id
    worker_id = None
    customer_id = None

    if user.role == "FL":
        worker_id = user.worker_id
        # 查询 sys_users 获取真实 display_name
        row = (
            await db.execute(
                text(
                    "SELECT display_name FROM sys_users"
                    " WHERE user_id = :uid AND deleted_at IS NULL"
                ),
                {"uid": user.user_id},
            )
        ).first()
        if row and row.display_name:
            display_name = str(row.display_name)
    elif user.role == "CST":
        customer_id = user.customer_id
        # 客户 display_name 从 mds_cpl_customers 查询
        if user.customer_id:
            row = (
                await db.execute(
                    text(
                        "SELECT name FROM mds_cpl_customers"
                        " WHERE customer_id = :cid AND deleted_at IS NULL"
                    ),
                    {"cid": user.customer_id},
                )
            ).first()
            if row and row.name:
                display_name = str(row.name)

    return UserMeResponse(
        user_id=user.user_id,
        roles=[user.role] if user.role else [],
        permissions=user.permissions,
        display_name=display_name,
        worker_id=worker_id,
        customer_id=customer_id,
    )

3.2 方案 B(最小改动):仅从 JWT 提取,不查库

优势:改动最小,不增加 DB 查询 劣势:display_name 仍无法获取(JWT 中未存储 display_name)

@router.get("/me", response_model=UserMeResponse)
async def current_user(user: CurrentUser = Depends(get_current_user)) -> UserMeResponse:
    """当前登录用户信息."""
    return UserMeResponse(
        user_id=user.user_id,
        roles=[user.role] if user.role else [],
        permissions=user.permissions,
        display_name="开发管理员" if user.role == "TL" else (
            user.worker_id or user.customer_id or user.user_id
        ),  # 仍非真实 display_name,但比 user_id 更有意义
        worker_id=user.worker_id,        # ← 新增
        customer_id=user.customer_id,    # ← 新增
    )

3.3 推荐方案

推荐方案 A:虽然多一次 DB 查询,但能返回正确的 display_name,用户体验最佳。/auth/me 是低频接口(仅冷启动调用),额外查询开销可接受。


四、测试验证

4.1 修复后预期响应

师傅(FL)

{
  "user_id": "USR-WKR-UAT0000001",
  "roles": ["FL"],
  "permissions": ["cpt:list:self"],
  "display_name": "張師傅",
  "worker_id": "WKR0UAT0000001"
}

客户(CST)

{
  "user_id": "CUS0UAT0000009",
  "roles": ["CST"],
  "permissions": ["cpt:list:self"],
  "display_name": "周太",
  "customer_id": "CUS0UAT0000009"
}

管理员(TL)

{
  "user_id": "USR-DEV-ADMIN-000001",
  "roles": ["TL"],
  "permissions": ["*"],
  "display_name": "开发管理员"
}

4.2 验证命令

# 师傅登录后验证
$workerToken = (Invoke-RestMethod -Uri "http://localhost:8000/api/v1/auth/login" `
    -Method Post -Body '{"username":"worker_uat_0001","password":"worker123"}' `
    -ContentType "application/json").access_token
Invoke-RestMethod -Uri "http://localhost:8000/api/v1/auth/me" `
    -Headers @{"Authorization"="Bearer $workerToken"} | ConvertTo-Json
# 预期:display_name="張師傅", worker_id="WKR0UAT0000001"

五、优先级与计划

维度 内容
严重程度 P2(中)
影响范围 师傅端 App 冷启动 hydrate + 客户端 App 冷启动 hydrate
修复时限 生产环境部署前必须修复
修复负责人 Kimi(代码修正)
验证负责人 DT(听写,回归测试)
修复状态 已修复(2026-08-11 14:06)— 采用方案 A
回归验证 通过(2026-08-11 14:08)— W3 从 FAIL 转为 PASS;27 项测试 25 PASS / 0 FAIL / 2 SKIP

5.1 修复确认

修复内容(auth.py:56-62, 208-251): 1. UserMeResponse 增加 worker_id: str | None = Nonecustomer_id: str | None = None 字段 2. current_user 函数增加 db: AsyncSession 依赖,查询 sys_users(FL 角色)或 mds_cpl_customers(CST 角色)获取真实 display_name 3. 返回 worker_idcustomer_id

修复后 /auth/me 响应

{
  "user_id": "USR-WKR-UAT0000001",
  "roles": ["FL"],
  "permissions": ["cpt:list:self"],
  "display_name": "張師傅",
  "worker_id": "WKR0UAT0000001",
  "customer_id": null
}


修订记录

版本 日期 修订人 修订内容
V1.0 2026-08-11 DT(听写) 创建:BUG-P3B-02 测试报告,含根因分析 + 2 套修复方案 + 验证命令

本报告对齐 DIP1-WLG V1.1 规范,编号 DWLG-20260811-06。BUG-P3B-02 为 P2 级代码问题,需在生产环境部署前修复。