feat: 节点互联双向同步——授权/计划/客户(邮箱)按业务键LWW增量同步,HTTPS+HMAC互联密钥,默认10分钟宽松节奏
This commit is contained in:
1 parent
06a9dac99b
commit
44387835e1
13 files changed
+1180
-1
No files matched your search
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @file sync.js
|
||||
* @description 节点互联(双向授权同步)管理 API // Logan.Li
|
||||
*/
|
||||
import request from './request'
|
||||
|
||||
export function listPeers() {
|
||||
return request.get('/sync/peers')
|
||||
}
|
||||
|
||||
export function savePeer(data) {
|
||||
return request.post('/sync/peers', data)
|
||||
}
|
||||
|
||||
export function deletePeer(id) {
|
||||
return request.delete(`/sync/peers/${id}`)
|
||||
}
|
||||
|
||||
export function handshakePeer(id) {
|
||||
return request.post(`/sync/peers/${id}/handshake`)
|
||||
}
|
||||
|
||||
export function syncPeerNow(id) {
|
||||
return request.post(`/sync/peers/${id}/sync`)
|
||||
}
|
||||
|
||||
export function generateKey() {
|
||||
return request.get('/sync/generate-key')
|
||||
}
|
||||
@@ -56,6 +56,11 @@
|
||||
<template #title>支付</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/sync">
|
||||
<el-icon><Connection /></el-icon>
|
||||
<template #title>节点同步</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu index="system">
|
||||
<template #title>
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
|
||||
@@ -71,6 +71,12 @@ const routes = [
|
||||
component: () => import('@/views/Logs.vue'),
|
||||
meta: { title: '操作日志', icon: 'Document' },
|
||||
},
|
||||
{
|
||||
path: 'sync',
|
||||
name: 'Sync',
|
||||
component: () => import('@/views/Sync.vue'),
|
||||
meta: { title: '节点同步', icon: 'Connection' },
|
||||
},
|
||||
{
|
||||
path: 'integration',
|
||||
name: 'Integration',
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<span class="node-info">本机节点 ID:<code>{{ nodeId }}</code></span>
|
||||
<el-button type="primary" @click="openCreate">添加互联节点</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert type="info" :closable="false" class="mb"
|
||||
title="双向同步:产品 / 计划 / 客户(含邮箱)/ 授权。两边各配置对方地址与相同互联密钥,握手激活后每 10 分钟自动同步一轮,也可手动触发。通道 = HTTPS + 互联密钥 HMAC 签名,手机/地址/设备 IP 不同步。" />
|
||||
|
||||
<el-table :data="peers" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" label="节点" min-width="120" />
|
||||
<el-table-column prop="base_url" label="对端地址" min-width="220" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : row.status === 'disabled' ? 'info' : 'warning'" size="small">
|
||||
{{ { active: '已激活', pending: '待握手', disabled: '已停用' }[row.status] || row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="密钥尾号" width="90">
|
||||
<template #default="{ row }"><code>…{{ row.key_tail }}</code></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_sync_at" label="上次同步" width="160" />
|
||||
<el-table-column prop="last_result" label="最近结果" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="success" @click="doSync(row)">立即同步</el-button>
|
||||
<el-button size="small" @click="doHandshake(row)">测试握手</el-button>
|
||||
<el-button size="small" type="danger" plain @click="doDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑节点' : '添加互联节点'" width="560px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="节点名称">
|
||||
<el-input v-model="form.name" placeholder="如:香港节点" />
|
||||
</el-form-item>
|
||||
<el-form-item label="对端地址">
|
||||
<el-input v-model="form.base_url" placeholder="https://l123.91ai.io(自动补 /api)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="互联密钥">
|
||||
<el-input v-model="form.shared_key" placeholder="32 位以上 hex,两边一致" show-password>
|
||||
<template #append>
|
||||
<el-button @click="genKey">生成</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<span class="tip">两台服务器都要添加对方,并使用同一把互联密钥;任一侧配置后点「测试握手」完成确认。</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 节点互联管理页:双向授权同步的 peer 配置与手动触发 // Logan.Li
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listPeers, savePeer, deletePeer, handshakePeer, syncPeerNow, generateKey } from '@/api/sync'
|
||||
|
||||
const peers = ref([])
|
||||
const nodeId = ref('')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const form = reactive({ id: 0, name: '', base_url: '', shared_key: '' })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listPeers()
|
||||
peers.value = res.data?.list || []
|
||||
nodeId.value = res.data?.node_id || ''
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, { id: 0, name: '', base_url: '', shared_key: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function genKey() {
|
||||
const res = await generateKey()
|
||||
form.shared_key = res.data?.shared_key || ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!form.name || !form.base_url || form.shared_key.length < 32) {
|
||||
ElMessage.error('请完整填写名称、地址,密钥至少 32 位')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
await savePeer({ ...form })
|
||||
ElMessage.success('已保存')
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function doHandshake(row) {
|
||||
const res = await handshakePeer(row.id)
|
||||
if (res.code === 0) ElMessage.success(res.message)
|
||||
await load()
|
||||
}
|
||||
|
||||
async function doSync(row) {
|
||||
const res = await syncPeerNow(row.id)
|
||||
if (res.code === 0) ElMessage.success(res.message)
|
||||
await load()
|
||||
}
|
||||
|
||||
async function doDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除互联节点「${row.name}」吗?删除后两台数据各自独立、不再同步。`, '删除确认', { type: 'warning' })
|
||||
await deletePeer(row.id)
|
||||
await load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||
.mb { margin-bottom: 16px; }
|
||||
.node-info { font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.node-info code { background: var(--el-fill-color); padding: 2px 6px; border-radius: 4px; }
|
||||
.tip { font-size: 12px; color: var(--el-text-color-secondary); line-height: 1.6; }
|
||||
</style>
|
||||
Reference in new issue
Block a user