From 8043fa198bddf9caadd2e14b77593aec8ef37072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E7=9D=BF=20=E5=BC=A0?= Date: Fri, 9 Oct 2026 09:39:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=85=AC=E5=8F=B8=E6=8E=88=E6=9D=83=E4=B8=8E=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=85=AC=E5=8F=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/views/Users.test.js | 85 +++++++- frontend/src/views/Users.vue | 48 ++++- internal/handler/api.go | 10 +- internal/model/model.go | 24 ++- internal/repository/user.go | 32 ++- internal/repository/user_organization.go | 192 ++++++++++++++++++ .../user_organization_integration_test.go | 133 ++++++++++++ internal/repository/user_organization_test.go | 140 +++++++++++++ internal/repository/user_test.go | 5 + internal/service/auth.go | 16 +- internal/service/auth_test.go | 27 +++ 11 files changed, 687 insertions(+), 25 deletions(-) create mode 100644 internal/repository/user_organization.go create mode 100644 internal/repository/user_organization_integration_test.go create mode 100644 internal/repository/user_organization_test.go diff --git a/frontend/src/views/Users.test.js b/frontend/src/views/Users.test.js index 1cfd8ac1..b62b10f4 100644 --- a/frontend/src/views/Users.test.js +++ b/frontend/src/views/Users.test.js @@ -7,7 +7,7 @@ import { organizationsApi } from '../api/organizations' const RoleSelectStub = { props: { - modelValue: Array, + modelValue: [Array, Number], multiple: Boolean }, emits: ['update:modelValue'], @@ -33,6 +33,7 @@ vi.mock('../stores/users', () => ({ useUsersStore: () => usersStore })) vi.mock('../stores/auth', () => ({ useAuthStore: () => ({ isAdmin: true }) })) vi.mock('../api/organizations', () => ({ organizationsApi: { + list: vi.fn().mockResolvedValue({ data: [{ id: 1, name: '甲公司', status: 'active' }, { id: 2, name: '乙公司', status: 'inactive' }] }), listRoles: vi.fn() } })) @@ -147,6 +148,8 @@ describe('新增用户后列表与搜索框保持一致', () => { state.form.username = 'lisi' state.form.password = '123456' state.form.role_ids = [2] + state.form.organization_ids = [1] + state.form.default_organization_id = 1 await state.handleSave() await flushPromises() @@ -176,6 +179,86 @@ describe('新增用户后列表与搜索框保持一致', () => { }) }) +describe('用户公司授权', () => { + test('编辑普通用户非空授权时必须选择默认公司', async () => { + organizationsApi.listRoles.mockResolvedValue({ data: [{ id: 2, code: 'warehouse_admin', enabled: true, name: '仓库管理员' }] }) + usersStore.updateUser.mockClear() + const wrapper = mountUsers() + await flushPromises() + const state = wrapper.vm.$.setupState + state.showEditModal({ id: 8, username: 'user', roles: [{ id: 2 }], organization_ids: [1], default_organization_id: null }) + await state.handleSave() + expect(usersStore.updateUser).not.toHaveBeenCalled() + wrapper.unmount() + }) + + test('超级管理员无需逐项授权,可以选择启用公司作为默认公司', async () => { + organizationsApi.listRoles.mockResolvedValue({ data: [{ id: 1, code: 'super_admin', enabled: true, name: '超级管理员' }] }) + const wrapper = mountUsers() + await flushPromises() + const state = wrapper.vm.$.setupState + state.showAddModal() + Object.assign(state.form, { username: 'admin2', password: 'secret', role_ids: [1], default_organization_id: 1 }) + await state.handleSave() + expect(state.defaultOrganizations.map(item => item.id)).toEqual([1]) + expect(usersStore.createUser).toHaveBeenLastCalledWith(expect.objectContaining({ organization_ids: [], default_organization_id: 1 })) + wrapper.unmount() + }) + + test('停用授权可以移除但不能重新分配给未持有的用户', async () => { + const wrapper = mountUsers() + await flushPromises() + const state = wrapper.vm.$.setupState + state.showEditModal({ id: 8, username: 'user', roles: [{ id: 2 }], organization_ids: [2], default_organization_id: null }) + await flushPromises() + const options = () => wrapper.findAllComponents(RoleOptionStub).filter(option => option.props('label') === '乙公司(已停用)') + expect(options()[0].props('disabled')).toBe(false) + state.form.organization_ids = [] + await flushPromises() + expect(options()[0].props('disabled')).toBe(true) + wrapper.unmount() + }) + + test('编辑展示历史停用授权,默认公司只提供已选启用公司', async () => { + const wrapper = mountUsers() + await flushPromises() + const state = wrapper.vm.$.setupState + state.showEditModal({ id: 8, username: 'user', roles: [{ id: 2 }], organization_ids: [1, 2], default_organization_id: 1 }) + await flushPromises() + expect(state.form.organization_ids).toEqual([1, 2]) + expect(wrapper.text()).toContain('乙公司(已停用)') + expect(state.defaultOrganizations.map(item => item.id)).toEqual([1]) + state.form.organization_ids = [2] + await flushPromises() + expect(state.form.default_organization_id).toBeNull() + wrapper.unmount() + }) + + test('新增普通用户缺少公司授权时禁止保存', async () => { + usersStore.createUser.mockClear() + const wrapper = mountUsers() + await flushPromises() + const state = wrapper.vm.$.setupState + state.showAddModal() + Object.assign(state.form, { username: 'new', password: 'secret', role_ids: [2] }) + await state.handleSave() + expect(usersStore.createUser).not.toHaveBeenCalled() + wrapper.unmount() + }) + + test('显式清空授权发送空集合和空默认公司', async () => { + const wrapper = mountUsers() + await flushPromises() + const state = wrapper.vm.$.setupState + state.showEditModal({ id: 8, username: 'user', roles: [{ id: 2 }], organization_ids: [1], default_organization_id: 1 }) + state.form.organization_ids = [] + await flushPromises() + await state.handleSave() + expect(usersStore.updateUser).toHaveBeenLastCalledWith(8, expect.objectContaining({ organization_ids: [], default_organization_id: null })) + wrapper.unmount() + }) +}) + // 已停用角色在下拉里怎么呈现,直接决定"编辑用户"这条路走不走得通:保存是全量替换 // 角色,前端若摘不掉那个停用角色、后端又要求角色全部启用,这个用户就永远存不回去。 // 现在后端已放行"本来就持有的停用角色",前端这两条是配套:既让人摘得掉,也不让人 diff --git a/frontend/src/views/Users.vue b/frontend/src/views/Users.vue index 1704f655..2146273f 100644 --- a/frontend/src/views/Users.vue +++ b/frontend/src/views/Users.vue @@ -75,6 +75,22 @@ /> + + + + + + + + + + @@ -90,7 +106,7 @@