😎1、租户管理增加指定库定位器选择(Id隔离时可以指定库) 2、升级依赖

This commit is contained in:
zuohuaijun 2025-07-27 15:12:45 +08:00
parent ce565879ee
commit 2a7cd8465b
8 changed files with 213 additions and 84 deletions

View File

@ -28,9 +28,9 @@
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" Aliases="BouncyCastleV2" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.0.7" />
<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.7.104" />
<PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.7.104" />
<PackageReference Include="Furion.Pure" Version="4.9.7.104" />
<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.7.105" />
<PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.7.105" />
<PackageReference Include="Furion.Pure" Version="4.9.7.105" />
<PackageReference Include="Hardware.Info" Version="101.0.1.1" />
<PackageReference Include="Hashids.net" Version="1.7.0" />
<PackageReference Include="IPTools.China" Version="1.6.0" />
@ -52,7 +52,7 @@
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
<PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="3.11.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.13.0" />
<PackageReference Include="SqlSugar.MongoDbCore" Version="5.1.4.232" />
<PackageReference Include="SqlSugar.MongoDbCore" Version="5.1.4.234" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.198" />
<PackageReference Include="SSH.NET" Version="2025.0.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.6" />
@ -60,15 +60,15 @@
<PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1273" />
<PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
<PackageReference Include="microsoft.semantickernel" Version="1.60.0" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Core" Version="1.60.0" />
<PackageReference Include="microsoft.semantickernel" Version="1.61.0" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Core" Version="1.61.0" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Amazon" Version="1.56.0-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.54.0-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.HuggingFace" Version="1.56.0-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.54.0-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.54.0-preview" />
<PackageReference Include="Microsoft.SemanticKernel.PromptTemplates.Handlebars" Version="1.60.0" />
<PackageReference Include="Microsoft.SemanticKernel.Yaml" Version="1.60.0" />
<PackageReference Include="Microsoft.SemanticKernel.PromptTemplates.Handlebars" Version="1.61.0" />
<PackageReference Include="Microsoft.SemanticKernel.Yaml" Version="1.61.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">

View File

@ -6,9 +6,28 @@
namespace Admin.NET.Core.Service;
/// <summary>
/// 数据库输出
/// </summary>
public class DbOutput
{
/// <summary>
/// 标识
/// </summary>
public string ConfigId { get; set; }
/// <summary>
/// 名称
/// </summary>
public string DbName { get; set; }
/// <summary>
/// 类型
/// </summary>
public SqlSugar.DbType DbType { get; set; }
/// <summary>
/// 连接字符串
/// </summary>
public string Connection { get; set; }
}

View File

@ -21,7 +21,6 @@ public class SysTenantService : IDynamicApiController, ITransient
private readonly SqlSugarRepository<SysUserExtOrg> _sysUserExtOrgRep;
private readonly SqlSugarRepository<SysRoleMenu> _sysRoleMenuRep;
private readonly SqlSugarRepository<SysUserRole> _userRoleRep;
private readonly SqlSugarRepository<SysFile> _fileRep;
private readonly SysUserRoleService _sysUserRoleService;
private readonly SysRoleService _sysRoleService;
private readonly SysRoleMenuService _sysRoleMenuService;
@ -39,7 +38,6 @@ public class SysTenantService : IDynamicApiController, ITransient
SqlSugarRepository<SysUserExtOrg> sysUserExtOrgRep,
SqlSugarRepository<SysRoleMenu> sysRoleMenuRep,
SqlSugarRepository<SysUserRole> userRoleRep,
SqlSugarRepository<SysFile> fileRep,
SysUserRoleService sysUserRoleService,
SysRoleService sysRoleService,
SysRoleMenuService sysRoleMenuService,
@ -57,7 +55,6 @@ public class SysTenantService : IDynamicApiController, ITransient
_sysUserExtOrgRep = sysUserExtOrgRep;
_sysRoleMenuRep = sysRoleMenuRep;
_userRoleRep = userRoleRep;
_fileRep = fileRep;
_sysUserRoleService = sysUserRoleService;
_sysRoleService = sysRoleService;
_sysRoleMenuService = sysRoleMenuService;
@ -111,16 +108,6 @@ public class SysTenantService : IDynamicApiController, ITransient
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 获取库隔离的租户列表
/// </summary>
/// <returns></returns>
[NonAction]
public async Task<List<SysTenant>> GetTenantDbList()
{
return await _sysTenantRep.GetListAsync(u => u.TenantType == TenantTypeEnum.Db && u.Status == StatusEnum.Enable);
}
/// <summary>
/// 增加租户 🔖
/// </summary>
@ -130,35 +117,24 @@ public class SysTenantService : IDynamicApiController, ITransient
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("增加租户")]
public async Task AddTenant(AddTenantInput input)
{
var isExist = await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1300);
{
if (string.IsNullOrWhiteSpace(input.Connection))
throw Oops.Oh(ErrorCodeEnum.Z1004);
isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.AdminAccount);
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1301);
if (await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name))
throw Oops.Oh(ErrorCodeEnum.D1300);
if (await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.AdminAccount))
throw Oops.Oh(ErrorCodeEnum.D1301);
// 从库配置判断
if (input.TenantType == TenantTypeEnum.Db && !string.IsNullOrWhiteSpace(input.SlaveConnections) && !JSON.IsValid(input.SlaveConnections, true))
throw Oops.Oh(ErrorCodeEnum.D1302);
switch (input.TenantType)
{
// Id隔离时设置与主库一致
case TenantTypeEnum.Id:
var config = _sysTenantRep.AsSugarClient().CurrentConnectionConfig;
input.DbType = config.DbType;
input.Connection = config.ConnectionString;
break;
case TenantTypeEnum.Db:
if (string.IsNullOrWhiteSpace(input.Connection))
throw Oops.Oh(ErrorCodeEnum.Z1004);
break;
default:
throw Oops.Oh(ErrorCodeEnum.D3004);
}
throw Oops.Oh(ErrorCodeEnum.D1302);
// 以租户Id作为库标识
input.Id = YitIdHelper.NextId();
input.ConfigId = input.Id.ToString();
var tenant = input.Adapt<TenantOutput>();
await _sysTenantRep.InsertAsync(tenant);
await InitNewTenant(tenant);
@ -324,31 +300,15 @@ public class SysTenantService : IDynamicApiController, ITransient
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新租户")]
public async Task UpdateTenant(UpdateTenantInput input)
{
var isExist = await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.OrgId);
if (isExist)
{
if (string.IsNullOrWhiteSpace(input.Connection))
throw Oops.Oh(ErrorCodeEnum.Z1004);
if (await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.OrgId))
throw Oops.Oh(ErrorCodeEnum.D1300);
isExist = await _sysUserRep.IsAnyAsync(u => u.Account == input.AdminAccount && u.Id != input.UserId);
if (isExist)
throw Oops.Oh(ErrorCodeEnum.D1301);
// Id隔离时设置与主库一致
switch (input.TenantType)
{
case TenantTypeEnum.Id:
var config = _sysTenantRep.AsSugarClient().CurrentConnectionConfig;
input.DbType = config.DbType;
input.Connection = config.ConnectionString;
break;
case TenantTypeEnum.Db:
if (string.IsNullOrWhiteSpace(input.Connection))
throw Oops.Oh(ErrorCodeEnum.Z1004);
break;
default:
throw Oops.Oh(ErrorCodeEnum.D3004);
}
if (await _sysUserRep.IsAnyAsync(u => u.Account == input.AdminAccount && u.Id != input.UserId))
throw Oops.Oh(ErrorCodeEnum.D1301);
// 从库配置判断
if (input.TenantType == TenantTypeEnum.Db && !string.IsNullOrWhiteSpace(input.SlaveConnections) && !JSON.IsValid(input.SlaveConnections, true))
@ -514,6 +474,35 @@ public class SysTenantService : IDynamicApiController, ITransient
public async Task<List<SysUser>> UserList(TenantIdInput input)
{
return await _sysUserRep.AsQueryable().ClearFilter().Where(u => u.TenantId == input.TenantId).ToListAsync();
}
/// <summary>
/// 获取所有租户数据库列表 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取所有租户数据库列表")]
public async Task<List<DbOutput>> GetTenantDbList()
{
var tenantDbList = await _sysTenantRep.AsQueryable().ClearFilter()
.LeftJoin<SysOrg>((u, a) => u.OrgId == a.Id)
//.GroupBy(u => new { u.DbType, u.Connection })
.Where(u => u.Status == StatusEnum.Enable)
.OrderBy(u => u.ConfigId)
.Select((u, a) => new DbOutput()
{
DbName = a.Name,
ConfigId = u.ConfigId.ToString(),
DbType = u.DbType,
Connection = u.Connection
}).ToListAsync();
//// 获取数据库名称
//foreach (var tenantDb in tenantDbList)
//{
// tenantDb.DbName = _sysTenantRep.AsTenant().GetConnectionScope(tenantDb.ConfigId).Ado.Connection.Database;
//}
return tenantDbList;
}
/// <summary>

View File

@ -2,7 +2,7 @@
"name": "admin.net.pro",
"type": "module",
"version": "2.4.33",
"lastBuildTime": "2025.07.24",
"lastBuildTime": "2025.07.27",
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
"author": "zuohuaijun",
"license": "MIT",
@ -26,7 +26,7 @@
"@vue-office/pdf": "^2.0.10",
"@vueuse/core": "^13.5.0",
"@vxe-ui/plugin-export-xlsx": "^4.2.3",
"@vxe-ui/plugin-render-element": "^4.0.13",
"@vxe-ui/plugin-render-element": "^4.0.15",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"animate.css": "^4.1.1",
@ -74,7 +74,7 @@
"vue-clipboard3": "^2.0.0",
"vue-demi": "0.14.10",
"vue-draggable-plus": "^0.6.0",
"vue-element-plus-x": "^1.3.11-beta",
"vue-element-plus-x": "^1.3.0",
"vue-grid-layout": "3.0.0-beta1",
"vue-i18n": "^11.1.11",
"vue-json-pretty": "^2.5.0",
@ -82,8 +82,8 @@
"vue-router": "^4.5.1",
"vue-signature-pad": "^3.0.2",
"vue3-tree-org": "^4.2.2",
"vxe-pc-ui": "^4.7.24",
"vxe-table": "^4.14.6",
"vxe-pc-ui": "^4.7.28",
"vxe-table": "^4.14.7",
"xe-utils": "^3.7.8",
"xlsx-js-style": "^1.2.0"
},
@ -99,8 +99,8 @@
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/compiler-sfc": "^3.5.18",
"code-inspector-plugin": "^0.20.17",
"eslint": "^9.31.0",
"code-inspector-plugin": "^1.0.0",
"eslint": "^9.32.0",
"eslint-plugin-vue": "^10.3.0",
"globals": "^16.3.0",
"less": "^4.4.0",

View File

@ -19,6 +19,7 @@ import { Configuration } from '../configuration';
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
import { AddTenantInput } from '../models';
import { AdminNETResultInt32 } from '../models';
import { AdminNETResultListDbOutput } from '../models';
import { AdminNETResultListInt64 } from '../models';
import { AdminNETResultListSysUser } from '../models';
import { AdminNETResultObject } from '../models';
@ -611,6 +612,49 @@ export const SysTenantApiAxiosParamCreator = function (configuration?: Configura
options: localVarRequestOptions,
};
},
/**
*
* @summary 🔖
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysTenantTenantDbListGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysTenant/tenantDbList`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication Bearer required
// http bearer authentication required
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
query.set(key, localVarQueryParameter[key]);
}
for (const key in options.params) {
query.set(key, options.params[key]);
}
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
options: localVarRequestOptions,
};
},
/**
*
* @summary 🔖
@ -935,6 +979,19 @@ export const SysTenantApiFp = function(configuration?: Configuration) {
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 🔖
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysTenantTenantDbListGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultListDbOutput>>> {
const localVarAxiosArgs = await SysTenantApiAxiosParamCreator(configuration).apiSysTenantTenantDbListGet(options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 🔖
@ -1105,6 +1162,15 @@ export const SysTenantApiFactory = function (configuration?: Configuration, base
async apiSysTenantSysInfoTenantIdGet(tenantId: number, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultObject>> {
return SysTenantApiFp(configuration).apiSysTenantSysInfoTenantIdGet(tenantId, options).then((request) => request(axios, basePath));
},
/**
*
* @summary 🔖
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysTenantTenantDbListGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultListDbOutput>> {
return SysTenantApiFp(configuration).apiSysTenantTenantDbListGet(options).then((request) => request(axios, basePath));
},
/**
*
* @summary 🔖
@ -1276,6 +1342,16 @@ export class SysTenantApi extends BaseAPI {
public async apiSysTenantSysInfoTenantIdGet(tenantId: number, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultObject>> {
return SysTenantApiFp(this.configuration).apiSysTenantSysInfoTenantIdGet(tenantId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 🔖
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysTenantApi
*/
public async apiSysTenantTenantDbListGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultListDbOutput>> {
return SysTenantApiFp(this.configuration).apiSysTenantTenantDbListGet(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 🔖

View File

@ -12,8 +12,9 @@
* Do not edit the class manually.
*/
import { DbType } from './db-type';
/**
*
*
*
* @export
* @interface DbOutput
@ -21,14 +22,32 @@
export interface DbOutput {
/**
*
*
* @type {string}
* @memberof DbOutput
*/
configId?: string | null;
/**
*
*
* @type {string}
* @memberof DbOutput
*/
dbName?: string | null;
/**
* @type {DbType}
* @memberof DbOutput
*/
dbType?: DbType;
/**
*
*
* @type {string}
* @memberof DbOutput
*/
connection?: string | null;
}

View File

@ -53,7 +53,7 @@
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="绑定域名" :rules="[{ required: true, message: '绑定域名不能为空', trigger: 'blur' }]">
<el-form-item label="绑定域名">
<el-input v-model="state.ruleForm.host" placeholder="例如https://gitee.com" clearable />
</el-form-item>
</el-col>
@ -67,6 +67,16 @@
<el-input-number v-model="state.ruleForm.orderNo" placeholder="排序" class="w100" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="库定位器">
<el-select v-model="state.ruleForm.configId" placeholder="库定位器" filterable @change="dbChanged()" class="w100">
<el-option v-for="item in state.tenantDbData" :key="item.dbName" :label="item.dbName" :value="item.configId">
<span style="float: left">{{ item.dbName }}</span>
<span style="float: right; color: var(--el-text-color-secondary)"> {{ item.configId }} </span>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="数据库类型">
<el-select v-model="state.ruleForm.dbType" placeholder="数据库类型" clearable class="w100" :disabled="state.ruleForm.tenantType == 0 && state.ruleForm.tenantType != undefined">
@ -156,7 +166,7 @@ import { reactive, ref } from 'vue';
import { getAPI } from '/@/utils/axios-utils';
import { SysOrgApi, SysTenantApi } from '/@/api-services/api';
import { SysOrg, UpdateTenantInput } from '/@/api-services/models';
import { DbOutput, SysOrg, UpdateTenantInput } from '/@/api-services/models';
const props = defineProps({
title: String,
@ -167,6 +177,7 @@ const state = reactive({
isShowDialog: false,
ruleForm: {} as UpdateTenantInput,
orgData: [] as Array<SysOrg>,
tenantDbData: [] as Array<DbOutput>,
});
//
const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'name', expandTrigger: 'hover' };
@ -174,14 +185,29 @@ const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label
//
const openDialog = async (row: any) => {
//
var res = await getAPI(SysOrgApi).apiSysOrgListGet(0);
state.orgData = res.data.result ?? [];
state.orgData = await getAPI(SysOrgApi)
.apiSysOrgListGet(0)
.then((res) => res.data.result ?? []);
//
state.tenantDbData = await getAPI(SysTenantApi)
.apiSysTenantTenantDbListGet()
.then((res) => res.data.result ?? []);
state.ruleForm = JSON.parse(JSON.stringify(row));
state.isShowDialog = true;
ruleFormRef.value?.resetFields();
};
// db
const dbChanged = async () => {
if (state.ruleForm.configId === '' || state.ruleForm.configId == null) return;
let db = state.tenantDbData.find((u: any) => u.configId == state.ruleForm.configId);
state.ruleForm.connection = db?.connection;
state.ruleForm.dbType = db?.dbType;
};
//
const closeDialog = () => {
emits('handleQuery');

View File

@ -39,8 +39,8 @@
<el-empty :image-size="200" />
</template>
<template #row_tenantType="{ row }">
<el-tag v-if="row.tenantType === 0" type="success">ID隔离</el-tag>
<el-tag v-else type="danger">库隔离</el-tag>
<el-tag v-if="row.tenantType === 0" type="info">ID隔离</el-tag>
<el-tag v-else type="primary">库隔离</el-tag>
</template>
<template #row_status="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="2" size="small" @change="changeStatus(scope)" :disabled="scope.row.id == 1300000000001" />
@ -153,8 +153,8 @@ const options = useVxeTable<TenantOutput>(
{ field: 'expirationTime', title: '过期时间', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'tenantType', title: '租户类型', minWidth: 100, showOverflow: 'tooltip', slots: { default: 'row_tenantType' } },
{ field: 'status', title: '状态', minWidth: 100, slots: { default: 'row_status' } },
{ field: 'dbType', title: '数据库类型', minWidth: 120, showOverflow: 'tooltip', slots: { default: 'row_dbType' } },
{ field: 'configId', title: '数据库标识', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'dbType', title: '数据库类型', minWidth: 120, showOverflow: 'tooltip', slots: { default: 'row_dbType' } },
{ field: 'connection', title: '数据库连接', minWidth: 300, showOverflow: 'tooltip' },
{ field: 'slaveConnections', title: '从库连接', minWidth: 300, showOverflow: 'tooltip' },
{ field: 'orderNo', title: '排序', width: 80, showOverflow: 'tooltip' },