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
|
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 = {
|
export const licenseApi = {
|
||||||
getAppLicense(appKey: string) {
|
getAppLicense(appKey: string) {
|
||||||
return licenseClient.get<{ data: { license: AppLicense; devices: LicenseDevice[] } }>(
|
return licenseClient.get<{ data: { license: AppLicense; devices: LicenseDevice[] } }>(
|
||||||
@ -118,4 +130,10 @@ export const licenseApi = {
|
|||||||
reactivateDevice(id: string) {
|
reactivateDevice(id: string) {
|
||||||
return licenseClient.put<{ data: null }>(`/api/license/admin/devices/${encodeURIComponent(id)}/reactivate`)
|
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[]
|
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 = {
|
export const pushAdminApi = {
|
||||||
getUserStatus(appKey: string, userId: string) {
|
getUserStatus(appKey: string, userId: string) {
|
||||||
return client.get<{ data: UserPushStatus }>('/push/admin/user-status', {
|
return client.get<{ data: UserPushStatus }>('/push/admin/user-status', {
|
||||||
@ -77,4 +89,10 @@ export const pushAdminApi = {
|
|||||||
payload: payload ?? null,
|
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>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</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-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
@ -218,9 +280,11 @@ import { appApi, type App } from '@/api/app'
|
|||||||
import { updateAdminApi, type OperationLog as UpdateOperationLog } from '@/api/update'
|
import { updateAdminApi, type OperationLog as UpdateOperationLog } from '@/api/update'
|
||||||
import { operationLogApi, type TenantOperationLog } from '@/api/operationLog'
|
import { operationLogApi, type TenantOperationLog } from '@/api/operationLog'
|
||||||
import { imAdminApi, type OperationLog as ImOperationLog } from '@/api/im'
|
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'
|
import { formatTime } from '@/utils/date'
|
||||||
|
|
||||||
const activeSource = ref<'TENANT' | 'IM' | 'UPDATE'>('TENANT')
|
const activeSource = ref<'TENANT' | 'IM' | 'UPDATE' | 'PUSH' | 'LICENSE'>('TENANT')
|
||||||
|
|
||||||
// ── 租户平台 state ───────────────────────────────────────────────────────────
|
// ── 租户平台 state ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -248,6 +312,24 @@ const updateLimit = ref(100)
|
|||||||
const apps = ref<App[]>([])
|
const apps = ref<App[]>([])
|
||||||
const updateAppKey = ref('')
|
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 () => {
|
onMounted(async () => {
|
||||||
@ -255,11 +337,13 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(activeSource, async (value) => {
|
watch(activeSource, async (value) => {
|
||||||
if ((value === 'UPDATE' || value === 'IM') && !apps.value.length) {
|
if (['UPDATE', 'IM', 'PUSH', 'LICENSE'].includes(value) && !apps.value.length) {
|
||||||
await loadApps()
|
await loadApps()
|
||||||
}
|
}
|
||||||
if (value === 'UPDATE') await loadUpdateLogs()
|
if (value === 'UPDATE') await loadUpdateLogs()
|
||||||
if (value === 'IM' && imAppKey.value) await loadImLogs()
|
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
|
apps.value = res.data.data
|
||||||
if (!updateAppKey.value && apps.value.length) updateAppKey.value = apps.value[0].appKey
|
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 (!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 */ }
|
} 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) {
|
function handleTenantPageChange(nextPage: number) {
|
||||||
@ -340,6 +452,26 @@ function handleUpdateAppChange() {
|
|||||||
loadUpdateLogs()
|
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 {
|
function moduleLabel(moduleType: string): string {
|
||||||
@ -468,6 +600,16 @@ function actionLabel(action: string): string {
|
|||||||
STORE_SUBMIT_STORE_STAGE: '单市场提交进度',
|
STORE_SUBMIT_STORE_STAGE: '单市场提交进度',
|
||||||
STORE_SUBMIT_ALREADY_LIVE: '市场已上架',
|
STORE_SUBMIT_ALREADY_LIVE: '市场已上架',
|
||||||
STORE_WITHDRAW_BEFORE_RESUBMIT: '撤回后重新提交',
|
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
|
return map[action] ?? action
|
||||||
}
|
}
|
||||||
@ -513,6 +655,11 @@ function resourceTypeLabel(resourceType: string): string {
|
|||||||
GRAY_MEMBER: '灰度成员',
|
GRAY_MEMBER: '灰度成员',
|
||||||
STORE_SUBMIT: '市场提交',
|
STORE_SUBMIT: '市场提交',
|
||||||
SERVICE: '服务配置',
|
SERVICE: '服务配置',
|
||||||
|
// push-service
|
||||||
|
PUSH: '推送消息',
|
||||||
|
// license-service
|
||||||
|
LICENSE: '授权许可',
|
||||||
|
DEVICE: '设备',
|
||||||
}[resourceType] ?? resourceType
|
}[resourceType] ?? resourceType
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,6 +680,26 @@ function generateImSummary(row: ImOperationLog): string {
|
|||||||
return `${act}(${res})`
|
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 {
|
function updateSummaryText(row: UpdateOperationLog): string {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户