尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Blazor Excel 导入怎么做?数据验证、权限与事务如何处理?

Blazor Excel 导入怎么做?数据验证、权限与事务如何处理? 已发布的《第十篇电子元件管理实战——Excel 导入导出》讲的是怎么导入。这篇往前一步讲导入这条链路里的四个工程问题列名怎么映射、数据怎么验证、权限怎么控、事务怎么保证。一、导入的完整链路点击导入按钮ShowImportButton ↓ AuthButton(add) 决定按钮是否显示 ShowImportDialogDropUpload.xlsx ↓ 文件大小 ≤ 5MB ↓ AuthButton(add) 服务端二次校验 ↓ MiniExcelmemoryStream.QueryTItem() ↓ OnBeforeImportAsync自定义校验 ↓ FilterAuthorizedImportRowsAsync数据权限过滤 ↓ InsertOrUpdate UpdateColumns ↓ OnFinishImportAsync导入后处理 ↓ OperationLog 重新查询 Toast 提示下面按这条链路的顺序展开。二、模板下载列从哪来protectedasyncTaskDownloadExcelTemplate(){if(OnExportAllDataAsync.HasDelegate){varargsnewAdminExportEventArgsTItem{Itemsnew(),IsTemplatetrue};awaitOnExportAllDataAsync.InvokeAsync(args);return;}awaitTableExport.ExportExcelAsync(newListTItem(),GetExportColumns().Where(xx.IsVisibleWhenAdd!false),newTableExportOptions{EnableAutoFilterfalse},${typeof(TItem).Name}_template.xlsx);}两个分支页面注册了OnExportAllDataAsync→ 由页面自己产出模板IsTemplate true可以据此区分导出模板和导出数据否则 → 用当前可见列导出空表作为模板。注意.Where(x x.IsVisibleWhenAdd ! false)新增时不可见的列比如创建时间“创建人”不会出现在模板里避免用户填了却写不进去。一个必须实测的点表头与属性名的匹配导出侧的列头来自列的显示名row[((IEditorItem)col).GetDisplayName()??field]awaitFormatExportValueAsync(...);导入侧用的是 MiniExcel 的强类型读取varrowsmemoryStream.QueryTItem().ToList();QueryTItem()是按列名匹配实体属性的。如果你的列头是中文显示名例如[DisplayName(标题)] Title而属性名是英文导入时就会读不到值。处理方式有两种让 Excel 的列名与实体属性名一致把模板表头改成属性名用OnImportFromDictionaryAsync拿原始字典自己做列名映射下一节。这一点建议在接入后先用一份真实 Excel 做一次往返测试。三、导入解析三种回调导入弹窗的主体逻辑op.ComponentBootstrapDynamicComponent.CreateComponentDropUpload(newDictionarystring,object?{[nameof(DropUpload.ShowProgress)]true,[nameof(DropUpload.ShowFooter)]true,[nameof(DropUpload.Accept)].xlsx,[nameof(DropUpload.FooterText)]ImportMessage,[nameof(DropUpload.OnChange)]asyncTask(UploadFilefile){if(filenull||file.Filenull)return;// 服务器端验证当文件大于 5MB 时提示文件太大信息if(file.SizeMaxFileLength){awaitMessageService.Error(CommonLocalizer[文件大小超过 5MB]);file.Code1;file.ErrorCommonLocalizer[文件大小超过 5MB];return;}ReadToken??newCancellationTokenSource();try{// 服务端导入校验与 UI 导入按钮显隐使用同一权限判定管理员自动放行if(!awaitadmin.AuthButton(add)){awaitMessageService.Error(string.Format(CommonLocalizer[没有权限执行该操作]));return;}usingvarbrowserStreamfile.File.OpenReadStream(MaxFileLength,ReadToken.Token);usingvarmemoryStreamnewMemoryStream();awaitbrowserStream.CopyToAsync(memoryStream,ReadToken.Token);memoryStream.Position0;...}}});文件大小上限来自常量staticlongMaxFileLength5*1024*1024;// 文件最大 5M三种处理路径回调输入适用场景默认ListTItem列名与实体属性一致的标准场景OnImportFromDictionaryAsyncIEnumerableIDictionarystring, object列名不固定、需要自定义映射OnImportAsyncListTItem完全接管写库逻辑返回受影响行数字典导入的实现细节if(OnImportFromDictionaryAsync.HasDelegate){vardictItemsmemoryStream.Query(true).CastIDictionarystring,object();varargsnewAdminImportDictionaryEventArgs{ItemsdictItems};awaitOnImportFromDictionaryAsync.InvokeAsync(args);if(args.Cancel)return;affectedRowsdictItems.Count();}注意args.Items是IEnumerable页面可以替换它AdminImportDictionaryEventArgs.Items有 setter所以过滤掉不合格的行也能在这里做。四、数据验证框架做多少页面做多少框架在导入路径上只做三件事文件必须是.xlsxDropUpload.Accept文件大小 ≤ 5MB有add权限。实体上的[Required]、[MaxLength]等数据注解不会在导入时自动生效——因为导入是直接走InsertOrUpdate的没有经过编辑表单的EditContext校验。所以业务校验要放在OnBeforeImportAsync[Parameter]publicEventCallbackAdminImportEventArgsTItemOnBeforeImportAsync{get;set;}页面用法示例伪代码结构按你项目里的服务写privateasyncTaskOnBeforeImport(AdminImportEventArgsOrdere){varerrorsnewListstring();foreach(var(item,index)ine.Items.Select((x,i)(x,i2))){if(string.IsNullOrWhiteSpace(item.OrderNo))errors.Add($第{index}行订单号不能为空);if(item.Amount0)errors.Add($第{index}行金额必须大于 0);}if(errors.Count0){awaitSwalService.Warning(导入校验失败,string.Join(br/,errors.Take(20)));e.Canceltrue;// 取消整批导入}}AdminImportEventArgsTItem提供了Items可整体替换和Cancel取消导入publicclassAdminImportEventArgsTItemwhereTItem:class{publicListTItemItems{get;set;}new();publicboolCancel{get;set;}}这里有两个选择e.Cancel true→ 整批不导入适合有一条错就全部退回直接改e.Items剔除错误行→ 只导入合法行适合尽量导入。注意重复数据的判定也属于业务校验框架的默认实现是InsertOrUpdateId 0更新、Id 0新增不会自动按业务字段去重。要按订单号这类业务键去重需要在OnBeforeImportAsync或OnImportAsync里自己查。五、数据权限最容易被忽略的一条默认导入路径// 数据权限Excel 导入/更新同样不能操作当前用户无权限的数据。// Id 0 的行会走数据库按主键更新因此必须逐行确认该记录在当前用户的数据权限范围内// 不能只依赖前端按钮权限否则伪造 Id 即可越权更新他人数据rowsawaitFilterAuthorizedImportRowsAsync(rows);if(rows.Count0)return;affectedRowsawait_repo.Orm.InsertOrUpdateTItem().SetSource(rows).UpdateColumns(updateColumns).ExecuteAffrowsAsync();过滤实现privateasyncTaskListTItemFilterAuthorizedImportRowsAsync(ListTItemrows){if(rows.Count0||!(UseDataPermission||_autoDataPermission)){returnrows;}varauthorizedawaitFilterAuthorizedAsync(rows);if(authorized.Countrows.Count){varrejectedrows.Count-authorized.Count;awaitToastService.Warning(CommonLocalizer[导入结果],string.Format(CommonLocalizer[已忽略 {0} 条没有权限操作的数据。],rejected));}returnauthorized;}它的判定复用查询路径的规则FilterAuthorizedAsync→ApplyDataPermission所以界面上看不到的数据导入也改不了。测试直接覆盖了这个场景/// summary/// Excel 导入/更新路径Id100 OrgIdB 的行不能让用户更新他人的记录。/// /summary[Fact]publicasyncTaskExcelImport_OtherOrgRow_IsDropped(){var(mine,theirs)awaitSeedAsync();// 模拟 Excel 中提交的内容一条自己的 一条他人的伪造 OrgId/Idtheirs.Name被篡改;varcandidatesnewListScopedRecord{mine,theirs};varauthorizedawait_repo.FilterAuthorizedAsyncScopedRecord,long(_admin,candidates,true);authorized.Should().HaveCount(1);authorized.Single().Id.Should().Be(mine.Id);}如果你用OnImportAsync完全接管导入这段过滤不会执行需要自己补上等价的权限校验。六、字段映射与批量写入更新哪些列由可见列决定varupdateColumnsGetExportColumns().Where(xx.IsVisibleWhenAdd!false).Select(xx.GetFieldName()).ToArray();然后一次性批量写入affectedRowsawait_repo.Orm.InsertOrUpdateTItem().SetSource(rows).UpdateColumns(updateColumns).ExecuteAffrowsAsync();行为说明Id 0按主键更新且只更新updateColumns里的列Id 0新增没在updateColumns里的列即使 Excel 里填了也不会写入这是一条重要的安全边界Excel 里多出来的列不会污染数据库。但也意味着新增时不可见的列无法通过导入赋值比如创建人、创建时间。新增记录的审计字段由框架的AuditValue自动填充CreatedUserId/CreatedTime/ 数据权限的OrgId不需要在 Excel 里提供。七、事务默认实现与自定义实现的差别这里要说清楚避免误解默认导入路径没有显式开启事务。它执行的是一条InsertOrUpdate ... ExecuteAffrowsAsync()语句由数据库保证这条语句自身的原子性。如果你需要整批数据要么全成功、要么全失败例如第 100 行违反唯一约束时前 99 行也要回滚要用OnImportAsync自己控制// 示意用 UnitOfWork 包裹整批写入usingvaruow_uowManager.Begin();// UnitOfWorkManager 由 DI 注入try{awaitrepo.Orm.InsertOrUpdateOrder().SetSource(rows).ExecuteAffrowsAsync();awaitotherRepo.InsertAsync(...);// 其他配套写入uow.Commit();}catch{uow.Rollback();throw;}关于事务与仓储绑定审批模块那篇第 18 篇有完整讲解原理一样仓储必须绑定到同一个 UnitOfWork否则回滚不完整。八、异常处理catch(Exceptionex){ServiceProvider.GetServiceOperationLogService()?.AddLog(OperationType.Import,OperationResult.Failure,failureReason:ex.ToString());awaitToastService.Warning(CommonLocalizer[导入失败],ex.Message);}成功时也会记一笔if(affectedRows0){// 添加操作日志ServiceProvider.GetServiceOperationLogService()?.AddLog(OperationType.Import,OperationResult.Success);// 重新查询数据awaitbase.QueryAsync(1);}awaitToastService.Success(CommonLocalizer[导入结果],string.Format(CommonLocalizer[成功导入{0}条数据。],affectedRows));失败原因写进操作日志的failureReason界面上只显示ex.Message。导入是用户操作出了问题要能在操作日志里查到上下文。九、导出同一套过滤不一样的查询导出走OnExportAllAsync和查询共用过滤链路但查询方式不同varselectGetSelect();if(OnBeforeQuery.HasDelegate){// 注意导出时 IsExport trueawaitOnBeforeQuery.InvokeAsync(newAdminQueryEventArgsTItem(select,context.Options){IsExporttrue});}varcolumnsGetExportColumns().ToList();...// 只查询导出列动态投影避免 SELECT * 把大文本/导航数据全部拉回来// 查询链路不变OnBeforeQuery 过滤 数据权限 动态过滤 排序Include 不会执行rowsawaitLoadExportRowsAsync(select,columns,context.Options,lookupService,exportOptions);几个要点IsExport true页面在OnBeforeQuery里可以据此跳过Include上面的注释明确说了Include 不会执行动态投影按列名用Reflection.Emit生成 DTO 再投影查询避免把正文这类大字段拉回来投影失败回退捕获异常后回退到整实体查询保证导出可用MiniExcel 写流直接SaveAsAsync到内存流再交给DownloadService避免逐单元格异步格式化的开销。十、常见问题现象原因处理导入后字段全为空Excel 列名与实体属性名不匹配用OnImportFromDictionaryAsync做映射或让列名等于属性名导入报没有权限当前用户没有add按钮权限角色勾上该菜单的添加按钮部分行没导入且提示已忽略 N 条数据权限过滤正常行为确认这些行是否属于当前用户的数据范围修改了 Excel 里的创建时间但没生效该列不在updateColumns里用可见列或在OnImportAsync里自己处理一批里有一条错其余也没进去数据库约束导致整条语句失败需要部分成功就在导入前过滤数据导入后想全回滚默认实现没有显式事务用OnImportAsync UnitOfWork数据重复插入Id 0的行会被当成新增在OnBeforeImportAsync里按业务键去重或让 Excel 带主键十一、小结Excel 导入这条链路上框架负责安全与一致性底线页面负责业务规则关注点由谁负责实现位置文件类型与大小框架DropUpload.Accept、MaxFileLength按钮权限 服务端权限框架AuthButton(add)列名 → 实体映射框架按属性名/ 页面自定义映射QueryTItem()/OnImportFromDictionaryAsync业务校验、去重页面OnBeforeImportAsync数据权限框架FilterAuthorizedImportRowsAsync只更新允许的列框架UpdateColumns全批次事务页面可选OnImportAsync UnitOfWork操作日志框架OperationLogService.AddLog一句话框架保证能导入的都在权限范围内、只会写允许的列业务规则由你在导入前拦住。如果你正在用 .NET 10 Blazor 做后台导入导出是高频需求。EasyAdminBlazor 的AdminTable已经把模板下载、导入校验、数据权限、批量写入串成了一条链路可以直接用也可以按自己的业务规则在回调里扩展。文档https://easyadmin.wang-zhan.com.cn/doc源码https://gitee.com/gudufy/EasyAdminBlazor
返回列表