实现系统管理后台 V1

This commit is contained in:
andy
2026-07-10 18:32:34 +08:00
parent e76cb80f28
commit aa5783ba61
92 changed files with 9383 additions and 21 deletions

View File

@@ -79,6 +79,18 @@ function createDebugMenu(): AuthMenuResult {
}
}
function createSystemMenu(): AuthMenuResult {
return {
menu_code: 'SYSTEM_SETTINGS',
menu_name: '系统设置',
route_path: '/system',
component_key: 'SystemSettings',
icon_key: 'pi pi-cog',
permission_code: 'SYSTEM_ADMIN_CONSOLE_ACCESS',
sort_order: 40,
}
}
async function mountShell(menus = [createReservationOrdersMenu(), createReservationTasksMenu()]) {
const i18n = createI18n({
legacy: false,
@@ -108,6 +120,11 @@ async function mountShell(menus = [createReservationOrdersMenu(), createReservat
component: { template: '<div />' },
meta: { titleKey: 'debugEml.title' },
},
{
path: '/system',
component: { template: '<div />' },
meta: { titleKey: 'nav.systemSettings' },
},
],
})
const pinia = createPinia()
@@ -158,6 +175,13 @@ describe('ReservationAppShell', () => {
expect(wrapper.text()).toContain('Debug EML')
})
it('shows System Settings only when backend menus include it', async () => {
const wrapper = await mountShell([createReservationOrdersMenu(), createSystemMenu()])
expect(wrapper.find('a[href="/system"]').exists()).toBe(true)
expect(wrapper.text()).toContain('系统设置')
})
it('shows current user and hotel context in the top bar', async () => {
const wrapper = await mountShell()

View File

@@ -53,6 +53,15 @@ describe('reservation router', () => {
expect(router.resolve('/debug/eml-superagent').name).toBe('debug-eml-superagent')
})
it('exposes the System Admin V1 routes', () => {
expect(router.resolve('/system/users').name).toBe('system-users')
expect(router.resolve('/system/roles').name).toBe('system-roles')
expect(router.resolve('/system/menus').name).toBe('system-menus')
expect(router.resolve('/system/hotels').name).toBe('system-hotels')
expect(router.resolve('/system/audits').name).toBe('system-audits')
expect(router.resolve('/system/not-ready').name).toBe('not-found')
})
it('declares permissions for protected P0 routes', () => {
expect(router.resolve('/reservation/orders').meta.permission).toBe('RESERVATION_ORDER_READ')
expect(router.resolve('/reservation/orders/20001').meta.permission).toBe('RESERVATION_ORDER_READ')
@@ -62,6 +71,11 @@ describe('reservation router', () => {
'SOURCE_MESSAGE_ORIGINAL_READ',
)
expect(router.resolve('/debug/eml-superagent').meta.permission).toBe('SYSTEM_DEBUG_EML_RUN')
expect(router.resolve('/system/users').meta.permission).toBe('SYSTEM_USER_MANAGE')
expect(router.resolve('/system/roles').meta.permission).toBe('SYSTEM_ROLE_MANAGE')
expect(router.resolve('/system/menus').meta.permission).toBe('SYSTEM_MENU_MANAGE')
expect(router.resolve('/system/hotels').meta.permission).toBe('HOTEL_MANAGE')
expect(router.resolve('/system/audits').meta.permission).toBe('SYSTEM_ADMIN_CONSOLE_ACCESS')
})
it('redirects anonymous users to login with the protected target as redirect', async () => {
@@ -119,6 +133,19 @@ describe('reservation router', () => {
expect(restored).toBe(true)
})
it('redirects the System Admin entry to the first allowed management page', async () => {
const guard = createAuthGuard(() =>
createAuthStoreForGuard({
hasPermission: (permission: string) =>
permission === 'SYSTEM_ADMIN_CONSOLE_ACCESS' || permission === 'HOTEL_MANAGE',
}),
)
await expect(guard(createRoute('/system', { permission: 'SYSTEM_ADMIN_CONSOLE_ACCESS' }))).resolves.toBe(
'/system/hotels',
)
})
it('redirects the root route to the default page when the backend menu points to root', async () => {
const guard = createAuthGuard(() =>
createAuthStoreForGuard({

View File

@@ -0,0 +1,238 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
assignAdminRolePermissions,
assignAdminUserHotels,
assignAdminUserRoles,
createAdminHotel,
createAdminMenu,
createAdminRole,
createAdminUser,
fetchAdminAudits,
fetchAdminHotels,
fetchAdminMenus,
fetchAdminPermissions,
fetchAdminRoleDetail,
fetchAdminRoles,
fetchAdminUserDetail,
fetchAdminUsers,
resetAdminUserPassword,
updateAdminHotel,
updateAdminHotelStatus,
updateAdminMenu,
updateAdminRole,
updateAdminUser,
} from '@/services/systemAdminService'
const jsonHeaders = {
headers: {
get: (name: string) => (name.toLowerCase() === 'content-type' ? 'application/json' : null),
},
}
function mockJsonResponse(payload: unknown): Response {
return {
ok: true,
status: 200,
json: async () => payload,
text: async () => JSON.stringify(payload),
...jsonHeaders,
} as Response
}
describe('systemAdminService', () => {
beforeEach(() => {
vi.restoreAllMocks()
sessionStorage.clear()
})
it('fetches admin users with filters', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
mockJsonResponse({
items: [],
page: {
page_num: 2,
page_size: 10,
total: 0,
},
}),
)
await fetchAdminUsers({
keyword: 'admin',
user_status: 'ACTIVE',
page_num: 2,
page_size: 10,
})
expect(fetchMock).toHaveBeenCalledWith(
'/api/admin/users?keyword=admin&user_status=ACTIVE&page_num=2&page_size=10',
expect.objectContaining({ method: 'GET' }),
)
})
it('fetches role, menu, hotel lists and details from admin endpoints', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockJsonResponse({ items: [], page: {} }))
await fetchAdminRoles({ role_status: 'DISABLED' })
await fetchAdminMenus({ menu_status: 'ACTIVE' })
await fetchAdminHotels({ hotel_status: 'ACTIVE' })
await fetchAdminUserDetail('10001')
await fetchAdminRoleDetail('20001')
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/admin/roles?role_status=DISABLED',
expect.objectContaining({ method: 'GET' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/admin/menus?menu_status=ACTIVE',
expect.objectContaining({ method: 'GET' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/admin/hotels?hotel_status=ACTIVE',
expect.objectContaining({ method: 'GET' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'/api/admin/users/10001',
expect.objectContaining({ method: 'GET' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
5,
'/api/admin/roles/20001',
expect.objectContaining({ method: 'GET' }),
)
})
it('fetches the readonly permission list', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
mockJsonResponse([
{
id: '1',
permission_code: 'SYSTEM_USER_MANAGE',
permission_name: '用户管理',
permission_group: 'SYSTEM',
permission_status: 'ACTIVE',
},
]),
)
const result = await fetchAdminPermissions()
expect(fetchMock).toHaveBeenCalledWith('/api/admin/permissions', expect.objectContaining({ method: 'GET' }))
expect(result[0]?.permission_code).toBe('SYSTEM_USER_MANAGE')
})
it('sends admin user write operations to backend endpoints', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockJsonResponse({ id: '10001' }))
await createAdminUser({
username: 'operator',
initial_password: 'User@123456',
display_name: 'Operator',
user_status: 'ACTIVE',
role_ids: ['20001'],
hotel_ids: ['HOTEL-TEST'],
default_hotel_id: 'HOTEL-TEST',
})
await updateAdminUser('10001', {
display_name: 'Operator A',
user_status: 'DISABLED',
})
await assignAdminUserRoles('10001', {
role_ids: ['20002'],
})
await assignAdminUserHotels('10001', {
hotel_ids: ['HOTEL-TEST'],
default_hotel_id: 'HOTEL-TEST',
})
await resetAdminUserPassword('10001')
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/admin/users',
expect.objectContaining({ method: 'POST' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/admin/users/10001',
expect.objectContaining({ method: 'PUT' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/admin/users/10001/roles',
expect.objectContaining({ method: 'PUT' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'/api/admin/users/10001/hotels',
expect.objectContaining({ method: 'PUT' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
5,
'/api/admin/users/10001/password-reset',
expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }),
)
})
it('sends role, menu, hotel writes and audit queries to admin endpoints', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockJsonResponse({ id: '1', items: [], page: {} }))
await createAdminRole({ role_code: 'CUSTOM_ROLE', role_name: 'Custom role', role_status: 'ACTIVE' })
await updateAdminRole('20001', { role_name: 'Custom role A', role_status: 'DISABLED' })
await assignAdminRolePermissions('20001', { permission_ids: ['30001'] })
await createAdminMenu({
menu_code: 'CUSTOM_MENU',
menu_name: 'Custom menu',
menu_type: 'PAGE',
visible: false,
menu_status: 'DISABLED',
})
await updateAdminMenu('40001', {
menu_name: 'Custom menu A',
menu_type: 'PAGE',
visible: true,
menu_status: 'ACTIVE',
})
await createAdminHotel({
hotel_id: 'HOTEL-BKK',
hotel_name: 'Bangkok Hotel',
time_zone: 'Asia/Bangkok',
})
await updateAdminHotel('HOTEL-BKK', {
hotel_name: 'Bangkok Hotel A',
time_zone: 'Asia/Bangkok',
})
await updateAdminHotelStatus('HOTEL-BKK', { hotel_status: 'ACTIVE' })
await fetchAdminAudits({ target_type: 'PLATFORM_USER', page_num: 1, page_size: 20 })
expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/admin/roles', expect.objectContaining({ method: 'POST' }))
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/admin/roles/20001', expect.objectContaining({ method: 'PUT' }))
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/admin/roles/20001/permissions',
expect.objectContaining({ method: 'PUT' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(4, '/api/admin/menus', expect.objectContaining({ method: 'POST' }))
expect(fetchMock).toHaveBeenNthCalledWith(5, '/api/admin/menus/40001', expect.objectContaining({ method: 'PUT' }))
expect(fetchMock).toHaveBeenNthCalledWith(6, '/api/admin/hotels', expect.objectContaining({ method: 'POST' }))
expect(fetchMock).toHaveBeenNthCalledWith(
7,
'/api/admin/hotels/HOTEL-BKK',
expect.objectContaining({ method: 'PUT' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
8,
'/api/admin/hotels/HOTEL-BKK/status',
expect.objectContaining({ method: 'PUT' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
9,
'/api/admin/audits?target_type=PLATFORM_USER&page_num=1&page_size=20',
expect.objectContaining({ method: 'GET' }),
)
})
})