😎1、优化字典值合规性校验特性 2、优化种子初始化 3、优化前端导入数据

This commit is contained in:
zuohuaijun 2025-06-11 01:18:59 +08:00
parent cdb58a28ab
commit d32a4b2613
4 changed files with 215 additions and 60 deletions

View File

@ -50,23 +50,13 @@ public class DictAttribute : ValidationAttribute, ITransient
// 判断是否允许空值
if (AllowNullValue && value == null) return ValidationResult.Success;
var valueAsString = value?.ToString();
// 是否忽略空字符串
if (AllowEmptyStrings && string.IsNullOrEmpty(valueAsString)) return ValidationResult.Success;
// 获取属性的类型
var property = validationContext.ObjectType.GetProperty(validationContext.MemberName!);
if (property == null) return new ValidationResult($"未知属性: {validationContext.MemberName}");
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
string importHeaderName = GetImporterHeaderName(property, validationContext.MemberName);
// 枚举类型验证
if (propertyType.IsEnum)
{
if (!Enum.IsDefined(propertyType, value!)) return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!");
return ValidationResult.Success;
}
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
// 先尝试从 ValidationContext 的依赖注入容器中拿服务,拿不到或类型不匹配时,再从全局的 App 容器中获取
if (validationContext.GetService(typeof(SysDictDataService)) is not SysDictDataService sysDictDataService)
@ -78,8 +68,71 @@ public class DictAttribute : ValidationAttribute, ITransient
// 使用 HashSet 来提高查找效率
var dictHash = new HashSet<string>(dictDataList.Select(u => u.Value));
if (!dictHash.Contains(valueAsString)) return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{valueAsString}】!");
// 判断是否为集合类型
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>))
{
// 如果是空集合并且允许空值,则直接返回成功
if (value == null && AllowNullValue) return ValidationResult.Success;
// 处理集合为空的情况
var collection = value as IEnumerable;
if (collection == null) return ValidationResult.Success;
// 获取集合的元素类型
var elementType = propertyType.GetGenericArguments()[0];
var underlyingElementType = Nullable.GetUnderlyingType(elementType) ?? elementType;
// 如果元素类型是枚举,则逐个验证
if (underlyingElementType.IsEnum)
{
foreach (var item in collection)
{
if (item == null && AllowNullValue) continue;
if (!Enum.IsDefined(underlyingElementType, item!))
return new ValidationResult($"提示:{ErrorMessage}|枚举值【{item}】不是有效的【{underlyingElementType.Name}】枚举类型值!", [importHeaderName]);
}
return ValidationResult.Success;
}
foreach (var item in collection)
{
if (item == null && AllowNullValue) continue;
var itemString = item?.ToString();
if (!dictHash.Contains(itemString))
return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{itemString}】!", [importHeaderName]);
}
return ValidationResult.Success;
}
var valueAsString = value?.ToString();
// 是否忽略空字符串
if (AllowEmptyStrings && string.IsNullOrEmpty(valueAsString)) return ValidationResult.Success;
// 枚举类型验证
if (propertyType.IsEnum)
{
if (!Enum.IsDefined(propertyType, value!)) return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!", [importHeaderName]);
return ValidationResult.Success;
}
if (!dictHash.Contains(valueAsString))
return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{valueAsString}】!", [importHeaderName]);
return ValidationResult.Success;
}
/// <summary>
/// 获取本字段上 [ImporterHeader(Name = "xxx")] 里的Name如果没有则使用defaultName.
/// 用于在从excel导入数据时能让调用者知道是哪个字段验证失败而不是抛异常
/// </summary>
private static string GetImporterHeaderName(PropertyInfo property, string defaultName)
{
var importerHeader = property.GetCustomAttribute<ImporterHeaderAttribute>();
string importerHeaderName = importerHeader?.Name ?? defaultName;
return importerHeaderName;
}
}

View File

@ -318,7 +318,7 @@ public static class SqlSugarExtension
#endregion
#region
#region
/// <summary>
/// 初始化表实体
@ -418,16 +418,16 @@ public static class SqlSugarExtension
}
// 若实体包含Id字段则设置为当前租户Id递增1
var idProp = entityType.GetProperty(nameof(EntityBaseId.Id));
var idProperty = entityType.GetProperty(nameof(EntityBaseId.Id));
var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType);
if (idProp != null && entityInfo.Columns.Any(u => u.PropertyName == nameof(EntityBaseId.Id)))
if (idProperty != null && idProperty.PropertyType == typeof(long) && entityInfo.Columns.Any(u => u.PropertyName == nameof(EntityBaseId.Id)))
{
var seedId = config.ConfigId.ToLong();
foreach (var sd in seedData)
{
var id = idProp!.GetValue(sd, null);
var id = idProperty!.GetValue(sd, null);
if (id == null || id.ToString() == "0" || string.IsNullOrWhiteSpace(id.ToString()))
idProp.SetValue(sd, ++seedId);
idProperty.SetValue(sd, ++seedId);
}
}
@ -476,7 +476,7 @@ public static class SqlSugarExtension
return (total, insertCount, updateCount);
}
#endregion
#endregion
#region

View File

@ -2,7 +2,7 @@
"name": "admin.net.pro",
"type": "module",
"version": "2.4.33",
"lastBuildTime": "2025.06.10",
"lastBuildTime": "2025.06.11",
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
"author": "zuohuaijun",
"license": "MIT",

View File

@ -1,6 +1,6 @@
<template>
<div class="sys-import-data-container">
<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="300px">
<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="400px" @close="resetDialog">
<template #header>
<div style="color: #fff">
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-UploadFilled /> </el-icon>
@ -9,21 +9,34 @@
</template>
<el-row :gutter="15" v-loading="state.loading">
<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
<el-button class="ml10" type="info" icon="ele-Download" v-reclick="3000" @click="() => download()" :disabled="state.loading">{{ t('list.template') }}</el-button>
<el-col :span="24" class="mb10">
<el-button icon="ele-Download" v-reclick="3000" @click="download" :disabled="state.loading">{{ t('list.template') }}</el-button>
</el-col>
<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
<el-upload :limit="1" :show-file-list="false" :on-exceed="handleExceed" :http-request="handleImportData" ref="uploadRef">
<el-col :span="24" class="mb15 flex">
<el-upload :limit="1" :show-file-list="false" :auto-upload="false" :on-exceed="handleExceed" :on-change="handleFileChange" ref="uploadRef">
<template #trigger>
<el-button type="primary" icon="ele-MostlyCloudy" v-reclick="3000" :disabled="state.loading">{{ t('list.import') }}</el-button>
<el-button class="mr10" type="primary" icon="ele-MostlyCloudy" :disabled="state.isCompleted || state.loading">{{ t('list.import') }}</el-button>
</template>
</el-upload>
<span class="selected-file">{{ state.selectedFile ? state.selectedFile.name : '未选择文件' }}</span>
</el-col>
<!-- 错误提示区域 -->
<el-col :span="24" v-if="state.importResultUrl" class="mt10">
<div v-if="state.hasError" style="color: red; margin-bottom: 10px">导入完毕存在部分错误请下载导入结果查看详情</div>
<el-link type="primary" :underline="false" @click="downloadImportResult">
<el-icon class="mr5"><ele-Download /></el-icon>
</el-link>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button icon="ele-CircleCloseFilled" @click="() => (state.isShowDialog = false)" :disabled="state.loading">{{ t('list.cancelButtonText') }}</el-button>
<el-button icon="ele-CircleCloseFilled" @click="closeDialog" :disabled="state.loading">{{ t('list.cancelButtonText') }}</el-button>
<el-button type="primary" @click="state.isCompleted ? closeDialog() : submitImport()" :disabled="(!state.selectedFile && !state.isCompleted) || state.loading">
{{ state.isCompleted ? '关 闭' : '确 定' }}
</el-button>
</span>
</template>
</el-dialog>
@ -31,7 +44,7 @@
</template>
<script lang="ts" setup name="sysImportData">
import type { UploadInstance, UploadProps, UploadRawFile, UploadRequestOptions } from 'element-plus';
import type { UploadInstance, UploadProps, UploadRawFile, UploadFile } from 'element-plus';
import { ElUpload, ElMessage, genFileId } from 'element-plus';
import { downloadStreamFile } from '/@/utils/download';
import { reactive, ref } from 'vue';
@ -42,6 +55,11 @@ const uploadRef = ref<UploadInstance>();
const state = reactive({
isShowDialog: false,
loading: false,
isCompleted: false,
hasError: false,
selectedFile: null as File | null,
importResultUrl: '' as string,
importResultName: '' as string,
});
// /
@ -50,9 +68,28 @@ const emit = defineEmits(['refresh']);
//
const openDialog = () => {
resetDialog();
state.isShowDialog = true;
};
//
const resetDialog = () => {
state.isCompleted = false;
state.hasError = false;
state.selectedFile = null;
state.importResultUrl = '';
state.importResultName = '';
uploadRef.value?.clearFiles();
};
//
const closeDialog = () => {
state.isShowDialog = false;
if (state.importResultUrl) {
URL.revokeObjectURL(state.importResultUrl);
}
};
//
const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.clearFiles();
@ -61,39 +98,87 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.handleStart(file);
};
//
const handleImportData = (opt: UploadRequestOptions): any => {
state.loading = true;
props
.import(opt.file)
.then((res: any) => {
// json
const contentType = res.headers['content-type'];
if (contentType && contentType.toLowerCase().includes('application/json')) {
const decoder = new TextDecoder('utf-8');
const data = decoder.decode(res.data);
try {
const result = JSON.parse(data);
if (result.code == '200') {
ElMessage.success(result.message);
} else {
ElMessage.error(result.message);
return;
}
} catch (e) {
console.error('解析数据导入结果失败:', e);
downloadStreamFile(res);
}
//
const handleFileChange = (file: UploadFile) => {
state.selectedFile = file.raw as File;
};
//
const submitImport = async () => {
if (!state.selectedFile) return;
try {
state.loading = true;
const res = await props.import(state.selectedFile);
//
const contentType = res.headers['content-type'] || '';
if (contentType.includes('application/json')) {
// JSON
const decoder = new TextDecoder('utf-8');
const data = decoder.decode(res.data);
const result = JSON.parse(data);
if (result.code === 200) {
ElMessage.success(result.message);
emit('refresh');
state.hasError = false;
closeDialog(); //
} else {
downloadStreamFile(res);
ElMessage.error(result.message);
state.hasError = false;
}
} else {
//
const blob = new Blob([res.data]);
const contentDisposition = res.headers['content-disposition'];
let filename = '导入结果.xlsx';
if (contentDisposition) {
const match = contentDisposition.match(/filename="?([^"]+)"?/);
if (match && match[1]) {
filename = decodeURIComponent(match[1]);
}
}
// URL
if (state.importResultUrl) {
URL.revokeObjectURL(state.importResultUrl);
}
// URL
state.importResultUrl = URL.createObjectURL(blob);
state.importResultName = filename;
state.isCompleted = true;
state.hasError = true;
//
emit('refresh');
state.isShowDialog = false;
})
.finally(() => {
uploadRef.value?.clearFiles();
state.loading = false;
});
ElMessage.warning('导入完成,存在部分错误');
}
} catch (error) {
console.error('导入错误:', error);
ElMessage.error('导入过程中发生错误');
state.hasError = false;
} finally {
state.loading = false;
}
};
//
const downloadImportResult = () => {
if (!state.importResultUrl) return;
const link = document.createElement('a');
link.href = state.importResultUrl;
link.download = state.importResultName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
//
// closeDialog();
};
//
@ -101,9 +186,26 @@ const download = () => {
props
.download()
.then((res: any) => downloadStreamFile(res))
.catch((res: any) => ElMessage.error(`${t('list.downloadError')}: ${res}`));
.catch((err: any) => ElMessage.error('下载错误: ' + err));
};
//
defineExpose({ openDialog });
defineExpose({ openDialog, closeDialog });
</script>
<style scoped>
.selected-file {
margin-left: 10px;
line-height: 32px;
color: var(--el-text-color-regular);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
.flex {
display: flex;
align-items: center;
}
</style>