refactor(tenant): 移除租户数据源及数据源级隔离适配代码

This commit is contained in:
2025-07-15 21:41:30 +08:00
parent 6a96eee9e9
commit 112c27e49c
11 changed files with 14 additions and 519 deletions

View File

@@ -24,16 +24,6 @@ export function listRoleDict(query?: { name: string, status: number }) {
return http.get<LabelValueState[]>(`${BASE_URL}/dict/role`, query)
}
/** @desc 查询租户套餐列表 */
export function listTenantPackageDict(query?: { description: string, status: number }) {
return http.get<LabelValueState[]>(`${BASE_URL}/dict/package`, query)
}
/** @desc 查询租户数据源列表 */
export function listTenantDatasourceDict(query?: { description: string }) {
return http.get<LabelValueState[]>(`${BASE_URL}/dict/datasource`, query)
}
/** @desc 查询字典列表 */
export function listCommonDict(code: string) {
return http.get<LabelValueState[]>(`${BASE_URL}/dict/${code}`)

View File

@@ -0,0 +1,9 @@
import http from '@/utils/http'
import type { LabelValueState } from '@/types/global'
const BASE_URL = '/tenant/common'
/** @desc 查询租户套餐列表 */
export function listTenantPackageDict(query?: { description: string, status: number }) {
return http.get<LabelValueState[]>(`${BASE_URL}/dict/package`, query)
}

View File

@@ -1,36 +0,0 @@
import type * as T from './type'
import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/tenant/datasource'
/** @desc 查询租户数据源列表 */
export function listTenantDatasource(query: T.TenantDatasourcePageQuery) {
return http.get<PageRes<T.TenantDatasourceResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询租户数据源详情 */
export function getTenantDatasource(id: string) {
return http.get<T.TenantDatasourceResp>(`${BASE_URL}/${id}`)
}
/** @desc 新增租户数据源 */
export function addTenantDatasource(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 修改租户数据源 */
export function updateTenantDatasource(data: any, id: string) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 删除租户数据源 */
export function deleteTenantDatasource(id: string) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 测试租户数据源连接 */
export function testTenantDatasourceConnection(id: string) {
return http.post(`${BASE_URL}/${id}/test/connection`)
}

View File

@@ -1,2 +1,3 @@
export * from './common'
export * from './management'
export * from './package'
export * from './datasource'

View File

@@ -5,11 +5,9 @@ export interface TenantResp {
code: string
domain: string
expireTime: string
isolationLevel: number
description: number
status: string
packageId: string
datasourceId: string
createUser: string
createTime: string
updateUser: string
@@ -17,7 +15,6 @@ export interface TenantResp {
createUserString: string
updateUserString: string
packageName: string
datasourceName: string
}
export interface TenantQuery {
description?: string
@@ -49,25 +46,3 @@ export interface TenantPackageQuery {
sort: Array<string>
}
export interface TenantPackagePageQuery extends TenantPackageQuery, PageQuery {}
/** 租户数据源 */
export interface TenantDatasourceResp {
id: string
name: string
databaseType: string
host: string
port: string
username: string
description: string
createUser: string
createTime: string
updateUser: string
updateTime: string
createUserString: string
updateUserString: string
}
export interface TenantDatasourceQuery {
description?: string
sort: Array<string>
}
export interface TenantDatasourcePageQuery extends TenantDatasourceQuery, PageQuery {}

View File

@@ -1,187 +0,0 @@
<template>
<a-modal
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 500 ? 500 : '100%'"
draggable
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :columns="columns">
<template #password>
<a-input-password
v-model="form.password"
:placeholder="!isUpdate ? '请输入密码' : '保持密码为空将不更改密码'"
/>
</template>
</GiForm>
</a-modal>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addTenantDatasource, getTenantDatasource, updateTenantDatasource } from '@/apis/tenant/datasource'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
import { encryptByRsa } from '@/utils/encrypt'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改数据源' : '新增数据源'))
const formRef = ref<InstanceType<typeof GiForm>>()
const { datasource_database_type_enum } = useDict('datasource_database_type_enum')
const [form, resetForm] = useResetReactive({
databaseType: 1,
port: 3306,
})
const columns: ColumnItem[] = reactive([
{
label: '名称',
field: 'name',
type: 'input',
span: 24,
required: true,
props: {
maxLength: 30,
},
},
{
label: '数据库类型',
field: 'databaseType',
type: 'radio-group',
span: 24,
required: true,
props: {
type: 'button',
options: datasource_database_type_enum,
},
},
{
label: '主机',
field: 'host',
type: 'input',
span: 24,
required: true,
props: {
maxLength: 128,
},
},
{
label: '端口',
field: 'port',
type: 'input-number',
span: 24,
required: true,
props: {
min: 0,
max: 65535,
},
},
{
label: '用户名',
field: 'username',
type: 'input',
span: 24,
required: true,
props: {
maxLength: 128,
},
},
{
label: '密码',
field: 'password',
type: 'input-password',
span: 24,
required: true,
props: {
maxLength: 128,
},
show: () => {
return !isUpdate.value
},
},
{
label: '密码',
field: 'password',
type: 'input-password',
span: 24,
props: {
maxLength: 128,
},
show: () => {
return isUpdate.value
},
},
{
label: '描述',
field: 'description',
type: 'textarea',
span: 24,
},
])
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
}
// 保存
const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
if (isUpdate.value) {
await updateTenantDatasource({
...form,
password: form.password === undefined ? undefined : encryptByRsa(form.password),
}, dataId.value)
Message.success('修改成功')
} else {
await addTenantDatasource({
...form,
password: encryptByRsa(form.password),
})
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
return false
}
}
// 新增
const onAdd = async () => {
reset()
dataId.value = ''
visible.value = true
}
// 修改
const onUpdate = async (id: string) => {
reset()
dataId.value = id
const { data } = await getTenantDatasource(id)
Object.assign(form, data)
form.password = undefined
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>
<style scoped lang="scss"></style>

View File

@@ -1,47 +0,0 @@
<template>
<a-drawer v-model:visible="visible" title="数据源详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-descriptions :column="2" size="large" class="general-description">
<a-descriptions-item label="ID">{{ dataDetail?.id }}</a-descriptions-item>
<a-descriptions-item label="名称">{{ dataDetail?.name }}</a-descriptions-item>
<a-descriptions-item label="数据库类型"><GiCellTag :value="dataDetail?.databaseType" :dict="datasource_database_type_enum" /></a-descriptions-item>
<a-descriptions-item label="主机">{{ dataDetail?.host }}</a-descriptions-item>
<a-descriptions-item label="端口">{{ dataDetail?.port }}</a-descriptions-item>
<a-descriptions-item label="用户名">{{ dataDetail?.username }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ dataDetail?.createUserString }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ dataDetail?.createTime }}</a-descriptions-item>
<a-descriptions-item label="修改人">{{ dataDetail?.updateUserString }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ dataDetail?.updateTime }}</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">{{ dataDetail?.description }}</a-descriptions-item>
</a-descriptions>
</a-drawer>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { type TenantDatasourceResp, getTenantDatasource as getDetail } from '@/apis/tenant/datasource'
import { useDict } from '@/hooks/app'
const { width } = useWindowSize()
const dataId = ref('')
const dataDetail = ref<TenantDatasourceResp>()
const visible = ref(false)
const { datasource_database_type_enum } = useDict('datasource_database_type_enum')
// 查询详情
const getDataDetail = async () => {
const { data } = await getDetail(dataId.value)
dataDetail.value = data
}
// 打开
const onOpen = async (id: string) => {
dataId.value = id
await getDataDetail()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss"></style>

View File

@@ -1,161 +0,0 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.description" placeholder="搜索名称/描述" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['tenant:datasource:create']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
</template>
<template #databaseType="{ record }">
<GiCellTag :value="record.databaseType" :dict="datasource_database_type_enum" />
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['tenant:datasource:get']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['tenant:datasource:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-dropdown>
<a-button v-if="has.hasPermOr(['tenant:datasource:testConnection', 'tenant:datasource:delete'])" type="text" size="mini" title="更多">
<template #icon>
<icon-more :size="16" />
</template>
</a-button>
<template #content>
<a-doption v-permission="['tenant:datasource:testConnection']" title="测试连接" @click="onTestConnection(record)">测试连接</a-doption>
<a-doption
v-permission="['tenant:datasource:delete']"
:disabled="record.disabled"
:title="record.disabled ? '禁止删除' : '删除'"
@click="onDelete(record)"
>
删除
</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</GiTable>
<AddModal ref="AddModalRef" @save-success="search" />
<DetailDrawer ref="DetailDrawerRef" />
</GiPageLayout>
</template>
<script setup lang="ts">
import { Message, type TableInstance } from '@arco-design/web-vue'
import AddModal from './AddModal.vue'
import DetailDrawer from './DetailDrawer.vue'
import {
type TenantDatasourceQuery,
type TenantDatasourceResp,
deleteTenantDatasource,
listTenantDatasource,
testTenantDatasourceConnection,
} from '@/apis/tenant/datasource'
import { useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import { useDict } from '@/hooks/app'
defineOptions({ name: 'TenantDatasource' })
const { datasource_database_type_enum } = useDict('datasource_database_type_enum')
const queryForm = reactive<TenantDatasourceQuery>({
description: undefined,
sort: ['createTime,desc'],
})
const {
tableData: dataList,
loading,
pagination,
search,
handleDelete,
} = useTable((page) => listTenantDatasource({ ...queryForm, ...page }), { immediate: true })
const columns: TableInstance['columns'] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
fixed: !isMobile() ? 'left' : undefined,
},
{ title: '名称', dataIndex: 'name', slotName: 'name', ellipsis: true, tooltip: true, fixed: !isMobile() ? 'left' : undefined },
{ title: '数据库类型', dataIndex: 'databaseType', slotName: 'databaseType', align: 'center' },
{ title: '主机', dataIndex: 'host', slotName: 'host', align: 'center' },
{ title: '端口', dataIndex: 'port', slotName: 'port', align: 'center' },
{ title: '用户名', dataIndex: 'username', slotName: 'username', align: 'center' },
{ title: '描述', dataIndex: 'description', ellipsis: true, tooltip: true },
{ title: '创建人', dataIndex: 'createUserString', ellipsis: true, tooltip: true, show: false },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{ title: '修改人', dataIndex: 'updateUserString', ellipsis: true, tooltip: true, show: false },
{ title: '修改时间', dataIndex: 'updateTime', width: 180, show: false },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['tenant:datasource:get', 'tenant:datasource:update', 'tenant:datasource:delete']),
},
]
// 重置
const reset = () => {
queryForm.description = undefined
search()
}
// 删除
const onDelete = (record: TenantDatasourceResp) => {
return handleDelete(() => deleteTenantDatasource(record.id), {
content: `是否确定删除数据源「${record.name}」?`,
showModal: true,
})
}
// 测试连接
const onTestConnection = async (record: TenantDatasourceResp) => {
await testTenantDatasourceConnection(record.id)
Message.success('测试连接成功')
}
const AddModalRef = ref<InstanceType<typeof AddModal>>()
// 新增
const onAdd = () => {
AddModalRef.value?.onAdd()
}
// 修改
const onUpdate = (record: TenantDatasourceResp) => {
AddModalRef.value?.onUpdate(record.id)
}
const DetailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
// 详情
const onDetail = (record: TenantDatasourceResp) => {
DetailDrawerRef.value?.onOpen(record.id)
}
</script>
<style scoped lang="scss"></style>

View File

@@ -20,8 +20,7 @@ import { addTenant, getTenant, updateTenant } from '@/apis/tenant/management'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { encryptByRsa } from '@/utils/encrypt'
import { listTenantDatasourceDict, listTenantPackageDict } from '@/apis'
import { useDict } from '@/hooks/app'
import { listTenantPackageDict } from '@/apis/tenant'
import type { LabelValueState } from '@/types/global'
const emit = defineEmits<{
@@ -35,20 +34,13 @@ const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改租户' : '新增租户'))
const formRef = ref<InstanceType<typeof GiForm>>()
const { tenant_isolation_level_enum } = useDict('tenant_isolation_level_enum')
const packageList = ref<LabelValueState[]>([])
const datasourceList = ref<LabelValueState[]>([])
// 查询租户套餐
const getPackageList = async () => {
const { data } = await listTenantPackageDict()
packageList.value = data
}
// 查询租户数据源
const getDatasourceList = async () => {
const { data } = await listTenantDatasourceDict()
datasourceList.value = data
}
const [form, resetForm] = useResetReactive({
isolationLevel: 1,
@@ -120,33 +112,6 @@ const columns: ColumnItem[] = reactive([
return isUpdate.value
},
},
{
label: '隔离级别',
field: 'isolationLevel',
type: 'radio-group',
span: 24,
required: true,
props: {
type: 'button',
options: tenant_isolation_level_enum,
},
hide: () => {
return isUpdate.value
},
},
{
label: '数据源',
field: 'dbConnectId',
type: 'select',
span: 24,
required: true,
hide: () => {
return isUpdate.value || form.isolationLevel !== 2
},
props: {
options: datasourceList,
},
},
{
label: '描述',
field: 'description',
@@ -173,7 +138,6 @@ const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
getPackageList()
getDatasourceList()
}
// 保存

View File

@@ -15,7 +15,6 @@
</a-descriptions-item>
<a-descriptions-item label="过期时间">
<span v-if="!dataDetail?.expireTime">
<icon-check-circle-fill class="success" />
<span>永不过期</span>
</span>
<span v-else>{{ dataDetail?.expireTime }}</span>
@@ -24,8 +23,6 @@
<a v-if="dataDetail?.domain" style="color: rgb(var(--arcoblue-7))">{{ dataDetail?.domain }}</a>
<span v-else style="color: red" class="text-red-4">未设置</span>
</a-descriptions-item>
<a-descriptions-item label="隔离级别"><GiCellTag :value="dataDetail?.isolationLevel" :dict="tenant_isolation_level_enum" /></a-descriptions-item>
<a-descriptions-item label="数据源">{{ dataDetail?.datasourceName }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ dataDetail?.createUserString }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ dataDetail?.createTime }}</a-descriptions-item>
<a-descriptions-item label="修改人">{{ dataDetail?.updateUserString }}</a-descriptions-item>
@@ -38,7 +35,7 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { type TenantResp, getTenant as getDetail } from '@/apis/tenant/management'
import { useDict, useMenu } from '@/hooks/app'
import { useMenu } from '@/hooks/app'
const { menuList, getTenantPackageMenuList } = useMenu()
const { width } = useWindowSize()
@@ -46,7 +43,6 @@ const { width } = useWindowSize()
const dataId = ref('')
const dataDetail = ref<TenantResp>()
const visible = ref(false)
const { tenant_isolation_level_enum } = useDict('tenant_isolation_level_enum')
// 查询详情
const getDataDetail = async () => {

View File

@@ -41,7 +41,6 @@
</template>
<template #expireTime="{ record }">
<span v-if="!record.expireTime">
<icon-check-circle-fill class="success" />
<span>永不过期</span>
</span>
<span v-else>{{ record.expireTime }}</span>
@@ -50,9 +49,6 @@
<a v-if="record.domain" style="color: rgb(var(--arcoblue-7))" :href="record.domain">{{ record.domain }}</a>
<span v-else style="color: red" class="text-red-4">未设置</span>
</template>
<template #isolationLevel="{ record }">
<GiCellTag :value="record.isolationLevel" :dict="tenant_isolation_level_enum" />
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['tenant:management:get']" title="详情" @click="onDetail(record)">详情</a-link>
@@ -94,14 +90,11 @@ import { type TenantQuery, type TenantResp, deleteTenant, listTenant } from '@/a
import { useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import { useDict } from '@/hooks/app'
import { listTenantPackageDict } from '@/apis'
import { listTenantPackageDict } from '@/apis/tenant'
import type { LabelValueState } from '@/types/global'
defineOptions({ name: 'TenantManagement' })
const { tenant_isolation_level_enum } = useDict('tenant_isolation_level_enum')
const queryForm = reactive<TenantQuery>({
description: undefined,
packageId: undefined,
@@ -130,8 +123,6 @@ const columns: TableInstance['columns'] = [
{ title: '状态', dataIndex: 'status', slotName: 'status' },
{ title: '过期时间', dataIndex: 'expireTime', slotName: 'expireTime', width: 180 },
{ title: '域名', dataIndex: 'domain', slotName: 'domain', ellipsis: true, tooltip: true },
{ title: '隔离级别', dataIndex: 'isolationLevel', slotName: 'isolationLevel', align: 'center' },
{ title: '数据源', dataIndex: 'datasourceName', slotName: 'datasourceName', align: 'center' },
{ title: '描述', dataIndex: 'description', ellipsis: true, tooltip: true },
{ title: '创建人', dataIndex: 'createUserString', ellipsis: true, tooltip: true, show: false },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },