feat(logs): 添加操作日志功能支持推送和授权模块
- 在JwtAuthFilter中设置认证详情到claims - 为license-service添加LicenseOperationLog相关实体、仓库和服务 - 为push-service添加PushOperationLog相关实体、仓库和服务 - 在LicenseAdminController中注入并使用操作日志记录授权变更 - 在PushManagementController中注入并使用操作日志记录推送操作 - 更新OperationLogService以支持从JWT claims获取用户信息 - 扩展OperationLogService支持推送和授权操作日志查询 - 在前端OperationLogView中添加推送和授权日志选项卡 - 添加LicenseOperationLog和PushOperationLog接口定义 - 实现推送和授权日志的数据加载和分页功能 - 添加操作类型和资源类型的标签映射支持
这个提交包含在:
父节点
908916fefd
当前提交
5b2a89e964
@ -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 {
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户