diff --git a/docs/superpowers/plans/2026-08-05-post-list-name-link-bound-users.md b/docs/superpowers/plans/2026-08-05-post-list-name-link-bound-users.md new file mode 100644 index 00000000..120a0d38 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-post-list-name-link-bound-users.md @@ -0,0 +1,285 @@ +# 职位列表名称链接 + 关联人员 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) |