fix: 渠道账号群聊同步缓存 key 及成员名称兜底;统一 Docker/Maven 项目约定

This commit is contained in:
2026-07-10 15:55:30 +08:00
parent 560bd4599b
commit 758ed73f9d
56 changed files with 7689 additions and 1127 deletions

View File

@@ -1,46 +1,64 @@
<template>
<div>
<div class="toolbar">
<el-button type="primary" @click="openCreateDialog">创建渠道主体</el-button>
</div>
<PageCard>
<template #toolbar-left>
<el-input
v-model="searchKeyword"
placeholder="搜索主体名称 / 编码 / CorpID"
clearable
class="search-input"
@keyup.enter="handleSearch"
/>
<el-button @click="handleSearch">查询</el-button>
</template>
<template #toolbar-right>
<el-button type="primary" @click="openCreateDialog">新增主体</el-button>
</template>
<el-table :data="subjects" border>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="channelType" label="渠道">
<el-table
:data="pagedSubjects"
class="sino-table"
@row-click="handleRowClick"
>
<el-table-column prop="subjectName" label="主体名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="channelType" label="渠道类型" width="100">
<template #default="{ row }">
{{ channelLabel(row.channelType) }}
</template>
</el-table-column>
<el-table-column prop="subjectCode" label="编码" />
<el-table-column prop="subjectName" label="名称" />
<el-table-column prop="corpId" label="CorpID" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">
{{ row.status === 1 ? '启用' : '禁用' }}
<el-tag :type="row.channelType === 'wecom' ? 'primary' : 'info'" size="small">
{{ channelLabel(row.channelType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="340">
<el-table-column prop="subjectCode" label="主体编码" min-width="140" show-overflow-tooltip />
<el-table-column prop="corpId" label="CorpID" min-width="220" show-overflow-tooltip />
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-button size="small" @click="openEditDialog(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
style="margin: 0 12px"
@change="(val: any) => handleStatusChange(row, val)"
/>
<el-button size="small" @click="openDetail(row)">详情</el-button>
<div class="status-cell" @click.stop>
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="(val: any) => handleStatusChange(row, val)"
/>
<span class="status-text">{{ row.status === 1 ? '启用' : '禁用' }}</span>
</div>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑渠道主体' : '创建渠道主体'" width="600px">
<template #pagination>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="filteredSubjects.length"
layout="total, prev, pager, next"
background
/>
</template>
<!-- 新增弹窗 -->
<el-dialog v-model="dialogVisible" title="新增渠道主体" width="600px" destroy-on-close>
<el-form :model="dialogForm" label-width="120px">
<el-form-item label="渠道">
<el-select v-model="dialogForm.channelType" placeholder="选择渠道">
<el-select v-model="dialogForm.channelType" placeholder="选择渠道" style="width: 100%">
<el-option label="企微" value="wecom" />
<el-option label="钉钉" value="dingtalk" />
<el-option label="飞书" value="feishu" />
@@ -55,9 +73,6 @@
<el-form-item label="CorpID">
<el-input v-model="dialogForm.corpId" placeholder="CorpID" />
</el-form-item>
<el-form-item label="CorpSecret">
<el-input v-model="dialogForm.corpSecret" placeholder="CorpSecret" />
</el-form-item>
<el-form-item label="AgentID">
<el-input v-model="dialogForm.agentId" placeholder="AgentID" />
</el-form-item>
@@ -77,26 +92,61 @@
</template>
</el-dialog>
<el-drawer v-model="drawerVisible" title="渠道主体详情" size="420px">
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">{{ currentSubject.id }}</el-descriptions-item>
<el-descriptions-item label="渠道">{{ channelLabel(currentSubject.channelType) }}</el-descriptions-item>
<el-descriptions-item label="主体编码">{{ currentSubject.subjectCode }}</el-descriptions-item>
<el-descriptions-item label="主体名称">{{ currentSubject.subjectName }}</el-descriptions-item>
<el-descriptions-item label="CorpID">{{ mask(currentSubject.corpId) }}</el-descriptions-item>
<el-descriptions-item label="CorpSecret">{{ mask(currentSubject.corpSecret) }}</el-descriptions-item>
<el-descriptions-item label="AgentID">{{ mask(currentSubject.agentId) }}</el-descriptions-item>
<el-descriptions-item label="AgentSecret">{{ mask(currentSubject.agentSecret) }}</el-descriptions-item>
<el-descriptions-item label="会话存档">{{ currentSubject.sessionArchiveEnabled === 1 ? '开启' : '关闭' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ currentSubject.status === 1 ? '启用' : '禁用' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ currentSubject.createTime }}</el-descriptions-item>
</el-descriptions>
<!-- 详情/编辑右侧抽屉 -->
<el-drawer
v-model="drawerVisible"
:title="`渠道主体详情${currentSubject.subjectName ? ' - ' + currentSubject.subjectName : ''}`"
size="480px"
destroy-on-close
>
<el-form :model="dialogForm" label-width="120px">
<el-form-item label="渠道">
<el-select v-model="dialogForm.channelType" placeholder="选择渠道" style="width: 100%">
<el-option label="企微" value="wecom" />
<el-option label="钉钉" value="dingtalk" />
<el-option label="飞书" value="feishu" />
</el-select>
</el-form-item>
<el-form-item label="主体编码">
<el-input v-model="dialogForm.subjectCode" placeholder="主体编码" />
</el-form-item>
<el-form-item label="主体名称">
<el-input v-model="dialogForm.subjectName" placeholder="主体名称" />
</el-form-item>
<el-form-item label="CorpID">
<el-input v-model="dialogForm.corpId" placeholder="CorpID" />
</el-form-item>
<el-form-item label="AgentID">
<el-input v-model="dialogForm.agentId" placeholder="AgentID" />
</el-form-item>
<el-form-item label="AgentSecret">
<el-input v-model="dialogForm.agentSecret" placeholder="AgentSecret" />
</el-form-item>
<el-form-item label="会话存档">
<el-switch v-model="dialogForm.sessionArchiveEnabled" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="会话存档私钥">
<el-input v-model="dialogForm.sessionArchivePrivateKey" type="textarea" :rows="3" placeholder="会话存档私钥" />
</el-form-item>
</el-form>
<template #footer>
<div class="drawer-footer">
<div class="drawer-actions-left">
<el-button v-if="currentSubject.channelType === 'wecom'" type="warning" plain @click="handleSyncEnterpriseContacts(currentSubject)">同步企业通讯录</el-button>
<el-button type="danger" plain @click="handleDrawerDelete">删除</el-button>
</div>
<div class="drawer-actions">
<el-button @click="drawerVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</div>
</div>
</template>
</el-drawer>
</div>
</PageCard>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
createChannelSubject,
@@ -105,6 +155,9 @@ import {
deleteChannelSubject,
updateChannelSubjectStatus,
} from '@/api/channelSubject'
import { syncSubjectEnterpriseContacts } from '@/api/syncTask'
import { getSyncTask } from '@/api/syncTask'
import PageCard from '@/components/PageCard.vue'
const subjects = ref<any[]>([])
const dialogVisible = ref(false)
@@ -116,7 +169,6 @@ const dialogForm = reactive({
subjectCode: '',
subjectName: '',
corpId: '',
corpSecret: '',
agentId: '',
agentSecret: '',
sessionArchiveEnabled: 0,
@@ -126,12 +178,35 @@ const dialogForm = reactive({
const drawerVisible = ref(false)
const currentSubject = ref<any>({})
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const filteredSubjects = computed(() => {
const kw = searchKeyword.value.trim().toLowerCase()
if (!kw) return subjects.value
return subjects.value.filter(
(s) =>
(s.subjectName || '').toLowerCase().includes(kw) ||
(s.subjectCode || '').toLowerCase().includes(kw) ||
(s.corpId || '').toLowerCase().includes(kw)
)
})
const pagedSubjects = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredSubjects.value.slice(start, start + pageSize.value)
})
function handleSearch() {
currentPage.value = 1
}
function resetDialogForm() {
dialogForm.channelType = 'wecom'
dialogForm.subjectCode = ''
dialogForm.subjectName = ''
dialogForm.corpId = ''
dialogForm.corpSecret = ''
dialogForm.agentId = ''
dialogForm.agentSecret = ''
dialogForm.sessionArchiveEnabled = 0
@@ -143,7 +218,6 @@ function fillDialogForm(row: any) {
dialogForm.subjectCode = row.subjectCode ?? ''
dialogForm.subjectName = row.subjectName ?? ''
dialogForm.corpId = row.corpId ?? ''
dialogForm.corpSecret = row.corpSecret ?? ''
dialogForm.agentId = row.agentId ?? ''
dialogForm.agentSecret = row.agentSecret ?? ''
dialogForm.sessionArchiveEnabled = row.sessionArchiveEnabled ?? 0
@@ -157,15 +231,11 @@ function openCreateDialog() {
dialogVisible.value = true
}
function openEditDialog(row: any) {
function handleRowClick(row: any) {
isEdit.value = true
editingId.value = row.id
fillDialogForm(row)
dialogVisible.value = true
}
function openDetail(row: any) {
currentSubject.value = { ...row }
fillDialogForm(row)
drawerVisible.value = true
}
@@ -188,6 +258,7 @@ async function handleSave() {
ElMessage.success('创建成功')
}
dialogVisible.value = false
drawerVisible.value = false
await loadSubjects()
} catch (e: any) {
ElMessage.error(e.message)
@@ -207,6 +278,12 @@ async function handleDelete(row: any) {
}
}
async function handleDrawerDelete() {
if (!currentSubject.value?.id) return
await handleDelete(currentSubject.value)
drawerVisible.value = false
}
async function handleStatusChange(row: any, val: any) {
try {
await updateChannelSubjectStatus(row.id, { status: val })
@@ -218,6 +295,42 @@ async function handleStatusChange(row: any, val: any) {
}
}
async function pollTask(taskId: number, label: string) {
const maxAttempts = 60
for (let i = 0; i < maxAttempts; i++) {
await new Promise((resolve) => setTimeout(resolve, 2000))
try {
const res: any = await getSyncTask(taskId)
const task = res.data
if (!task) continue
if (task.taskStatus === 2) {
ElMessage.success(`${label}同步完成,共 ${task.syncedCount ?? 0}`)
return
}
if (task.taskStatus === 3) {
ElMessage.error(`${label}同步失败:${task.errorMessage || '未知错误'}`)
return
}
} catch (e: any) {
// ignore polling error
}
}
ElMessage.warning(`${label}同步时间较长,请稍后到列表查看`)
}
async function handleSyncEnterpriseContacts(row: any) {
try {
const res: any = await syncSubjectEnterpriseContacts(row.id)
const taskId = res.data?.id
if (taskId) {
ElMessage.info('企业通讯录同步任务已提交')
await pollTask(taskId, '企业通讯录')
}
} catch (e: any) {
ElMessage.error(e.message)
}
}
function channelLabel(type: string) {
const map: Record<string, string> = {
wecom: '企微',
@@ -237,7 +350,19 @@ onMounted(loadSubjects)
</script>
<style scoped>
.toolbar {
margin-bottom: 16px;
.search-input {
width: 280px;
}
.drawer-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.drawer-actions {
display: flex;
align-items: center;
gap: 12px;
}
</style>