diff --git a/frontend/src/pages/logs/__tests__/operations.spec.ts b/frontend/src/pages/logs/__tests__/operations.spec.ts
index c4110b4..398b2d9 100644
--- a/frontend/src/pages/logs/__tests__/operations.spec.ts
+++ b/frontend/src/pages/logs/__tests__/operations.spec.ts
@@ -83,6 +83,26 @@ const mockUsers = {
per_page: 200,
}
+const mockRoles = {
+ items: [
+ { id: 3, name: 'developer' },
+ { id: 4, name: 'administrator' },
+ ],
+ total: 2,
+ page: 1,
+ per_page: 200,
+}
+
+const mockRouteGroups = {
+ items: [
+ { id: 10, name: 'default' },
+ { id: 11, name: 'api-management' },
+ ],
+ total: 2,
+ page: 1,
+ per_page: 200,
+}
+
// ─── Store Tests ───────────────────────────────────────
describe('useOperationLogStore', () => {
@@ -192,27 +212,59 @@ describe('useOperationLogStore', () => {
})
})
- describe('userMap', () => {
- it('S6: builds userMap from loaded users', async () => {
+ describe('lookup maps', () => {
+ it('S6: builds all maps from loaded lookups', async () => {
const { api } = await import('@/utils/request')
- vi.mocked(api.get).mockResolvedValueOnce(mockUsers)
+ vi.mocked(api.get)
+ .mockResolvedValueOnce(mockUsers)
+ .mockResolvedValueOnce(mockRoles)
+ .mockResolvedValueOnce(mockRouteGroups)
const store = useOperationLogStore()
await store.loadLookups()
expect(store.userMap.get(1)).toBe('admin')
expect(store.userMap.get(2)).toBe('editor')
+ expect(store.roleMap.get(3)).toBe('developer')
+ expect(store.roleMap.get(4)).toBe('administrator')
+ expect(store.routeGroupMap.get(10)).toBe('default')
+ expect(store.routeGroupMap.get(11)).toBe('api-management')
})
- it('S7: handles loadLookups failure gracefully', async () => {
+ it('S6a: userMap handles string id from API via Number() defense', () => {
+ const store = useOperationLogStore()
+ store.users = [{ id: '1' as any, username: 'admin' }] as any
+ expect(store.userMap.get(1)).toBe('admin')
+ })
+
+ it('S7: handles all lookups failure gracefully', async () => {
const { api } = await import('@/utils/request')
- vi.mocked(api.get).mockRejectedValueOnce(new Error('加载失败'))
+ vi.mocked(api.get)
+ .mockRejectedValueOnce(new Error('用户加载失败'))
+ .mockRejectedValueOnce(new Error('角色加载失败'))
+ .mockRejectedValueOnce(new Error('路由组加载失败'))
const store = useOperationLogStore()
await store.loadLookups()
- expect(store.users).toEqual([])
expect(store.userMap.size).toBe(0)
+ expect(store.roleMap.size).toBe(0)
+ expect(store.routeGroupMap.size).toBe(0)
+ })
+
+ it('S7a: partial lookup failure does not affect other maps', async () => {
+ const { api } = await import('@/utils/request')
+ vi.mocked(api.get)
+ .mockResolvedValueOnce(mockUsers)
+ .mockRejectedValueOnce(new Error('角色加载失败'))
+ .mockResolvedValueOnce(mockRouteGroups)
+
+ const store = useOperationLogStore()
+ await store.loadLookups()
+
+ expect(store.userMap.get(1)).toBe('admin')
+ expect(store.roleMap.size).toBe(0)
+ expect(store.routeGroupMap.get(10)).toBe('default')
})
})
})
@@ -237,6 +289,8 @@ describe('OperationLogPage', () => {
const { api } = await import('@/utils/request')
vi.mocked(api.get).mockImplementation((url: string) => {
if (url === '/api/v1/users') return Promise.resolve(mockUsers)
+ if (url === '/api/v1/roles') return Promise.resolve(mockRoles)
+ if (url === '/api/v1/route-groups') return Promise.resolve(mockRouteGroups)
if (url === '/api/v1/logs/operations') {
return Promise.resolve({
items: mockLogs,
@@ -293,11 +347,14 @@ describe('OperationLogPage', () => {
expect(tagTexts).toContain('order.delete')
})
- it('P5: displays target as type + id', async () => {
+ it('P5: displays target with name for small datasets, #id for large', async () => {
await mountPage()
const html = wrapper.html()
+ // user id=5 not in mockUsers, so fallback to #id
expect(html).toContain('用户 #5')
- expect(html).toContain('角色 #3')
+ // role id=3 is in mockRoles as 'developer'
+ expect(html).toContain('角色 developer')
+ // order is large dataset, always #id
expect(html).toContain('订单 #100')
})
@@ -358,6 +415,35 @@ describe('OperationLogPage', () => {
expect(api.get).toHaveBeenCalled()
})
+ it('P9a: renders operator username instead of #id', async () => {
+ await mountPage()
+ const html = wrapper.html()
+ expect(html).toContain('admin')
+ expect(html).not.toMatch(/#1(?![\d])/)
+ })
+
+ it('P9b: renders role name in target column', async () => {
+ await mountPage()
+ const html = wrapper.html()
+ // mockLogs[1] has target_type='role', target_id=3, which maps to 'developer'
+ expect(html).toContain('角色 developer')
+ expect(html).not.toContain('角色 #3')
+ })
+
+ it('P9c: preserves #id for large dataset targets', async () => {
+ await mountPage()
+ const html = wrapper.html()
+ // mockLogs[2] has target_type='order', target_id=100 — large dataset, keep #id
+ expect(html).toContain('订单 #100')
+ })
+
+ it('P9d: calls all three lookup APIs on mount', async () => {
+ const { api } = await mountPage()
+ expect(api.get).toHaveBeenCalledWith('/api/v1/users', { per_page: 200 })
+ expect(api.get).toHaveBeenCalledWith('/api/v1/roles', { per_page: 200 })
+ expect(api.get).toHaveBeenCalledWith('/api/v1/route-groups', { per_page: 200 })
+ })
+
it('P9: view button opens drawer and loads detail', async () => {
const { api } = await mountPage()
diff --git a/frontend/src/pages/logs/__tests__/requests.spec.ts b/frontend/src/pages/logs/__tests__/requests.spec.ts
index 94d6dce..5cbe16a 100644
--- a/frontend/src/pages/logs/__tests__/requests.spec.ts
+++ b/frontend/src/pages/logs/__tests__/requests.spec.ts
@@ -214,6 +214,19 @@ describe('useRequestLogStore', () => {
expect(store.userMap.get(2)).toBe('editor')
})
+ it('S7a: userMap handles string id from API via Number() defense', async () => {
+ const store = useRequestLogStore()
+ // Simulate API returning string-typed ids (the actual bug scenario)
+ store.users = [
+ { id: '1' as any, username: 'admin' },
+ { id: '2' as any, username: 'editor' },
+ ] as any
+
+ // Number() defense in store ensures Map keys are numbers
+ expect(store.userMap.get(1)).toBe('admin')
+ expect(store.userMap.get(2)).toBe('editor')
+ })
+
it('S7: handles loadLookups failure gracefully', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.get).mockRejectedValueOnce(new Error('加载失败'))
@@ -369,6 +382,25 @@ describe('RequestLogPage', () => {
expect(api.get).toHaveBeenCalled()
})
+ it('P9a: renders username instead of #id in user column', async () => {
+ await mountPage()
+ const html = wrapper.html()
+ // user_id=1 should render as 'admin', not '#1'
+ expect(html).toContain('admin')
+ expect(html).not.toMatch(/#1(?![\d])/)
+ // user_id=2 should render as 'editor', not '#2'
+ expect(html).toContain('editor')
+ })
+
+ it('P9b: renders dash for null user_id', async () => {
+ await mountPage()
+ // mockLogs[1] has user_id: null, should render '-'
+ // The table should contain '-' for the null user_id row
+ const cells = wrapper.findAll('td')
+ const userCells = cells.filter((c) => c.text() === '-')
+ expect(userCells.length).toBeGreaterThan(0)
+ })
+
it('P9: view button opens drawer and loads detail', async () => {
const { api } = await mountPage()
diff --git a/frontend/src/pages/logs/operations.vue b/frontend/src/pages/logs/operations.vue
index c6a9cef..52b938e 100644
--- a/frontend/src/pages/logs/operations.vue
+++ b/frontend/src/pages/logs/operations.vue
@@ -63,8 +63,8 @@ const createdAtRange = computed({
},
})
-onMounted(() => {
- store.loadLookups()
+onMounted(async () => {
+ await store.loadLookups()
store.fetchLogs()
})
@@ -89,8 +89,20 @@ function formatTime(time: string | null) {
return time.replace('T', ' ').substring(0, 16)
}
-function formatTarget(type: string, id: number) {
+function formatTarget(type: string, id: number): string {
const label = targetTypeLabels[type] || type
+ if (type === 'user') {
+ const name = store.userMap.get(Number(id))
+ return name ? `${label} ${name}` : `${label} #${id}`
+ }
+ if (type === 'role') {
+ const name = store.roleMap.get(Number(id))
+ return name ? `${label} ${name}` : `${label} #${id}`
+ }
+ if (type === 'route_group') {
+ const name = store.routeGroupMap.get(Number(id))
+ return name ? `${label} ${name}` : `${label} #${id}`
+ }
return `${label} #${id}`
}
@@ -198,7 +210,7 @@ async function handleViewDetail(record: { id: number }) {
{{
record.user_id
- ? (store.userMap.get(record.user_id) || `#${record.user_id}`)
+ ? (store.userMap.get(Number(record.user_id)) || `#${record.user_id}`)
: '-'
}}
@@ -255,7 +267,7 @@ async function handleViewDetail(record: { id: number }) {
{{
detail.user_id
- ? (store.userMap.get(detail.user_id) || `#${detail.user_id}`)
+ ? (store.userMap.get(Number(detail.user_id)) || `#${detail.user_id}`)
: '-'
}}
diff --git a/frontend/src/pages/logs/requests.vue b/frontend/src/pages/logs/requests.vue
index e3767e7..364c157 100644
--- a/frontend/src/pages/logs/requests.vue
+++ b/frontend/src/pages/logs/requests.vue
@@ -72,8 +72,8 @@ const createdAtRange = computed({
},
})
-onMounted(() => {
- store.loadLookups()
+onMounted(async () => {
+ await store.loadLookups()
store.fetchLogs()
})
@@ -214,7 +214,7 @@ async function handleViewDetail(record: { id: number }) {
- {{ record.user_id ? (store.userMap.get(record.user_id) || `#${record.user_id}`) : '-' }}
+ {{ record.user_id ? (store.userMap.get(Number(record.user_id)) || `#${record.user_id}`) : '-' }}
@@ -274,7 +274,7 @@ async function handleViewDetail(record: { id: number }) {
{{
detail.user_id
- ? (store.userMap.get(detail.user_id) || `#${detail.user_id}`)
+ ? (store.userMap.get(Number(detail.user_id)) || `#${detail.user_id}`)
: '-'
}}
diff --git a/frontend/src/stores/operation-log.ts b/frontend/src/stores/operation-log.ts
index 8098c89..e5b8c06 100644
--- a/frontend/src/stores/operation-log.ts
+++ b/frontend/src/stores/operation-log.ts
@@ -12,6 +12,16 @@ interface UserLookup {
username: string
}
+interface RoleLookup {
+ id: number
+ name: string
+}
+
+interface RouteGroupLookup {
+ id: number
+ name: string
+}
+
export const useOperationLogStore = defineStore('operationLog', () => {
const logs = ref([])
const loading = ref(false)
@@ -28,19 +38,28 @@ export const useOperationLogStore = defineStore('operationLog', () => {
})
const users = ref([])
+ const roles = ref([])
+ const routeGroups = ref([])
+
const userMap = computed(
- () => new Map(users.value.map((u) => [u.id, u.username])),
+ () => new Map(users.value.map((u) => [Number(u.id), u.username])),
+ )
+ const roleMap = computed(
+ () => new Map(roles.value.map((r) => [Number(r.id), r.name])),
+ )
+ const routeGroupMap = computed(
+ () => new Map(routeGroups.value.map((rg) => [Number(rg.id), rg.name])),
)
async function loadLookups() {
- try {
- const data = await api.get>('/api/v1/users', {
- per_page: 200,
- })
- users.value = data.items
- } catch (err: unknown) {
- console.warn('加载用户列表失败', err)
- }
+ const [userData, roleData, rgData] = await Promise.allSettled([
+ api.get>('/api/v1/users', { per_page: 200 }),
+ api.get>('/api/v1/roles', { per_page: 200 }),
+ api.get>('/api/v1/route-groups', { per_page: 200 }),
+ ])
+ if (userData.status === 'fulfilled') users.value = userData.value.items
+ if (roleData.status === 'fulfilled') roles.value = roleData.value.items
+ if (rgData.status === 'fulfilled') routeGroups.value = rgData.value.items
}
async function fetchLogs() {
@@ -86,6 +105,8 @@ export const useOperationLogStore = defineStore('operationLog', () => {
filters,
users,
userMap,
+ roleMap,
+ routeGroupMap,
loadLookups,
fetchLogs,
resetFilters,
diff --git a/frontend/src/stores/request-log.ts b/frontend/src/stores/request-log.ts
index 3a62b2b..e1db2a7 100644
--- a/frontend/src/stores/request-log.ts
+++ b/frontend/src/stores/request-log.ts
@@ -30,7 +30,7 @@ export const useRequestLogStore = defineStore('requestLog', () => {
const users = ref([])
const userMap = computed(
- () => new Map(users.value.map((u) => [u.id, u.username])),
+ () => new Map(users.value.map((u) => [Number(u.id), u.username])),
)
async function loadLookups() {