接通对比报表删除和正式操作日志查询
保留列表确认流程,按后端权限删除全部版本,失败保留记录且成功刷新。操作日志页替换静态数据,补充隔离浏览器验收覆盖事务、权限、来源独立及日志保留。
| | |
| | | # 对比报表页面 |
| | | |
| | | ## 当前实现(数据分析 Ticket04) |
| | | ## 当前实现(数据分析 Ticket06 待用户验收) |
| | | |
| | | - 正式数据来自 `src/api/compareReports.ts`,不使用本地报表 Store;首次保存与更新来源由后端重新读取所选持久化当前有效数据。 |
| | | - 原列表筛选和分页保留;“编辑报表”读取最新快照,“修订记录”与详情“历史版本”共用正式版本摘要。 |
| | |
| | | - 默认详情读取最新版本;`?mode=view&version=N` 查看指定历史版,刷新保持对应版本,不提供回滚或删除历史版本。 |
| | | - “按最新来源生成版本”是独立动作,填写说明后重读当前来源并冻结;保留人工表格值,旧版和来源事实不被改写。来源测试已删除则拒绝更新,普通编辑及历史查看仍可使用保存快照。 |
| | | - 查看与历史接口执行 `system:compare-report:query`,编辑基准、普通编辑和更新来源执行 `system:compare-report:edit`;不限制为创建人本人。 |
| | | - 打印、模板持久化和整份报表删除尚未接入。本节为当前实施事实;下文为保留的旧原型说明,不作为新 Ticket 验收依据。 |
| | | - Ticket05 已接入指定保存版本打印及真实修订 CSV,导出权限为 `system:compare-report:export`;未保存先提示保存,打印不记录 PDF 下载成功。 |
| | | - 列表“删除”按 `system:compare-report:remove` 显示并调用正式 DELETE 接口,不限制创建人;确认范围包含整份报表及全部版本,不影响来源测试。取消不提交,失败保留正式记录,成功后刷新列表。 |
| | | - “操作日志”页复用平台查询接口显示真实日志,报表删除只保留身份、操作人和时间,不包含测量快照;删除与日志同步提交,平台清理接口不能删除该记录。日志查询沿用 `monitor:operlog:list` 权限。 |
| | | - 模板持久化仍待后续 Ticket。本节为当前实施事实;下文为保留的旧原型说明,不作为新 Ticket 验收依据。 |
| | | |
| | | - 路由:`/report-compare` |
| | | - 新增路由:`/report-compare/new` |
| | |
| | | return (await request<ApiResponse & { rows: CompareReportSummary[] }>('/system/compare-report/list')).rows |
| | | } |
| | | |
| | | /** 删除整份报表及全部版本,后端在同一事务保留最小删除日志。 */ |
| | | export async function deleteCompareReport(id: number) { |
| | | await request<ApiResponse>(`/system/compare-report/${id}`, { method: 'DELETE' }) |
| | | } |
| | | |
| | | export interface CompareSelection { |
| | | testId: number |
| | | testBatteryId: number |
| New file |
| | |
| | | import { request, type ApiResponse } from './request' |
| | | |
| | | interface PlatformOperationLog { |
| | | operId: number |
| | | operName: string |
| | | title: string |
| | | method: string |
| | | businessType: number |
| | | operParam: string |
| | | operTime: string |
| | | status: number |
| | | } |
| | | |
| | | export interface OperationLogRecord { |
| | | id: number |
| | | operator: string |
| | | content: string |
| | | operatedAt: string |
| | | } |
| | | |
| | | function operationContent(item: PlatformOperationLog) { |
| | | if (item.method === 'compare-report.delete') { |
| | | try { |
| | | const identity = JSON.parse(item.operParam) |
| | | if (typeof identity.reportId === 'number' && typeof identity.reportName === 'string') |
| | | return `删除对比报表“${identity.reportName}”(#${identity.reportId})及全部版本` |
| | | } catch { /* 非结构化平台日志仍展示模块与动作。 */ } |
| | | } |
| | | const actions = ['操作', '新增', '修改', '删除', '授权', '导出', '导入', '强退', '生成代码', '清空数据'] |
| | | return `${item.title}:${actions[item.businessType] || '操作'}(${item.status === 0 ? '成功' : '失败'})` |
| | | } |
| | | |
| | | /** 读取正式平台日志供既有本地筛选分页使用,不展示通用请求或响应中的业务内容。 */ |
| | | export async function listOperationLogs(): Promise<OperationLogRecord[]> { |
| | | const records: PlatformOperationLog[] = [] |
| | | for (let pageNum = 1; ; pageNum++) { |
| | | const result = await request<ApiResponse & { rows: PlatformOperationLog[]; total: number }>(`/monitor/operlog/list?pageNum=${pageNum}&pageSize=100`) |
| | | records.push(...result.rows) |
| | | if (!result.rows.length || records.length >= result.total) break |
| | | } |
| | | return records.map(item => ({ id: item.operId, operator: item.operName || '-', |
| | | content: operationContent(item), operatedAt: item.operTime || '', |
| | | })) |
| | | } |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref } from 'vue' |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { operationLogRecords } from '@/stores/operationLogs' |
| | | import { listOperationLogs, type OperationLogRecord } from '@/api/operationLogs' |
| | | |
| | | interface FilterState { |
| | | id: string |
| | |
| | | const appliedFilters = ref<FilterState>({ id: '', operator: '全部', dateRange: null }) |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | const operatorOptions = computed(() => [...new Set(operationLogRecords.map(item => item.operator))].sort((a, b) => a.localeCompare(b, 'zh-CN'))) |
| | | const operationLogRecords = ref<OperationLogRecord[]>([]) |
| | | const loading = ref(false) |
| | | const loadError = ref('') |
| | | onMounted(async () => { |
| | | loading.value = true |
| | | try { operationLogRecords.value = await listOperationLogs() } catch (error) { |
| | | loadError.value = error instanceof Error ? error.message : '操作日志加载失败' |
| | | } finally { loading.value = false } |
| | | }) |
| | | const operatorOptions = computed(() => [...new Set(operationLogRecords.value.map(item => item.operator))].sort((a, b) => a.localeCompare(b, 'zh-CN'))) |
| | | |
| | | const filteredRecords = computed(() => { |
| | | const query = appliedFilters.value |
| | | return operationLogRecords |
| | | return operationLogRecords.value |
| | | .filter(item => !query.id.trim() || String(item.id).includes(query.id.trim())) |
| | | .filter(item => query.operator === '全部' || item.operator === query.operator) |
| | | .filter(item => !query.dateRange || (item.operatedAt.slice(0, 10) >= query.dateRange[0] && item.operatedAt.slice(0, 10) <= query.dateRange[1])) |
| | |
| | | |
| | | <template> |
| | | <div class="operation-log-page"> |
| | | <el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon /> |
| | | <section class="filter-panel" aria-label="操作日志查询条件"> |
| | | <header class="section-heading"><div><i></i><strong>查询条件</strong><span>OPERATION LOG QUERY</span></div><small>共 {{ operationLogRecords.length }} 条日志</small></header> |
| | | <div class="filter-grid"> |
| | |
| | | <thead><tr><th>日志ID</th><th>操作人</th><th>操作内容</th><th>操作时间</th></tr></thead> |
| | | <tbody> |
| | | <tr v-for="item in pagedRecords" :key="item.id"><td class="id-cell">#{{ item.id }}</td><td><span class="operator-avatar">{{ item.operator.slice(0, 1) }}</span><strong>{{ item.operator }}</strong></td><td :title="item.content">{{ item.content }}</td><td class="time-cell">{{ item.operatedAt }}</td></tr> |
| | | <tr v-if="!pagedRecords.length"><td colspan="4" class="empty-cell">暂无符合条件的操作日志</td></tr> |
| | | <tr v-if="!pagedRecords.length"><td colspan="4" class="empty-cell">{{ loading ? '正在加载操作日志' : loadError ? '操作日志加载失败' : '暂无符合条件的操作日志' }}</td></tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useRouter } from 'vue-router' |
| | | import { listCompareReports } from '@/api/compareReports' |
| | | import { deleteCompareReport, listCompareReports } from '@/api/compareReports' |
| | | import type { CompareReportRecord } from '@/stores/compareReports' |
| | | import { getUserInfo } from '@/api/auth' |
| | | import CompareReportHistoryDialog from '@/components/CompareReportHistoryDialog.vue' |
| | |
| | | const canCreate = ref(false) |
| | | const canEdit = ref(false) |
| | | const canExport = ref(false) |
| | | const canDelete = ref(false) |
| | | const deletingId = ref<number | null>(null) |
| | | const historyVisible = ref(false) |
| | | const historyReportId = ref<number | null>(null) |
| | | onMounted(async () => { |
| | |
| | | canCreate.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:add')) |
| | | canEdit.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:edit')) |
| | | canExport.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:export')) |
| | | records.value = (await listCompareReports()).map(item => ({ ...item, |
| | | createdAt: new Date(item.createdAt).toLocaleString('sv-SE'), |
| | | baselineTest: '', comparisonTest: '', description: '', revisions: [], |
| | | })) |
| | | canDelete.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:remove')) |
| | | await loadReports() |
| | | } catch (error) { |
| | | loadError.value = error instanceof Error ? error.message : '报表列表加载失败' |
| | | } finally { loading.value = false } |
| | |
| | | router.push({ path: `/report-compare/${item.id}/edit`, query: { mode: 'view' } }) |
| | | } |
| | | |
| | | async function loadReports() { |
| | | records.value = (await listCompareReports()).map(item => ({ ...item, |
| | | createdAt: new Date(item.createdAt).toLocaleString('sv-SE'), |
| | | baselineTest: '', comparisonTest: '', description: '', revisions: [], |
| | | })) |
| | | currentPage.value = Math.min(currentPage.value, Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value))) |
| | | loadError.value = '' |
| | | } |
| | | |
| | | async function deleteReport(item: CompareReportRecord) { |
| | | if (deletingId.value !== null) return |
| | | deletingId.value = item.id |
| | | try { |
| | | await ElMessageBox.confirm(`将删除对比报表“${item.name}”(#${item.id})及全部版本,删除后无法恢复,不影响来源测试及其数据。是否继续?`, '确认删除对比报表', { |
| | | confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning', |
| | | }) |
| | | } catch { |
| | | deletingId.value = null |
| | | return |
| | | } |
| | | try { |
| | | await deleteCompareReport(item.id) |
| | | // 正式删除成功后才移除展示缓存;刷新失败不能误报删除失败。 |
| | | records.value = records.value.filter(record => record.id !== item.id) |
| | | ElMessage.success('对比报表及全部版本已删除') |
| | | try { await loadReports() } catch (error) { |
| | | loadError.value = `报表已删除,列表刷新失败:${error instanceof Error ? error.message : '请刷新页面重试'}` |
| | | } |
| | | } catch (error) { |
| | | ElMessage.error(`删除失败:${error instanceof Error ? error.message : '请稍后重试'}`) |
| | | } finally { deletingId.value = null } |
| | | } |
| | | |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | <tbody> |
| | | <tr v-for="item in pagedRecords" :key="item.id"> |
| | | <td class="id-cell">#{{ item.id }}</td><td :title="item.name"><strong>{{ item.name }}</strong></td><td>{{ item.createdAt }}</td><td>{{ item.creator }}</td> |
| | | <td class="action-column"><div class="row-actions"><button type="button" @click="openView(item)">查看报表</button><button v-if="canEdit" type="button" @click="router.push({ path: `/report-compare/${item.id}/edit`, query: { mode: 'edit' } })">编辑报表</button><button type="button" @click="historyReportId = item.id; historyVisible = true">修订记录</button></div></td> |
| | | <td class="action-column"><div class="row-actions"><button type="button" @click="openView(item)">查看报表</button><button v-if="canEdit" type="button" @click="router.push({ path: `/report-compare/${item.id}/edit`, query: { mode: 'edit' } })">编辑报表</button><button type="button" @click="historyReportId = item.id; historyVisible = true">修订记录</button><button v-if="canDelete" class="delete-action" type="button" :disabled="deletingId !== null" @click="deleteReport(item)">删除</button></div></td> |
| | | </tr> |
| | | <tr v-if="!pagedRecords.length"><td colspan="5" class="empty-cell">{{ loading ? '正在加载报表' : loadError ? '报表列表加载失败' : '暂无符合条件的对比报表' }}</td></tr> |
| | | </tbody> |
| | |
| | | .row-actions button { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; } |
| | | .row-actions button:hover { color: #176fcb; text-decoration: underline; } |
| | | .row-actions .delete-action { color: #ad4a4a; } |
| | | .row-actions button:disabled { opacity: .55; cursor: wait; } |
| | | .empty-cell { height: 160px; color: #8795a3; text-align: center; } |
| | | .pagination-bar { min-height: 58px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 14px 8px 16px; border-top: 1px solid var(--line); } |
| | | .pagination-bar > span { color: #7b8794; font-size: 11px; } |
| New file |
| | |
| | | import { expect, test, type Page } from '@playwright/test' |
| | | import { Ticket04Stack } from './support/ticket04-stack' |
| | | |
| | | const stack = new Ticket04Stack() |
| | | test.use({ actionTimeout: 20_000 }) |
| | | test.beforeAll(async () => stack.start()) |
| | | test.afterAll(async () => stack.stop()) |
| | | |
| | | async function login(page: Page, username = 'admin') { |
| | | if (page.url().startsWith('http')) await page.evaluate(() => { |
| | | localStorage.removeItem('battery_detection_auth_token') |
| | | localStorage.removeItem('battery_detection_auth_user') |
| | | }) |
| | | await page.goto('/login') |
| | | await page.locator('input[autocomplete="username"]').fill(username) |
| | | await page.locator('input[autocomplete="current-password"]').fill(username === 'admin' ? 'admin123' : 'Report123!') |
| | | await page.getByRole('button', { name: '登录', exact: true }).click() |
| | | await expect(page).toHaveURL(/\/dashboard$/) |
| | | } |
| | | |
| | | async function api(page: Page, path: string, method = 'GET', data?: unknown, code = 200) { |
| | | const token = await page.evaluate(() => localStorage.getItem('battery_detection_auth_token')) |
| | | const response = await page.request.fetch(`${stack.backendUrl}${path}`, { |
| | | method, data, headers: { Authorization: `Bearer ${token}` }, |
| | | }) |
| | | const body = await response.json() |
| | | expect(body.code, `${path}: ${body.msg}`).toBe(code) |
| | | return body |
| | | } |
| | | |
| | | test('删除整份报表及全部版本,日志同步保留且失败完整回滚', async ({ page }) => { |
| | | // 已保存版本作为受控前置数据;删除只能依赖报表身份,不能重读来源或快照内容。 |
| | | stack.executeIsolatedSql(` |
| | | INSERT INTO t_compare_report(report_id,report_name,creator_id,creator_name,created_at_utc) |
| | | VALUES(8601,'删除验收报表',1,'admin',UTC_TIMESTAMP(3)); |
| | | INSERT INTO t_compare_report_version(report_id,version_no,snapshot_json,operator_id,operator_name,created_at_utc,change_summary) |
| | | VALUES(8601,1,'{"name":"旧版","items":[{"voltage":3.7}]}',1,'admin',UTC_TIMESTAMP(3),'首次保存'), |
| | | (8601,2,'{"name":"删除验收报表","items":[{"voltage":3.8}]}',1,'admin',UTC_TIMESTAMP(3),'修改名称'); |
| | | `) |
| | | await login(page) |
| | | const current = (await api(page, '/system/compare-report/8601')).data |
| | | const history = (await api(page, '/system/compare-report/8601/versions/1')).data |
| | | // 在最后一步日志写入注入失败,已执行的版本和身份删除必须一并回滚。 |
| | | stack.executeIsolatedSql("CREATE TRIGGER reject_report_delete_log BEFORE INSERT ON sys_oper_log FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='删除日志写入失败'") |
| | | try { |
| | | await api(page, '/system/compare-report/8601', 'DELETE', undefined, 500) |
| | | expect((await api(page, '/system/compare-report/8601')).data).toEqual(current) |
| | | expect((await api(page, '/system/compare-report/8601/versions/1')).data).toEqual(history) |
| | | expect(stack.queryDatabase("SELECT COUNT(*) FROM sys_oper_log WHERE method='compare-report.delete'")).toBe('0') |
| | | } finally { stack.executeIsolatedSql('DROP TRIGGER reject_report_delete_log') } |
| | | await api(page, '/system/compare-report/8601', 'DELETE') |
| | | expect(stack.queryDatabase('SELECT COUNT(*) FROM t_compare_report WHERE report_id=8601')).toBe('0') |
| | | expect(stack.queryDatabase('SELECT COUNT(*) FROM t_compare_report_version WHERE report_id=8601')).toBe('0') |
| | | await api(page, '/system/compare-report/8601', 'GET', undefined, 500) |
| | | await api(page, '/system/compare-report/8601/versions/1', 'GET', undefined, 500) |
| | | await api(page, '/system/compare-report/8601/versions/2/print', 'GET', undefined, 500) |
| | | expect((await api(page, '/system/compare-report/list')).rows).toEqual([]) |
| | | const logs = (await api(page, '/monitor/operlog/list?title=' + encodeURIComponent('对比报表删除'))).rows |
| | | expect(logs).toHaveLength(1) |
| | | expect(logs[0]).toMatchObject({ title: '对比报表删除', businessType: 3, operName: 'admin', status: 0 }) |
| | | expect(JSON.parse(logs[0].operParam)).toEqual({ reportId: 8601, reportName: '删除验收报表', operatorId: 1 }) |
| | | expect(logs[0].operTime).toBeTruthy() |
| | | expect(logs[0].jsonResult).toBe('') |
| | | await api(page, '/system/compare-report/8601', 'DELETE', undefined, 500) |
| | | expect(stack.queryDatabase("SELECT COUNT(*) FROM sys_oper_log WHERE method='compare-report.delete'")).toBe('1') |
| | | }) |
| | | |
| | | test('列表取消与失败保留记录,跨创建人删除不改变来源,日志只读可查', async ({ page }) => { |
| | | const pageErrors: string[] = [] |
| | | page.on('pageerror', error => pageErrors.push(error.message)) |
| | | stack.executeIsolatedSql(` |
| | | INSERT INTO t_battery_group(group_id,group_name,model) VALUES(8602,'删除验收组','型号'); |
| | | INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(8602,'删除验收类型'); |
| | | INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status) |
| | | VALUES(8602,'删除报表保留的来源',1,1,8602,'隔离验收','COMPLETED','ENDED'); |
| | | INSERT INTO t_test_battery(test_battery_id,test_id,group_id,battery_no,collector_id) VALUES(8602,8602,8602,'BAT-D',8602); |
| | | INSERT INTO t_detection_cabinet(cabinet_id,adapter_code,cabinet_code,cabinet_name,vendor_device_type,vendor_device_id) |
| | | VALUES(8602,'delete-fixture','delete-fixture','删除验收检测柜',27,1); |
| | | INSERT INTO t_detection_channel(channel_id,adapter_code,cabinet_id,devtype,devid,subdevid,chlid) VALUES(8602,'delete-fixture',8602,27,1,1,1); |
| | | INSERT INTO t_test_channel_binding(binding_id,test_id,test_battery_id,channel_id) VALUES(8602,8602,8602,8602); |
| | | INSERT INTO t_test_realtime_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,cabinet_devtype,cabinet_devid,cabinet_subdevid,cabinet_chlid,cabinet_sequence_id,cabinet_device_time_utc,server_received_at_utc,cycle_no,step_no,step_name,voltage_v,current_a,content_sha256) |
| | | VALUES(8602,8602,8602,'delete-fixture','delete:27:1:1:1','1',27,1,1,1,1,'2026-09-01 00:00:10.123','2026-09-01 00:00:11.123',1,1,'充电',3.7,-2,UNHEX(SHA2('d1',256))), |
| | | (8602,8602,8602,'delete-fixture','delete:27:1:1:1','2',27,1,1,1,2,'2026-09-01 00:01:10.123','2026-09-01 00:01:11.123',1,2,'放电',3.6,1,UNHEX(SHA2('d2',256))); |
| | | INSERT INTO t_test_step_record(test_id,test_battery_id,binding_id,dedupe_key,server_received_at_utc,adapter_code,source_identity,source_record_id,adapter_timezone_id,device_time_utc,device_time_raw,cycle_no,step_no,voltage_v,content_sha256) |
| | | VALUES(8602,8602,8602,'record:1','2026-09-01 00:01:11.123','delete-fixture','delete:27:1:1:1','1','Asia/Hong_Kong','2026-09-01 00:01:10.123','2026-09-01 08:01:10.123',1,1,3.7,UNHEX(SHA2('s1',256))); |
| | | INSERT INTO t_test_cycle_record(test_id,test_battery_id,binding_id,dedupe_key,server_received_at_utc,adapter_code,source_identity,source_record_id,adapter_timezone_id,device_time_utc,device_time_raw,cycle_no,charge_energy_wh,discharge_energy_wh,content_sha256) |
| | | VALUES(8602,8602,8602,'record:1','2026-09-01 00:01:11.123','delete-fixture','delete:27:1:1:1','1','Asia/Hong_Kong','2026-09-01 00:01:10.123','2026-09-01 08:01:10.123',1,0.1361,0.1694,UNHEX(SHA2('c1',256))); |
| | | `) |
| | | await login(page) |
| | | const payload = { |
| | | name: '跨用户删除报表', description: '说明不进入删除日志', experimentName: '删除验收', |
| | | selections: [1, 2].map(stepNo => ({ testId: 8602, testBatteryId: 8602, cycleNo: 1, stepNo, logicalNo: null })), |
| | | settings: { metrics: ['voltage'], seriesColors: ['#123456', '#109854'], batteryTemplateFields: [], specialFields: [], detailFieldValues: {} }, |
| | | } |
| | | const v1 = (await api(page, '/system/compare-report', 'POST', payload)).data |
| | | const v2 = (await api(page, `/system/compare-report/${v1.id}/versions`, 'POST', { |
| | | name: '跨用户删除报表V2', description: payload.description, experimentName: payload.experimentName, |
| | | settings: payload.settings, baseVersionNo: 1, changeSummary: '删除前保留两版', |
| | | })).data |
| | | const survivor = (await api(page, '/system/compare-report', 'POST', { ...payload, name: '来源删除仍保留报表' })).data |
| | | const sourceTables = ['t_test', 't_test_battery', 't_test_realtime_record', 't_test_step_record', 't_test_cycle_record', 't_test_data_revision_action'] |
| | | const beforeSources = sourceTables.map(table => stack.queryDatabase(`SELECT * FROM ${table} WHERE test_id=8602`)) |
| | | const menus = (await api(page, '/system/menu/permissionList')).data |
| | | const comparePage = menus.flatMap((module: any) => module.pages).find((item: any) => item.pageName === '对比报表') |
| | | expect(comparePage.permissions.some((item: any) => item.perms === 'system:compare-report:remove')).toBe(true) |
| | | for (const [username, actions] of [['delete_reader', ['查看']], ['delete_operator', ['查看', '删除']], ['delete_only', ['删除']]] as const) { |
| | | const menuIds = comparePage.permissions.filter((item: any) => (actions as readonly string[]).includes(item.menuName)).map((item: any) => item.menuId) |
| | | await api(page, '/system/role', 'POST', { roleName: username, roleKey: 'common', roleSort: 1, status: '0', menuIds }) |
| | | const role = (await api(page, `/system/role/list?roleName=${username}`)).rows[0] |
| | | await api(page, '/system/user', 'POST', { userName: username, nickName: username, password: 'Report123!', status: '0', roleIds: [role.roleId] }) |
| | | } |
| | | await login(page, 'delete_reader') |
| | | await api(page, `/system/compare-report/${v1.id}`, 'DELETE', undefined, 403) |
| | | await page.goto('/report-compare') |
| | | await expect(page.getByRole('row').filter({ hasText: '跨用户删除报表V2' })).toBeVisible() |
| | | await expect(page.getByRole('button', { name: '删除', exact: true })).toHaveCount(0) |
| | | await api(page, '/monitor/operlog/list', 'GET', undefined, 403) |
| | | await page.goto('/operation-log') |
| | | await expect(page.locator('.operation-log-page .el-alert')).toBeVisible() |
| | | await expect(page.locator('.operation-log-page .empty-cell')).toHaveText('操作日志加载失败') |
| | | |
| | | await login(page, 'delete_operator') |
| | | await page.goto('/report-compare') |
| | | const row = page.getByRole('row').filter({ hasText: '跨用户删除报表V2' }) |
| | | await row.getByRole('button', { name: '删除', exact: true }).click() |
| | | const dialog = page.getByRole('dialog', { name: '确认删除对比报表' }) |
| | | await expect(dialog).toContainText('全部版本') |
| | | await expect(dialog).toContainText('不影响来源测试') |
| | | for (const width of [1440, 390]) { |
| | | await page.setViewportSize({ width, height: 900 }) |
| | | await expect(dialog.getByRole('button', { name: '删除', exact: true })).toBeInViewport() |
| | | await page.screenshot({ path: `tests/e2e/.artifacts/compare-delete-confirm-${width}.png`, fullPage: true }) |
| | | } |
| | | await dialog.getByRole('button', { name: '取消', exact: true }).click() |
| | | expect((await api(page, `/system/compare-report/${v1.id}`)).data).toEqual(v2) |
| | | await page.setViewportSize({ width: 1440, height: 900 }) |
| | | // 正式后端日志故障在浏览器反馈失败,列表仍保留尚未删除的记录。 |
| | | stack.executeIsolatedSql("CREATE TRIGGER reject_browser_delete_log BEFORE INSERT ON sys_oper_log FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='删除日志写入失败'") |
| | | try { |
| | | await row.getByRole('button', { name: '删除', exact: true }).click() |
| | | await dialog.getByRole('button', { name: '删除', exact: true }).click() |
| | | await expect(page.locator('.el-message--error')).toBeVisible() |
| | | await expect(row).toBeVisible() |
| | | expect((await api(page, `/system/compare-report/${v1.id}`)).data).toEqual(v2) |
| | | } finally { stack.executeIsolatedSql('DROP TRIGGER reject_browser_delete_log') } |
| | | await row.getByRole('button', { name: '删除', exact: true }).click() |
| | | await dialog.getByRole('button', { name: '删除', exact: true }).click() |
| | | await expect(page.getByText('对比报表及全部版本已删除', { exact: true })).toBeVisible() |
| | | await expect(row).toHaveCount(0) |
| | | await page.reload() |
| | | await expect(row).toHaveCount(0) |
| | | expect(sourceTables.map(table => stack.queryDatabase(`SELECT * FROM ${table} WHERE test_id=8602`))).toEqual(beforeSources) |
| | | expect(stack.queryDatabase(`SELECT COUNT(*) FROM t_compare_report_version WHERE report_id=${v1.id}`)).toBe('0') |
| | | |
| | | await login(page) |
| | | const logs = (await api(page, '/monitor/operlog/list?title=' + encodeURIComponent('对比报表删除'))).rows |
| | | const log = logs.find((item: any) => JSON.parse(item.operParam).reportId === v1.id) |
| | | expect(log.operName).toBe('delete_operator') |
| | | expect(JSON.parse(log.operParam).reportName).toBe('跨用户删除报表V2') |
| | | await page.goto('/operation-log') |
| | | await expect(page.getByRole('row').filter({ hasText: '跨用户删除报表V2' })).toContainText('delete_operator') |
| | | await page.getByPlaceholder('输入日志ID').fill(String(log.operId)) |
| | | await page.getByRole('button', { name: '查询', exact: true }).click() |
| | | await expect(page.locator('.operation-log-page .table-wrap tbody tr')).toHaveCount(1) |
| | | await expect(page.locator('.operation-log-page .table-wrap tbody')).toContainText(`#${v1.id}`) |
| | | await page.screenshot({ path: 'tests/e2e/.artifacts/compare-delete-log.png', fullPage: true }) |
| | | await page.route('**/dev-api/monitor/operlog/list?*', route => route.fulfill({ json: { code: 500, msg: '日志查询故障' } }), { times: 1 }) |
| | | await page.reload() |
| | | await expect(page.locator('.operation-log-page .el-alert')).toContainText('日志查询故障') |
| | | await page.reload() |
| | | await expect(page.getByRole('row').filter({ hasText: '跨用户删除报表V2' })).toContainText('delete_operator') |
| | | // 现有平台批量删除和清空接口不得移除必须保留的报表删除日志。 |
| | | await api(page, `/monitor/operlog/${log.operId}`, 'DELETE', undefined, 500) |
| | | await api(page, '/monitor/operlog/clean', 'DELETE') |
| | | expect(stack.queryDatabase(`SELECT COUNT(*) FROM sys_oper_log WHERE oper_id=${log.operId}`)).toBe('1') |
| | | await api(page, '/system/test/8602', 'DELETE') |
| | | expect((await api(page, `/system/compare-report/${survivor.id}`)).data).toEqual(survivor) |
| | | expect(stack.queryDatabase("SELECT COUNT(*) FROM sys_oper_log WHERE oper_url='/system/test/8602'")).toBe('0') |
| | | await login(page, 'delete_only') |
| | | await api(page, `/system/compare-report/${survivor.id}`, 'GET', undefined, 403) |
| | | await api(page, `/system/compare-report/${survivor.id}`, 'DELETE') |
| | | expect(stack.queryDatabase(`SELECT COUNT(*) FROM t_compare_report_version WHERE report_id=${survivor.id}`)).toBe('0') |
| | | expect(pageErrors).toEqual([]) |
| | | }) |