From f3f841b1f01e0da2063a7b03f8e30f5cd9715e80 Mon Sep 17 00:00:00 2001 From: 12451 <12451@example.com> Date: Mon, 21 Sep 2026 05:20:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(users):=20=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E8=A1=A8=E6=A0=BC=E5=86=85=E7=9B=B4=E6=8E=A5=E5=88=86?= =?UTF-8?q?=E9=85=8D=E7=BB=84=E7=BB=87=EF=BC=88=E8=B6=85=E7=AE=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 起因 用户指出「所属组织要可以分配」。核查后发现两件事: 1. **组织没丢** —— 生产 46 个组织、28 个项目、68 个柜子都在; 2. **但 5 个用户里 0 个被分到组织**,所以那一列全是 `-`。 其中 2 个是总管理员(按规则本就不属组织,正确), 另 3 个(李四/企业管理员、张三/王五/操作员)**从未分配**,违反 「操作员必须有企业归属」的规则。 原流程要「点编辑图标 → 弹窗里选组织 → 保存」三步才能改一个用户, 3 个用户要重复 3 次。现改成**表格里直接分配**。 ## 改动 `users.tsx` 的「所属组织」列: - 超管 + 非总管理员 ⇒ 渲染成内联 `Select`,选完即存(`PUT /users/:id` 只传 `organization_id`,后端 `UpdateUserInput` 本就支持,无需改后端); - 总管理员行仍显示 `-`(后端 `resolve_update_org_id` 对 `role=2` 强制 `None`); - 非超管操作者仍只读。 顺带修 `api/user.ts`:`fetchUsers` 的参数类型漏了 `role`(上一轮加了角色过滤 但类型没跟上,靠结构兼容蒙混过关)。 ## 实测(测试环境,临时账号用完即删) ``` PUT 只传 organization_id → HTTP 200,库中 organization_id 已更新 OK 改分配到另一组织 → HTTP 200,库中已更新 OK 只改名不传组织 → 组织保持原值,未被清空 OK 给总管理员分组织 → HTTP 200 但库中仍为 None(被强制忽略) OK 列表 organization_name → 正确返回组织名 OK ``` ## ⚠️ 两个我自己出的岔子(都已修正) 1. **断言写错**:我原以为「给总管理员分组织」应返回 400,实测返回 200。 读库确认 `organization_id` 仍是 `None` —— 后端是**静默忽略**而非拒绝, 行为符合规则。是测试预期错,不是代码错。 2. **测试脚本留了脏数据**:临时用户删不掉,因为 `operation_logs` 有外键引用 (`log_operation` 写了日志)。已补删 `operation_logs` → `user_roles` → `users` 清理干净,测试库恢复 1 用户 / 1 组织 / 1 项目 / 1 柜子。 ## 待用户决定(生产数据,我不擅自改) 李四 / 张三 / 王五 三个账号要分到哪个组织?生产有 46 个组织可选。 现在超管可以在表格里直接点选分配,你也可以告诉我分配方案我来执行。 ## 验收 - web `npm run typecheck`:exit 0 - `npm run build`:exit 0 - 已发布生产(web 已更新,0 ERROR、域名 401/200) --- software/web/src/api/user.ts | 3 ++ software/web/src/pages/settings/users.tsx | 38 +++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/software/web/src/api/user.ts b/software/web/src/api/user.ts index c649bb1..9c995fd 100644 --- a/software/web/src/api/user.ts +++ b/software/web/src/api/user.ts @@ -47,11 +47,14 @@ export async function fetchUsers(params: { page?: number; page_size?: number; keyword?: string; + /** 角色过滤:0=操作员 1=企业管理员 2=总管理员;不给 = 全部 */ + role?: number; }): Promise { const searchParams = new URLSearchParams(); if (params.page) searchParams.set('page', String(params.page)); if (params.page_size) searchParams.set('page_size', String(params.page_size)); if (params.keyword) searchParams.set('keyword', params.keyword); + if (params.role !== undefined) searchParams.set('role', String(params.role)); const res = await api.get(`/users?${searchParams.toString()}`); return res.data; } diff --git a/software/web/src/pages/settings/users.tsx b/software/web/src/pages/settings/users.tsx index 23521ce..268f16c 100644 --- a/software/web/src/pages/settings/users.tsx +++ b/software/web/src/pages/settings/users.tsx @@ -339,6 +339,19 @@ export default function UsersPage() { setScopeModalVisible(true); }; + /** 表格内直接分配组织(仅超管可见/可操作)。 + * 不必进编辑弹窗 —— 生产上有 3 个用户从未分过组织,逐个进弹窗改太慢。 */ + const handleAssignOrg = async (user: User, orgId: number) => { + try { + await updateUser(user.id, { organization_id: orgId }); + Message.success(`已将「${user.name || user.phone}」分配到该组织`); + fetchData(); + } catch (e: any) { + Message.error(e?.message || '分配失败'); + fetchData(); // 失败也刷新,把下拉回滚到真实值 + } + }; + /** 表格列 */ const columns: TableColumnProps[] = [ { title: '手机号', dataIndex: 'phone', width: 130 }, @@ -347,8 +360,29 @@ export default function UsersPage() { { title: '所属组织', dataIndex: 'organization_name', - width: 150, - render: (v: string | null) => v || '-', + width: 180, + render: (v: string | null, record: User) => { + // 总管理员本就不属任何组织(后端 resolve_update_org_id 对 role=2 强制 None); + // 非超管操作者也只能看,不能改。 + if (!isSuperAdmin || record.role === 2) { + return v || '-'; + } + // 超管 + 非总管理员 ⇒ 表格里直接分配(生产上有用户从未分过组织) + return ( +