From 5641545fd54a4bfdfe331babab13b648bdc7c9df Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 25 Aug 2026 09:52:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/iam/api/platformIam.test.ts | 33 +++++++- src/modules/iam/api/platformIam.ts | 41 +++++++++- .../iam/pages/InternalUsersView.test.ts | 81 ++++++++++++++++++- src/modules/iam/pages/InternalUsersView.vue | 59 ++++++++++++++ 4 files changed, 208 insertions(+), 6 deletions(-) diff --git a/src/modules/iam/api/platformIam.test.ts b/src/modules/iam/api/platformIam.test.ts index fb96c9eb..df42ce2f 100644 --- a/src/modules/iam/api/platformIam.test.ts +++ b/src/modules/iam/api/platformIam.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' +import { overseasHttp } from '@/shared/api/overseasHttp' import { approveEnterpriseReview, archivePlatformTenant, @@ -24,6 +25,7 @@ import { listPlatformRoles, listPlatformInternalUsers, listPlatformManagedUsers, + listSharedReceivingOverseasWarehouseOptions, getPlatformUserOptions, qualificationFileName, replacePlatformRolePermissions, @@ -49,6 +51,10 @@ vi.mock('@/shared/api/http', () => ({ unwrapApiResponse: (response: { data: T }) => response.data, })) +vi.mock('@/shared/api/overseasHttp', () => ({ + overseasHttp: { get: vi.fn() }, +})) + const role: PlatformRoleResource = { id: 4000002, tenantId: 0, @@ -108,6 +114,7 @@ const managedUser: PlatformManagedUser = { roleNames: ['运营管理员'], lastLoginAt: '2026-06-01T10:30:00', protectedAccount: false, + overseasWarehouseId: null, } describe('platform IAM roles', () => { @@ -116,6 +123,7 @@ describe('platform IAM roles', () => { vi.mocked(http.post).mockReset() vi.mocked(http.put).mockReset() vi.mocked(http.delete).mockReset() + vi.mocked(overseasHttp.get).mockReset() }) it('loads roles from the platform IAM prefix', async () => { @@ -212,15 +220,15 @@ describe('platform IAM roles', () => { it('maintains paginated platform users through the unified user contract', async () => { const userOptions = { tenants: [{ id: '0', name: '平台', status: 'ACTIVE' }], - roles: [{ id: '40001', tenantId: '0', name: '运营管理员', status: 'ACTIVE' }], + roles: [{ id: '40001', tenantId: '0', roleCode: 'PLATFORM_OPERATOR', name: '运营管理员', status: 'ACTIVE' }], } const create = { username: 'zhangwei', displayName: '张伟', phone: '13800138000', email: 'zhangwei@example.com', - tenantId: '0', roleIds: ['40001'], password: 'Password1', status: 'ACTIVE' as const, + tenantId: '0', roleIds: ['40001'], overseasWarehouseId: null, password: 'Password1', status: 'ACTIVE' as const, } const update = { displayName: '张伟', phone: '13800138000', email: 'zhangwei@example.com', - tenantId: '0', roleIds: ['40001'], + tenantId: '0', roleIds: ['40001'], overseasWarehouseId: null, } const userPage = { records: [managedUser], page: 2, size: 10, total: 11, pages: 2 } vi.mocked(http.get).mockResolvedValueOnce({ @@ -247,6 +255,25 @@ describe('platform IAM roles', () => { expect(http.put).toHaveBeenNthCalledWith(3, `/platform/v1/iam/users/${managedUser.membershipId}/password`, { newPassword: 'ChangeMe123' }) }) + it('loads only active shared receiving overseas warehouses through the overseas gateway', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ + code: '200', message: 'success', data: { + records: [ + { id: '90071992547409931', customerId: '0', warehouseCode: 'LAX-01', warehouseName: '洛杉矶收货仓', warehouseType: 'OVERSEAS', countryCode: 'US', status: 'ACTIVE' }, + { id: '2', customerId: '100', warehouseCode: 'CUST', warehouseName: '客户仓', warehouseType: 'OVERSEAS', countryCode: 'US', status: 'ACTIVE' }, + { id: '3', customerId: '0', warehouseCode: 'PARTNER', warehouseName: '合作仓', warehouseType: 'OVERSEAS_PARTNER', countryCode: 'US', status: 'ACTIVE' }, + ], page: 1, size: 200, total: 3, pages: 1, + }, + }) + + await expect(listSharedReceivingOverseasWarehouseOptions()).resolves.toEqual([ + { id: '90071992547409931', warehouseCode: 'LAX-01', warehouseName: '洛杉矶收货仓', countryCode: 'US' }, + ]) + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/warehouse-admin', { + params: { customerId: '0', page: 1, size: 200, capabilityCode: 'RECEIVING', status: 'ACTIVE' }, + }) + }) + it('loads qualifications for the selected tenant', async () => { const qualification = { id: 12, diff --git a/src/modules/iam/api/platformIam.ts b/src/modules/iam/api/platformIam.ts index bdfa1177..adb523ad 100644 --- a/src/modules/iam/api/platformIam.ts +++ b/src/modules/iam/api/platformIam.ts @@ -1,4 +1,5 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import { overseasHttp } from '@/shared/api/overseasHttp' import type { PlatformPermissionResource } from '@/shared/types/platformPermission' export type { PlatformPermissionResource } from '@/shared/types/platformPermission' @@ -253,6 +254,7 @@ export interface PlatformManagedUser { roleNames: string[] lastLoginAt: string | null protectedAccount: boolean + overseasWarehouseId: number | string | null } export interface PlatformManagedUserPage { @@ -272,7 +274,14 @@ export interface PlatformManagedUserQuery { export interface PlatformUserOptions { tenants: Array<{ id: number | string; name: string; status: string }> - roles: Array<{ id: number | string; tenantId: number | string; name: string; status: string }> + roles: Array<{ id: number | string; tenantId: number | string; roleCode: string; name: string; status: string }> +} + +export interface SharedReceivingOverseasWarehouseOption { + id: string + warehouseCode: string + warehouseName: string + countryCode: string } export interface CreatePlatformManagedUserRequest { @@ -282,6 +291,7 @@ export interface CreatePlatformManagedUserRequest { email: string tenantId: number | string roleIds: Array + overseasWarehouseId: number | string | null password: string status: 'ACTIVE' | 'DISABLED' } @@ -292,6 +302,7 @@ export interface UpdatePlatformManagedUserRequest { email: string tenantId: number | string roleIds: Array + overseasWarehouseId: number | string | null } async function getData(path: string): Promise { @@ -558,6 +569,34 @@ export function getPlatformUserOptions() { return getData('/platform/v1/iam/users/options') } +export async function listSharedReceivingOverseasWarehouseOptions() { + interface WarehousePage { + records: Array<{ + id: number | string + customerId: number | string + warehouseCode: string + warehouseName: string + warehouseType: string + countryCode: string + status: string + }> + } + const response = await overseasHttp.get, ApiEnvelope>( + '/platform/v1/warehouse-admin', + { params: { customerId: '0', page: 1, size: 200, capabilityCode: 'RECEIVING', status: 'ACTIVE' } }, + ) + return unwrapApiResponse(response).records + .filter((warehouse) => String(warehouse.customerId) === '0' + && warehouse.warehouseType === 'OVERSEAS' + && warehouse.status === 'ACTIVE') + .map((warehouse) => ({ + id: String(warehouse.id), + warehouseCode: warehouse.warehouseCode, + warehouseName: warehouse.warehouseName, + countryCode: warehouse.countryCode, + })) +} + export function createPlatformManagedUser(payload: CreatePlatformManagedUserRequest) { return postData('/platform/v1/iam/users', payload) } diff --git a/src/modules/iam/pages/InternalUsersView.test.ts b/src/modules/iam/pages/InternalUsersView.test.ts index 1d6afc75..c2f027ba 100644 --- a/src/modules/iam/pages/InternalUsersView.test.ts +++ b/src/modules/iam/pages/InternalUsersView.test.ts @@ -5,8 +5,11 @@ import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { + createPlatformManagedUser, getPlatformUserOptions, + listSharedReceivingOverseasWarehouseOptions, listPlatformManagedUsers, + updatePlatformManagedUser, type PlatformManagedUser, } from '@/modules/iam/api/platformIam' import { useAccessPreviewStore } from '@/core/access/accessPreview' @@ -18,6 +21,7 @@ const internalUsersSource = readFileSync(resolve(process.cwd(), 'src/modules/iam vi.mock('@/modules/iam/api/platformIam', () => ({ listPlatformManagedUsers: vi.fn(), getPlatformUserOptions: vi.fn(), + listSharedReceivingOverseasWarehouseOptions: vi.fn(), createPlatformManagedUser: vi.fn(), updatePlatformManagedUser: vi.fn(), changePlatformManagedUserStatus: vi.fn(), @@ -48,6 +52,7 @@ const user: PlatformManagedUser = { memberType: 'INTERNAL', memberNo: 'PLATFORM-30', status: 'ACTIVE', roleIds: ['40'], roleNames: ['运营管理员'], lastLoginAt: '2026-06-01T10:30:00', protectedAccount: false, + overseasWarehouseId: null, } async function mountView() { @@ -101,10 +106,15 @@ describe('InternalUsersView platform internal user management', () => { { id: '10001', name: '华远贸易', status: 'ACTIVE' }, ], roles: [ - { id: '40', tenantId: '0', name: '运营管理员', status: 'ACTIVE' }, - { id: '41', tenantId: '10001', name: '租户管理员', status: 'ACTIVE' }, + { id: '40', tenantId: '0', roleCode: 'PLATFORM_OPERATOR', name: '运营管理员', status: 'ACTIVE' }, + { id: '41', tenantId: '10001', roleCode: 'CUSTOMER_ADMIN', name: '客户管理员', status: 'ACTIVE' }, ], }) + vi.mocked(listSharedReceivingOverseasWarehouseOptions).mockReset().mockResolvedValue([ + { id: '90071992547409931', warehouseCode: 'LAX-01', warehouseName: '洛杉矶收货仓', countryCode: 'US' }, + ]) + vi.mocked(createPlatformManagedUser).mockReset().mockResolvedValue(user) + vi.mocked(updatePlatformManagedUser).mockReset().mockResolvedValue(user) }) it('loads a server page and only renders internal-user fields', async () => { @@ -143,6 +153,73 @@ describe('InternalUsersView platform internal user management', () => { expect(document.body.textContent).toContain('确认添加') }) + it('requires a shared receiving warehouse for WAREHOUSE_PICKER', async () => { + vi.mocked(getPlatformUserOptions).mockResolvedValueOnce({ + tenants: [{ id: '0', name: '平台', status: 'ACTIVE' }], + roles: [ + { id: '40', tenantId: '0', roleCode: 'PLATFORM_OPERATOR', name: '运营管理员', status: 'ACTIVE' }, + { id: '4000005', tenantId: '0', roleCode: 'WAREHOUSE_PICKER', name: '仓库拣货员', status: 'ACTIVE' }, + ], + }) + const wrapper = await mountView() + await wrapper.findAll('button').find((button) => button.text().trim() === '新增用户')?.trigger('click') + await flushPromises() + + expect(document.body.textContent).not.toContain('实际海外收货仓') + const roleSelect = wrapper.findAllComponents({ name: 'ElSelect' }) + .find((component) => component.props('placeholder') === '选择角色') + expect(roleSelect).toBeDefined() + roleSelect?.vm.$emit('update:modelValue', ['4000005']) + await flushPromises() + + expect(listSharedReceivingOverseasWarehouseOptions).toHaveBeenCalledOnce() + expect(document.body.textContent).toContain('实际海外收货仓') + const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }) + .find((component) => component.props('placeholder') === '请选择平台共享海外收货仓') + expect(warehouseSelect).toBeDefined() + }) + + it('uses the stable role code instead of localized role names to activate the warehouse binding', () => { + expect(internalUsersSource).toContain("role.roleCode === 'WAREHOUSE_PICKER'") + expect(internalUsersSource).toContain('overseasWarehouseId: hasWarehousePickerRole.value ? form.overseasWarehouseId : null') + expect(internalUsersSource).not.toContain("role.name === '仓库拣货员'") + }) + + it('preserves the picker warehouse binding when editing a WAREHOUSE_PICKER user', async () => { + const pickerUser: PlatformManagedUser = { + ...user, + roleIds: ['4000005'], + roleNames: ['仓库拣货员'], + overseasWarehouseId: '90071992547409931', + } + vi.mocked(listPlatformManagedUsers).mockResolvedValueOnce({ + records: [pickerUser], page: 1, size: 10, total: 1, pages: 1, + }) + vi.mocked(getPlatformUserOptions).mockResolvedValueOnce({ + tenants: [{ id: '0', name: '平台', status: 'ACTIVE' }], + roles: [{ id: '4000005', tenantId: '0', roleCode: 'WAREHOUSE_PICKER', name: '仓库拣货员', status: 'ACTIVE' }], + }) + vi.mocked(updatePlatformManagedUser).mockResolvedValueOnce(pickerUser) + + const wrapper = await mountView() + const setupState = (wrapper.vm.$ as unknown as { + setupState: { openEdit: (row: PlatformManagedUser) => void } + }).setupState + setupState.openEdit(pickerUser) + await flushPromises() + await wrapper.findAll('button').find((button) => button.text().trim() === '保存修改')?.trigger('click') + await flushPromises() + + expect(updatePlatformManagedUser).toHaveBeenCalledWith('30', { + displayName: '张伟', + phone: '13800138000', + email: 'zhangwei@example.com', + tenantId: '0', + roleIds: ['4000005'], + overseasWarehouseId: '90071992547409931', + }) + }) + it('requests server-side pages and filters instead of slicing locally', async () => { const wrapper = await mountView() await wrapper.find('button[aria-label="第 2 页"]').trigger('click') diff --git a/src/modules/iam/pages/InternalUsersView.vue b/src/modules/iam/pages/InternalUsersView.vue index b19b6604..0a298716 100644 --- a/src/modules/iam/pages/InternalUsersView.vue +++ b/src/modules/iam/pages/InternalUsersView.vue @@ -6,11 +6,13 @@ import { changePlatformManagedUserStatus, createPlatformManagedUser, getPlatformUserOptions, + listSharedReceivingOverseasWarehouseOptions, listPlatformManagedUsers, resetPlatformManagedUserPassword, updatePlatformManagedUser, type PlatformManagedUser, type PlatformUserOptions, + type SharedReceivingOverseasWarehouseOption, } from '@/modules/iam/api/platformIam' import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' @@ -30,6 +32,9 @@ const access = useAccessPreviewStore() const auth = useAuthStore() const users = ref([]) const options = ref({ tenants: [], roles: [] }) +const overseasWarehouses = ref([]) +const overseasWarehousesLoading = ref(false) +const overseasWarehousesError = ref(null) const loading = ref(false) const saving = ref(false) const loadError = ref(null) @@ -38,6 +43,7 @@ const formRef = ref() const editingUser = ref(null) const rowActionId = ref('') let loadRequestId = 0 +let warehouseRequestId = 0 const page = ref(1) const pageSize = ref(10) const total = ref(0) @@ -72,6 +78,7 @@ const emptyForm = () => ({ email: '', tenantId: '0', roleIds: [] as string[], + overseasWarehouseId: '', password: '', confirmPassword: '', status: 'ACTIVE' as 'ACTIVE' | 'DISABLED', @@ -98,6 +105,10 @@ const availableRoles = computed(() => options.value.roles.filter((role) => String(role.tenantId) === '0' && (role.status === 'ACTIVE' || form.roleIds.includes(String(role.id))), )) +const warehousePickerRoleIds = computed(() => new Set( + availableRoles.value.filter((role) => role.roleCode === 'WAREHOUSE_PICKER').map((role) => String(role.id)), +)) +const hasWarehousePickerRole = computed(() => form.roleIds.some((roleId) => warehousePickerRoleIds.value.has(roleId))) const formRules = computed(() => ({ username: editingUser.value ? [] : [ @@ -111,6 +122,9 @@ const formRules = computed(() => ({ ], email: editingUser.value && !canEdit.value ? [] : [{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }], roleIds: editingUser.value && !canEdit.value ? [] : [{ type: 'array', required: true, min: 1, message: '请选择角色', trigger: 'change' }], + overseasWarehouseId: !hasWarehousePickerRole.value || (editingUser.value && !canEdit.value) ? [] : [ + { required: true, message: '请选择实际海外收货仓', trigger: 'change' }, + ], password: editingUser.value && !form.resetPassword ? [] : [ { required: true, message: '请输入密码', trigger: 'blur' }, { pattern: /^(?=.*[A-Za-z])(?=.*\d).{8,64}$/, message: '密码为 8-64 位,且必须包含字母和数字', trigger: 'blur' }, @@ -200,6 +214,7 @@ function openEdit(row: PlatformManagedUser) { email: row.email ?? '', tenantId: '0', roleIds: row.roleIds.map(String), + overseasWarehouseId: row.overseasWarehouseId == null ? '' : String(row.overseasWarehouseId), password: '', confirmPassword: '', status: row.status, @@ -209,6 +224,23 @@ function openEdit(row: PlatformManagedUser) { nextTick(() => formRef.value?.clearValidate()) } +async function loadOverseasWarehouses() { + const requestId = ++warehouseRequestId + overseasWarehousesLoading.value = true + overseasWarehousesError.value = null + try { + const records = await listSharedReceivingOverseasWarehouseOptions() + if (requestId !== warehouseRequestId) return + overseasWarehouses.value = records + } catch (error) { + if (requestId !== warehouseRequestId) return + overseasWarehouses.value = [] + overseasWarehousesError.value = getApiErrorDetails(error, '共享海外收货仓加载失败,请重试') + } finally { + if (requestId === warehouseRequestId) overseasWarehousesLoading.value = false + } +} + function isCurrentUser(row: PlatformManagedUser | null) { if (!row) return false return String(row.userId) === auth.user?.id || String(row.authUserId) === auth.user?.id @@ -231,6 +263,7 @@ async function saveUser() { email: form.email.trim(), tenantId: '0', roleIds: form.roleIds, + overseasWarehouseId: hasWarehousePickerRole.value ? form.overseasWarehouseId : null, password: form.password, status: form.status, }) @@ -244,6 +277,7 @@ async function saveUser() { email: form.email.trim(), tenantId: '0', roleIds: form.roleIds, + overseasWarehouseId: hasWarehousePickerRole.value ? form.overseasWarehouseId : null, }) } if (canChangeStatus.value && form.status !== row.status) { @@ -296,6 +330,14 @@ function applyFilters() { } watch([page, pageSize], () => void loadUsers()) +watch(hasWarehousePickerRole, (selected) => { + if (!selected) { + form.overseasWarehouseId = '' + overseasWarehousesError.value = null + return + } + void loadOverseasWarehouses() +}) onMounted(loadInitialData) @@ -377,6 +419,20 @@ onMounted(loadInitialData) + + + + +

+ {{ overseasWarehousesError.message }} + 重新加载 +

+

仓库将写入登录 Token,海外 PDA 不提供仓库切换。

+
@@ -401,6 +457,9 @@ onMounted(loadInitialData)