# 职位列表名称链接 + 关联人员 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 职位管理列表中,职位名称可点击打开查看弹窗;并展示该职位全部关联人员姓名(超宽换行)。 **Architecture:** 在 `list_posts` 对本页职位批量查询 `sys_user_post` + `sys_user`,向每行附加只读 `boundUserNames`。前端职位名称用 `el-button link`,新增「关联人员」列全部展示并换行;关联用户保存后刷新列表。 **Tech Stack:** FastAPI / SQLAlchemy / Vue 3 / Element Plus / pytest **Spec:** `docs/superpowers/specs/2026-08-05-post-list-name-link-bound-users-design.md` --- ## File map | File | Responsibility | |------|----------------| | `backend/app/crud/post.py` | `_row_to_dict` 增加 `boundUserNames`;批量 enrichment;`list_posts` 调用 | | `backend/tests/test_post_list_bound_users.py` | 空入参与展示名逻辑、列表 enrichment 调用回归 | | `web-admin/src/types/api.d.ts` | `PostItem.boundUserNames` | | `web-admin/src/views/system/post/index.vue` | 名称链接、关联人员列、保存后刷新 | --- ### Task 1: 后端批量查询关联人员姓名(TDD) **Files:** - Create: `backend/tests/test_post_list_bound_users.py` - Modify: `backend/app/crud/post.py` - [ ] **Step 1: Write failing tests** ```python """职位列表 boundUserNames enrichment。""" import inspect from app.crud import post as post_crud class _DummySession: def query(self, *args, **kwargs): # pragma: no cover raise AssertionError("空入参不应访问数据库") def test_bound_user_names_by_post_ids_empty(): db = _DummySession() assert post_crud._bound_user_names_by_post_ids(db, [], "zkxw") == {} assert post_crud._bound_user_names_by_post_ids(db, ["p1"], "") == {} def test_user_display_name_prefers_nick(): assert post_crud._user_display_name("张三", "zhangsan") == "张三" assert post_crud._user_display_name("", "zhangsan") == "zhangsan" assert post_crud._user_display_name(" ", "u1") == "u1" assert post_crud._user_display_name(None, None) == "" def test_list_posts_enriches_bound_user_names(): source = inspect.getsource(post_crud.list_posts) assert "_bound_user_names_by_post_ids" in source assert "bound_user_names" in source or "boundUserNames" in source ``` - [ ] **Step 2: Run tests — expect FAIL** ```bash cd backend && source venv/bin/activate && pytest tests/test_post_list_bound_users.py -v ``` Expected: FAIL(`_bound_user_names_by_post_ids` / `_user_display_name` 未定义) - [ ] **Step 3: Implement helpers + list enrichment** 在 `backend/app/crud/post.py`: 1. Import `SysUser`, `SysUserPost` from `app.models.models`。 2. 增加: ```python def _user_display_name(nick_name: Optional[str], user_no: Optional[str]) -> str: n = (nick_name or "").strip() if n: return n return (user_no or "").strip() def _bound_user_names_by_post_ids( db: Session, post_ids: List[str], tenant_no: str ) -> Dict[str, List[str]]: """批量查询职位关联用户显示名(nick_name 优先,否则 user_no)。""" out: Dict[str, List[str]] = {} ids = [pid for pid in (post_ids or []) if pid] tenant = (tenant_no or "").strip() if not ids or not tenant: return out rows = ( db.query(SysUserPost.post_id, SysUser.nick_name, SysUser.user_no) .join( SysUser, and_( SysUser.id == SysUserPost.user_id, SysUser.del_flag == 0, ), ) .filter( and_( SysUserPost.post_id.in_(ids), SysUserPost.tenant_no == tenant, SysUserPost.del_flag == 0, ) ) .all() ) for post_id, nick_name, user_no in rows: name = _user_display_name(nick_name, user_no) if not name: continue out.setdefault(post_id, []).append(name) return out ``` 3. 修改 `_row_to_dict`: ```python def _row_to_dict(p: SysPost, bound_user_names: Optional[List[str]] = None) -> Dict[str, Any]: return { "id": p.id, "postName": p.post_name, "postCode": p.post_code, "status": p.status, "tenantNo": p.tenant_no, "orderNum": p.order_num if p.order_num is not None else 0, "remark": p.remark or "", "boundUserNames": list(bound_user_names or []), "createTime": p.create_time.strftime("%Y-%m-%d %H:%M:%S") if p.create_time else "", "updateTime": p.update_time.strftime("%Y-%m-%d %H:%M:%S") if p.update_time else "", } ``` 4. 修改 `list_posts` 返回前: ```python name_map = _bound_user_names_by_post_ids( db, [x.id for x in rows], tenant_no or "" ) return [ _row_to_dict(x, bound_user_names=name_map.get(x.id, [])) for x in rows ], int(total) ``` - [ ] **Step 4: Run tests — expect PASS** ```bash cd backend && source venv/bin/activate && pytest tests/test_post_list_bound_users.py -v ``` - [ ] **Step 5: Commit** ```bash git add backend/app/crud/post.py backend/tests/test_post_list_bound_users.py git commit -m "feat(post): enrich post list with boundUserNames" ``` --- ### Task 2: 前端类型 + 列表 UI **Files:** - Modify: `web-admin/src/types/api.d.ts`(`Api.Post.PostItem`) - Modify: `web-admin/src/views/system/post/index.vue` - [ ] **Step 1: 扩展类型** 在 `PostItem` 中增加: ```ts boundUserNames?: string[] ``` - [ ] **Step 2: 职位名称改为链接** 将「职位名称」列改为: ```vue ``` - [ ] **Step 3: 新增「关联人员」列(编码与状态之间)** ```vue ``` scoped 样式: ```scss .post-bound-users-cell { display: inline-block; max-width: 100%; white-space: normal; word-break: break-word; line-height: 1.45; text-align: center; } ``` - [ ] **Step 4: 关联用户保存后刷新列表** 在 `handlePostBindSubmit` 成功分支增加 `await loadData()`: ```ts await fetchUpdatePostBoundUsers(postBindPostId.value, [...postBindUserCheckedKeys.value]) postBindDialogVisible.value = false await loadData() ``` - [ ] **Step 5: Commit** ```bash git add web-admin/src/types/api.d.ts web-admin/src/views/system/post/index.vue git commit -m "feat(web-admin): post list name link and bound users column" ``` --- ### Task 3: 图谱更新与手工验收 - [ ] **Step 1: 更新 graphify** ```bash ./scripts/update-graph.sh all ``` - [ ] **Step 2: 手工验收清单** 1. 打开 `/#/system-manage/system/post`。 2. 点击职位名称 → 打开查看弹窗。 3. 有关联用户的职位,「关联人员」列显示全部姓名;列窄时换行。 4. 无关联用户显示 `—`。 5. 「关联用户」保存后列表人名更新。 - [ ] **Step 3: Commit graphify(若有变更)** ```bash git add backend/graphify-out web-admin/graphify-out git commit -m "chore: update graphify after post list enrichment" ``` --- ## Spec coverage | Spec 项 | Task | |---------|------| | 名称快捷链接 → handleView | Task 2 | | boundUserNames 列表 enrichment | Task 1 | | 全部姓名、换行 | Task 2 | | 关联用户保存后刷新 | Task 2 | | Create/Update 不写该字段 | 无需改 API body(只读 enrichment) |