Browse Source

业务数据同步

dev-V1.10.2-temp
zhuibobo 3 months ago
parent
commit
be293241a9
  1. 16
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/controller/BizSyncManageController.java
  2. 10
      zdxt-web-client/zdxt-admin-plus-ui/src/api/efcode/biz/index.ts
  3. 286
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/biz/index.vue

16
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/controller/BizSyncManageController.java

@ -145,6 +145,10 @@ public class BizSyncManageController {
Page<BizSyncRecord> page = recordMapper.selectPage( Page<BizSyncRecord> page = recordMapper.selectPage(
new Page<>(pageNum, pageSize), new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<BizSyncRecord>() new LambdaQueryWrapper<BizSyncRecord>()
// 列表页排除两个大 JSON 快照列,避免全量传输拖慢查询
.select(BizSyncRecord.class, f ->
!"snapshotV1Json".equals(f.getProperty())
&& !"snapshotV2Json".equals(f.getProperty()))
.eq(configKey != null && !configKey.isEmpty(), BizSyncRecord::getConfigKey, configKey) .eq(configKey != null && !configKey.isEmpty(), BizSyncRecord::getConfigKey, configKey)
.eq(status != null && !status.isEmpty(), BizSyncRecord::getStatus, status) .eq(status != null && !status.isEmpty(), BizSyncRecord::getStatus, status)
.orderByDesc(BizSyncRecord::getUpdateTime) .orderByDesc(BizSyncRecord::getUpdateTime)
@ -152,6 +156,18 @@ public class BizSyncManageController {
return R.ok(page); return R.ok(page);
} }
/**
* 查询影子记录详情(含 snapshot_v1_json / snapshot_v2_json 完整快照)
*/
@GetMapping("/record/detail/{id}")
public R<BizSyncRecord> recordDetail(@PathVariable Long id) {
BizSyncRecord record = recordMapper.selectById(id);
if (record == null) {
return R.fail("记录不存在");
}
return R.ok(record);
}
/** /**
* 重试死信记录 * 重试死信记录
*/ */

10
zdxt-web-client/zdxt-admin-plus-ui/src/api/efcode/biz/index.ts

@ -74,6 +74,16 @@ export const getRecordList = (params: { configKey?: string; status?: string; pag
}); });
}; };
/**
* 查询影子记录详情(含快照JSON)
*/
export const getRecordDetail = (id: number) => {
return request({
url: '/web/biz-sync/record/detail/' + id,
method: 'get'
});
};
/** /**
* 重试死信记录 * 重试死信记录
*/ */

286
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/biz/index.vue

@ -9,7 +9,10 @@ import {
triggerForceSync, triggerForceSync,
getTaskList, getTaskList,
getLogList, getLogList,
getJobStatus getJobStatus,
getRecordList,
getRecordDetail,
retryRecord
} from '@/api/efcode/biz'; } from '@/api/efcode/biz';
/** /**
@ -118,6 +121,105 @@ const syncConfigs = ref([
const loading = ref(false); const loading = ref(false);
// ============ Tab 切换 ============
const activeTab = ref('config');
/** Tab 切换:首次进入影子记录页时自动加载 */
const handleTabChange = (name: string | number) => {
if (name === 'record' && recordList.value.length === 0) {
loadRecordList();
}
};
// ============ 影子记录 Tab ============
const recordLoading = ref(false);
const recordList = ref<any[]>([]);
const recordTotal = ref(0);
const recordQuery = reactive({
configKey: '',
status: '',
pageNum: 1,
pageSize: 20
});
/** 加载影子记录列表(后端按 create_time 倒序) */
const loadRecordList = async () => {
recordLoading.value = true;
try {
const res: any = await getRecordList(recordQuery);
const data = res.data;
recordList.value = data?.records || [];
recordTotal.value = data?.total || 0;
} catch (e) {
console.error('加载影子记录失败', e);
} finally {
recordLoading.value = false;
}
};
/** 影子记录查询(重置到第一页) */
const handleRecordSearch = () => {
recordQuery.pageNum = 1;
loadRecordList();
};
/** 重试死信记录 */
const handleRetryRecord = async (row: any) => {
try {
await ElMessageBox.confirm(
`确认重试该死信记录(主键: ${row.pkValue})?`,
'确认',
{ type: 'warning' }
);
const res: any = await retryRecord(row.id);
ElMessage.success(res?.msg || '重试成功');
loadRecordList();
} catch (e: any) {
if (e !== 'cancel' && e?.action !== 'cancel') {
ElMessage.error(e?.msg || '重试失败');
}
}
};
/** 影子记录状态标签类型 */
const recordStatusType = (status: string) => {
switch (status) {
case 'NORMAL': return 'success';
case 'DEAD': return 'danger';
default: return 'info';
}
};
// ============ 同步记录详情弹窗 ============
const recordDetailVisible = ref(false);
const recordDetailLoading = ref(false);
const recordDetail = ref<any>(null);
/** 格式化 JSON 字符串(美化缩进) */
const prettyJson = (jsonStr: string) => {
if (!jsonStr) return '(空)';
try {
return JSON.stringify(JSON.parse(jsonStr), null, 2);
} catch (e) {
return jsonStr;
}
};
/** 查看同步记录详情(含快照JSON,按需加载) */
const handleViewRecordDetail = async (row: any) => {
recordDetailVisible.value = true;
recordDetailLoading.value = true;
recordDetail.value = null;
try {
const res: any = await getRecordDetail(row.id);
recordDetail.value = res?.data || null;
} catch (e: any) {
ElMessage.error(e?.msg || '加载详情失败');
} finally {
recordDetailLoading.value = false;
}
};
// ============ 异步任务轮询状态 ============ // ============ 异步任务轮询状态 ============
interface RunningJob { interface RunningJob {
jobId: string; jobId: string;
@ -452,6 +554,9 @@ onUnmounted(() => {
</template> </template>
</el-alert> </el-alert>
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<!-- Tab1: 同步配置列表 -->
<el-tab-pane label="同步配置" name="config">
<el-card shadow="never"> <el-card shadow="never">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
@ -512,6 +617,112 @@ onUnmounted(() => {
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
</el-tab-pane>
<!-- Tab2: 同步记录列表(biz_sync_record,按创建时间倒序) -->
<el-tab-pane label="同步记录" name="record">
<el-card shadow="never">
<template #header>
<div class="card-header">
<div class="header-title">
<span>同步记录 (biz_sync_record)</span>
<el-tag type="info" size="small" style="margin-left: 8px">
共 {{ recordTotal }} 条
</el-tag>
</div>
</div>
</template>
<!-- 查询条件 -->
<el-form inline style="margin-bottom: 8px">
<el-form-item label="配置Key">
<el-select
v-model="recordQuery.configKey"
placeholder="全部"
clearable
style="width: 280px"
@change="handleRecordSearch"
>
<el-option
v-for="cfg in syncConfigs"
:key="cfg.configKey"
:label="cfg.configKey"
:value="cfg.configKey"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="recordQuery.status"
placeholder="全部"
clearable
style="width: 140px"
@change="handleRecordSearch"
>
<el-option label="NORMAL" value="NORMAL" />
<el-option label="DEAD" value="DEAD" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="recordLoading" @click="handleRecordSearch">查询</el-button>
<el-button :loading="recordLoading" @click="loadRecordList">刷新</el-button>
</el-form-item>
</el-form>
<el-table v-loading="recordLoading" :data="recordList" border stripe size="small" style="width: 100%">
<el-table-column label="配置Key" prop="configKey" width="230" show-overflow-tooltip />
<el-table-column label="主键值" prop="pkValue" width="220" show-overflow-tooltip />
<el-table-column label="归属方" prop="ownerSide" width="80" align="center" />
<el-table-column label="最近动作" width="160" align="center">
<template #default="{ row }">
<el-tag :type="actionTagType(row.lastAction)" size="small">{{ row.lastAction || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="recordStatusType(row.status)" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="重试次数" prop="retryCount" width="90" align="center" />
<el-table-column label="最近同步时间" width="160">
<template #default="{ row }">{{ formatTime(row.lastSyncTime) }}</template>
</el-table-column>
<el-table-column label="创建时间" width="160">
<template #default="{ row }">{{ formatTime(row.createTime) }}</template>
</el-table-column>
<el-table-column label="更新时间" width="160">
<template #default="{ row }">{{ formatTime(row.updateTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="130" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleViewRecordDetail(row)">
详情
</el-button>
<el-button
v-if="row.status === 'DEAD'"
type="warning"
link
size="small"
@click="handleRetryRecord(row)"
>
重试
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="recordQuery.pageNum"
v-model:page-size="recordQuery.pageSize"
:total="recordTotal"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
class="mt-4"
@size-change="loadRecordList"
@current-change="loadRecordList"
/>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 任务记录弹窗 --> <!-- 任务记录弹窗 -->
<el-dialog <el-dialog
@ -597,6 +808,56 @@ onUnmounted(() => {
@current-change="loadLogList" @current-change="loadLogList"
/> />
</el-dialog> </el-dialog>
<!-- 同步记录详情弹窗(含快照JSON) -->
<el-dialog
v-model="recordDetailVisible"
title="同步记录详情"
width="1200px"
append-to-body
>
<div v-loading="recordDetailLoading">
<template v-if="recordDetail">
<el-descriptions :column="3" border size="small" style="margin-bottom: 16px">
<el-descriptions-item label="配置Key">{{ recordDetail.configKey }}</el-descriptions-item>
<el-descriptions-item label="主键值">{{ recordDetail.pkValue }}</el-descriptions-item>
<el-descriptions-item label="归属方">{{ recordDetail.ownerSide }}</el-descriptions-item>
<el-descriptions-item label="最近动作">
<el-tag :type="actionTagType(recordDetail.lastAction)" size="small">
{{ recordDetail.lastAction || '-' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="recordStatusType(recordDetail.status)" size="small">
{{ recordDetail.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="重试次数">{{ recordDetail.retryCount }}</el-descriptions-item>
<el-descriptions-item label="最近同步时间">{{ formatTime(recordDetail.lastSyncTime) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatTime(recordDetail.createTime) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatTime(recordDetail.updateTime) }}</el-descriptions-item>
<el-descriptions-item label="V1内容哈希" :span="3">
<span class="hash-text">{{ recordDetail.contentHashV1 || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="V2内容哈希" :span="3">
<span class="hash-text">{{ recordDetail.contentHashV2 || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
<el-row :gutter="12">
<el-col :span="12">
<div class="json-panel-title">V1 快照 (snapshot_v1_json)</div>
<pre class="json-panel">{{ prettyJson(recordDetail.snapshotV1Json) }}</pre>
</el-col>
<el-col :span="12">
<div class="json-panel-title">V2 快照 (snapshot_v2_json)</div>
<pre class="json-panel">{{ prettyJson(recordDetail.snapshotV2Json) }}</pre>
</el-col>
</el-row>
</template>
<el-empty v-else-if="!recordDetailLoading" description="未加载到详情数据" />
</div>
</el-dialog>
</div> </div>
</template> </template>
@ -617,6 +878,29 @@ onUnmounted(() => {
.mt-4 { .mt-4 {
margin-top: 16px; margin-top: 16px;
} }
.hash-text {
font-family: Consolas, Monaco, monospace;
font-size: 12px;
word-break: break-all;
}
.json-panel-title {
font-weight: bold;
margin-bottom: 6px;
}
.json-panel {
height: 420px;
overflow: auto;
margin: 0;
padding: 10px;
background: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
font-family: Consolas, Monaco, monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
</style> </style>
<!-- 全局样式(非 scoped):ElLoading 挂在 body 上,scoped 样式无法命中 --> <!-- 全局样式(非 scoped):ElLoading 挂在 body 上,scoped 样式无法命中 -->

Loading…
Cancel
Save