feat(logs): 添加操作日志功能支持推送和授权模块

- 在JwtAuthFilter中设置认证详情到claims
- 为license-service添加LicenseOperationLog相关实体、仓库和服务
- 为push-service添加PushOperationLog相关实体、仓库和服务
- 在LicenseAdminController中注入并使用操作日志记录授权变更
- 在PushManagementController中注入并使用操作日志记录推送操作
- 更新OperationLogService以支持从JWT claims获取用户信息
- 扩展OperationLogService支持推送和授权操作日志查询
- 在前端OperationLogView中添加推送和授权日志选项卡
- 添加LicenseOperationLog和PushOperationLog接口定义
- 实现推送和授权日志的数据加载和分页功能
- 添加操作类型和资源类型的标签映射支持
这个提交包含在:
XuqmGroup 2026-05-27 13:36:16 +08:00
父节点 908916fefd
当前提交 5b2a89e964
共有 3 个文件被更改,包括 205 次插入2 次删除

查看文件

@ -97,6 +97,18 @@ export interface LicenseDevice {
createdAt: string
}
export interface LicenseOperationLog {
id: string
appKey: string
operator: string
action: string
resourceType: string
resourceId?: string | null
summary?: string | null
detailJson?: string | null
createdAt: string
}
export const licenseApi = {
getAppLicense(appKey: string) {
return licenseClient.get<{ data: { license: AppLicense; devices: LicenseDevice[] } }>(
@ -118,4 +130,10 @@ export const licenseApi = {
reactivateDevice(id: string) {
return licenseClient.put<{ data: null }>(`/api/license/admin/devices/${encodeURIComponent(id)}/reactivate`)
},
getOperationLogs(appKey: string, page = 0, size = 20) {
return licenseClient.get<{ data: { content: LicenseOperationLog[]; total: number; totalPages: number } }>(
`/api/license/admin/operation-logs`, { params: { appKey, page, size } },
)
},
}

查看文件

@ -55,6 +55,18 @@ export interface TestPushResult {
targets: DeviceInfo[]
}
export interface PushOperationLog {
id: string
appKey: string
operator: string
action: string
resourceType: string
resourceId?: string | null
summary?: string | null
detail?: string | null
createdAt: string
}
export const pushAdminApi = {
getUserStatus(appKey: string, userId: string) {
return client.get<{ data: UserPushStatus }>('/push/admin/user-status', {
@ -77,4 +89,10 @@ export const pushAdminApi = {
payload: payload ?? null,
})
},
getOperationLogs(appKey: string, page = 0, size = 20) {
return client.get<{ data: { content: PushOperationLog[]; total: number; totalPages: number } }>(
'/push/admin/operation-logs', { params: { appKey, page, size } },
)
},
}

查看文件

@ -207,6 +207,68 @@
</el-table>
</div>
</el-tab-pane>
<!-- 消息推送 -->
<el-tab-pane label="消息推送" name="PUSH">
<div class="toolbar responsive-toolbar">
<el-select v-model="pushAppKey" placeholder="选择应用" style="width: 320px" filterable @change="handlePushAppChange">
<el-option v-for="app in apps" :key="app.appKey" :label="`${app.name} · ${app.packageName}`" :value="app.appKey" />
</el-select>
<el-button :loading="pushLoading" @click="loadPushLogs">刷新</el-button>
</div>
<div class="table-wrap">
<el-table :data="pushLogs" v-loading="pushLoading" border stripe>
<el-table-column prop="createdAt" label="时间" width="170">
<template #default="{ row }"><span class="time-text">{{ formatTime(row.createdAt) }}</span></template>
</el-table-column>
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-tag size="small" :type="actionTagType(row.action)" effect="dark">{{ actionLabel(row.action) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作详情" min-width="360">
<template #default="{ row }"><span class="summary-text">{{ pushSummaryText(row) }}</span></template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="120" />
</el-table>
</div>
<el-pagination style="margin-top: 16px" layout="total, prev, pager, next"
:total="pushTotal" :page-size="pushPageSize" :current-page="pushPage + 1"
@current-change="handlePushPageChange" />
</el-tab-pane>
<!-- 设备授权 -->
<el-tab-pane label="设备授权" name="LICENSE">
<div class="toolbar responsive-toolbar">
<el-select v-model="licenseAppKey" placeholder="选择应用" style="width: 320px" filterable @change="handleLicenseAppChange">
<el-option v-for="app in apps" :key="app.appKey" :label="`${app.name} · ${app.packageName}`" :value="app.appKey" />
</el-select>
<el-button :loading="licenseLoading" @click="loadLicenseLogs">刷新</el-button>
</div>
<div class="table-wrap">
<el-table :data="licenseLogs" v-loading="licenseLoading" border stripe>
<el-table-column prop="createdAt" label="时间" width="170">
<template #default="{ row }"><span class="time-text">{{ formatTime(row.createdAt) }}</span></template>
</el-table-column>
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-tag size="small" :type="actionTagType(row.action)" effect="dark">{{ actionLabel(row.action) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作详情" min-width="360">
<template #default="{ row }"><span class="summary-text">{{ licenseSummaryText(row) }}</span></template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="120" />
</el-table>
</div>
<el-pagination style="margin-top: 16px" layout="total, prev, pager, next"
:total="licenseTotal" :page-size="licensePageSize" :current-page="licensePage + 1"
@current-change="handleLicensePageChange" />
</el-tab-pane>
</el-tabs>
</el-card>
</div>
@ -218,9 +280,11 @@ import { appApi, type App } from '@/api/app'
import { updateAdminApi, type OperationLog as UpdateOperationLog } from '@/api/update'
import { operationLogApi, type TenantOperationLog } from '@/api/operationLog'
import { imAdminApi, type OperationLog as ImOperationLog } from '@/api/im'
import { pushAdminApi, type PushOperationLog } from '@/api/push'
import { licenseApi, type LicenseOperationLog } from '@/api/license'
import { formatTime } from '@/utils/date'
const activeSource = ref<'TENANT' | 'IM' | 'UPDATE'>('TENANT')
const activeSource = ref<'TENANT' | 'IM' | 'UPDATE' | 'PUSH' | 'LICENSE'>('TENANT')
// state
@ -248,6 +312,24 @@ const updateLimit = ref(100)
const apps = ref<App[]>([])
const updateAppKey = ref('')
// state
const pushLoading = ref(false)
const pushLogs = ref<PushOperationLog[]>([])
const pushTotal = ref(0)
const pushPage = ref(0)
const pushPageSize = ref(20)
const pushAppKey = ref('')
// state
const licenseLoading = ref(false)
const licenseLogs = ref<LicenseOperationLog[]>([])
const licenseTotal = ref(0)
const licensePage = ref(0)
const licensePageSize = ref(20)
const licenseAppKey = ref('')
//
onMounted(async () => {
@ -255,11 +337,13 @@ onMounted(async () => {
})
watch(activeSource, async (value) => {
if ((value === 'UPDATE' || value === 'IM') && !apps.value.length) {
if (['UPDATE', 'IM', 'PUSH', 'LICENSE'].includes(value) && !apps.value.length) {
await loadApps()
}
if (value === 'UPDATE') await loadUpdateLogs()
if (value === 'IM' && imAppKey.value) await loadImLogs()
if (value === 'PUSH' && pushAppKey.value) await loadPushLogs()
if (value === 'LICENSE' && licenseAppKey.value) await loadLicenseLogs()
})
//
@ -271,6 +355,8 @@ async function loadApps() {
apps.value = res.data.data
if (!updateAppKey.value && apps.value.length) updateAppKey.value = apps.value[0].appKey
if (!imAppKey.value && apps.value.length) imAppKey.value = apps.value[0].appKey
if (!pushAppKey.value && apps.value.length) pushAppKey.value = apps.value[0].appKey
if (!licenseAppKey.value && apps.value.length) licenseAppKey.value = apps.value[0].appKey
} catch { /* ignore */ }
}
@ -314,6 +400,32 @@ async function loadUpdateLogs() {
}
}
async function loadPushLogs() {
if (!pushAppKey.value) { pushLogs.value = []; return }
pushLoading.value = true
try {
const res = await pushAdminApi.getOperationLogs(pushAppKey.value, pushPage.value, pushPageSize.value)
const data = res.data.data
pushLogs.value = data.content ?? []
pushTotal.value = data.total ?? 0
} finally {
pushLoading.value = false
}
}
async function loadLicenseLogs() {
if (!licenseAppKey.value) { licenseLogs.value = []; return }
licenseLoading.value = true
try {
const res = await licenseApi.getOperationLogs(licenseAppKey.value, licensePage.value, licensePageSize.value)
const data = res.data.data
licenseLogs.value = data.content ?? []
licenseTotal.value = data.total ?? 0
} finally {
licenseLoading.value = false
}
}
//
function handleTenantPageChange(nextPage: number) {
@ -340,6 +452,26 @@ function handleUpdateAppChange() {
loadUpdateLogs()
}
function handlePushAppChange() {
pushPage.value = 0
loadPushLogs()
}
function handlePushPageChange(nextPage: number) {
pushPage.value = nextPage - 1
loadPushLogs()
}
function handleLicenseAppChange() {
licensePage.value = 0
loadLicenseLogs()
}
function handleLicensePageChange(nextPage: number) {
licensePage.value = nextPage - 1
loadLicenseLogs()
}
//
function moduleLabel(moduleType: string): string {
@ -468,6 +600,16 @@ function actionLabel(action: string): string {
STORE_SUBMIT_STORE_STAGE: '单市场提交进度',
STORE_SUBMIT_ALREADY_LIVE: '市场已上架',
STORE_WITHDRAW_BEFORE_RESUBMIT: '撤回后重新提交',
// push-service:
UPDATE_USER: '编辑用户信息',
UPDATE_USER_STATUS: '变更用户状态',
DELETE_USER: '删除用户',
IMPORT_USER: '导入用户',
TEST_PUSH: '发送测试推送',
// license-service:
UPDATE_LICENSE: '更新授权配置',
REVOKE_DEVICE: '吊销设备授权',
REACTIVATE_DEVICE: '重新激活设备',
}
return map[action] ?? action
}
@ -513,6 +655,11 @@ function resourceTypeLabel(resourceType: string): string {
GRAY_MEMBER: '灰度成员',
STORE_SUBMIT: '市场提交',
SERVICE: '服务配置',
// push-service
PUSH: '推送消息',
// license-service
LICENSE: '授权许可',
DEVICE: '设备',
}[resourceType] ?? resourceType
}
@ -533,6 +680,26 @@ function generateImSummary(row: ImOperationLog): string {
return `${act}${res}`
}
//
function pushSummaryText(row: PushOperationLog): string {
if (row.summary) return row.summary
const act = actionLabel(row.action)
const res = resourceTypeLabel(row.resourceType)
if (row.resourceId) return `${act}${res} ${row.resourceId}`
return `${act}${res}`
}
//
function licenseSummaryText(row: LicenseOperationLog): string {
if (row.summary) return row.summary
const act = actionLabel(row.action)
const res = resourceTypeLabel(row.resourceType)
if (row.resourceId) return `${act}${res} ${row.resourceId}`
return `${act}${res}`
}
//
function updateSummaryText(row: UpdateOperationLog): string {