
案例来源仓库自带的演示项目EasyAdminBlazor.Demo不是伪代码所有内容都能在仓库里逐行对照。本文把演示项目按接单的真实顺序走一遍需求 → 数据库设计 → 后台模块 → 权限 → 数据权限 → Excel/文件 → 审批 → 前台数据 → 配置与上线。一、需求一个企业官网 内容后台的项目演示项目的业务定位很清晰就是一个常见的接单场景客户要一个企业官网首页、产品、服务、新闻、联系我们同时要一个后台能自己维护这些内容。拆成模块模块前台后台关联能力产品产品列表 / 产品详情增删改查、上下架、图片文件上传、Excel 导入导出服务首页服务区块 / 服务详情增删改查、排序、启用图标、图片、富文本新闻随笔首页新闻 / 新闻详情增删改查、分类、审核两级审批、富文本、缩略图、草稿新闻分类——增删改查关联字段评论新闻详情页增删改查、审核标记关联字段留言反馈联系我们查看、标记已读、导出——注意后台不只是增删改查审核走审批、图片走文件服务、批量录入走 Excel、内容需要草稿——这些恰好是 EasyAdminBlazor 的现成能力。二、第一步数据库设计演示项目一共6 张业务表product 产品 service 服务项目 feedback 留言反馈 blog_classify 新闻分类 blog_article 新闻随笔 blog_comment 评论关系很简单blog_classify 1 ── n blog_article 1 ── n blog_comment其余三张表相互独立。实体怎么写以服务为例/// summary服务项目/summary[Table(Nameservice)]publicpartialclassService:EntityFull{[DisplayName(服务名称)][Column(StringLength100)]publicstringName{get;set;}string.Empty;/// summary图标FontAwesome 类名如 fa-solid fa-gears/summary[DisplayName(图标)][Column(StringLength100)]publicstringIcon{get;set;}string.Empty;[DisplayName(图片)][Column(StringLength400)]publicstringImage{get;set;}string.Empty;[DisplayName(摘要)][Column(StringLength500)]publicstringExcerpt{get;set;}string.Empty;[DisplayName(详情)][Column(StringLength-2)]publicstringContent{get;set;}string.Empty;[DisplayName(排序)]publicintSort{get;set;}[DisplayName(是否启用)]publicboolIsEnable{get;set;}true;}几个约定前面几篇反复讲过这里再压缩一遍主键来自EntityFulllong Id 雪花 ID不用自己声明[DisplayName]决定列表列头与表单标签[Column(StringLength -2)]是长文本富文本正文字段EntityFull自带创建人/创建时间/修改人/修改时间 软删除。主键怎么定演示数据里能看到固定的 Id例如510337460719685这是为了演示数据可复现。正常开发不写 Id插入时由[Snowflake]自动生成。新闻要审批所以基类不一样其他实体继承EntityFull新闻继承审批基类[Table(Nameblog_article)]publicpartialclassArticle:ApprovalEntityFull{[DisplayName(随笔专栏)][Required]publiclong?ClassifyId{get;set;}publicClassifyClassify{get;set;}default!;[Column(StringLength200)][DisplayName(标题)]publicstringTitle{get;set;}string.Empty;[Column(StringLength-2)][DisplayName(正文)][Required]publicstringContent{get;set;}string.Empty;publicDateTime?PublishTime{get;set;}DateTime.Now;...}换基类这一步就把审批状态字段ApprovalStatus/CurrentLevel带进了实体——这就是为什么数据库设计阶段就要想清楚哪些单据需要审批。三、第二步从实体到后台模块一个模块的标准产物是两个文件列表页 编辑模板。列表页page /Admin/Product * 页面组件与实体同名时用别名避免组件类名遮蔽实体类型CS0311/CS1061 * using ProductEntity EasyAdminBlazor.Test.Products.Product AdminTable TItemProductEntity TKeylong OnBeforeQueryOnBeforeQuery EditDialogSizeSize.ExtraLarge EnableSaveWithoutClose EnableDraft ShowImportButton ShowExportButton ShowExtendButtons IsPagination ShowSearch ShowAdvancedSearch IsMultipleSelect TableColumns TableColumn bind-Fieldcontext.Title Filterabletrue Searchabletrue / TableColumn bind-Fieldcontext.Price Filterabletrue / TableColumn bind-Fieldcontext.Stock Filterabletrue / TableColumn bind-Fieldcontext.Image Filterabletrue Template Contextv if (!string.IsNullOrEmpty(v.Row.Image)) { img srcv.Row.Image height36 alt产品图 / } /Template /TableColumn TableColumn bind-Fieldcontext.IsOnSale Filterabletrue / TableColumn bind-Fieldcontext.CreatedTime Filterabletrue / /TableColumns EditTemplate ProductEdit itemcontext / /EditTemplate /AdminTable这一页就同时具备了分页、排序、模糊搜索、高级搜索、列头筛选、多选、导入、导出、草稿、保存不关闭、图片列预览。留言反馈页更简单连编辑模板都不需要只读 标记已读 导出page /Admin/Feedback using FeedbackEntity EasyAdminBlazor.Test.Feedbacks.Feedback AdminTable TItemFeedbackEntity TKeylong OnBeforeQueryOnBeforeQuery EditDialogSizeSize.Large ShowSearch ShowAdvancedSearchfalse ShowExportButton ShowExtendButtons IsPagination IsMultipleSelect TableColumns TableColumn bind-Fieldcontext.Name Filterabletrue Searchabletrue / TableColumn bind-Fieldcontext.Phone Filterabletrue Searchabletrue / TableColumn bind-Fieldcontext.Subject Filterabletrue Searchabletrue / TableColumn bind-Fieldcontext.Content Filterabletrue / TableColumn bind-Fieldcontext.IsRead Filterabletrue / TableColumn bind-Fieldcontext.CreatedTime Filterabletrue / /TableColumns /AdminTable编辑模板using ProductEntity EasyAdminBlazor.Test.Products.Product div classrow form-inline g-3 div classcol-12 col-sm-6 label classform-label产品名称/label input binditem.Title typetext classform-control maxlength100 / /div div classcol-12 col-sm-6 input binditem.Price typenumber step0.01 classform-control / /div div classcol-12 col-sm-6 Checkbox bind-Valueitem.IsOnSale DisplayText上架 / /div div classcol-12 col-sm-6 AdminFileInput bind-Valueitem.Image DisplayText产品图片 / /div div classcol-12 Textarea bind-Valueitem.Excerpt maxlength500/Textarea /div div classcol-12 AdminEditor bind-Valueitem.Content DisplayText产品详情 / /div /div code { [Parameter] [NotNull] public ProductEntity? item { get; set; } }到这里一个模块的后台代码就写完了——没有 Controller、没有 Service、没有 DTO、没有前端请求封装。页面写完了还要建菜单在第 01 篇强调过页面存在 ≠ 能访问。演示项目在Program.cs里补种了菜单vardemoRootfsql.SelectSysMenu().Where(aa.Label内容管理a.Path).First();repo.Insert(new[]{newSysMenu{Label服务,PathAdmin/Service,ParentIddemoRoot.Id,Sort10004,TypeSysMenuType.Menu,ChildsnewListSysMenu(cudButtons())},...});cudButtons()生成add/edit/remove三个按钮——它们就是AdminTable按钮显隐和服务端校验的依据。真实项目里菜单可以用后台菜单管理手工建也可以像演示项目这样用代码补齐适合交付多个环境时保持一致。演示代码里还处理了旧版本菜单路径带前导斜杠的迁移属于典型的升级兼容逻辑。四、第三步权限与数据权限功能权限菜单 按钮建完菜单后去角色 → 分配菜单勾选。之后侧边栏菜单只显示勾选过的项进入页面前AuthPath会校验页面里的添加/编辑/删除按钮按AuthButton显隐保存/删除时服务端会再校验一次伪造请求也没用。演示项目里的角色页还有个细节值得学——角色-菜单关系变化后立即让权限缓存失效await_repo.UpdateAsync(select);awaitadmin.InvalidatePermissionCacheAsync();数据权限让只能看自己的变成默认行为演示项目里的用户管理页就是这么开的AdminTable ... TItemSysUser TKeylong ShowToolbar EditDialogSizeSize.ExtraLarge ShowImportButton ShowExportButton ShowExtendButtons ShowAdvancedSearch IsMultipleSelect IsPagination ShowSearch UseDataPermission ...只要SysUser实现了IDataPermission框架里已经实现再加上UseDataPermission非管理员用户就只能看到自己组织范围内的用户。查询、修改、删除、Excel 导入四条路径都会被过滤见第 09 篇。对客户来说这就是部门经理只能管本部门的人——不需要你写一行过滤代码。五、第四步把 Excel 和文件接上Excel产品页开了ShowImportButton ShowExportButton于是自动获得下载导入模板按可见列生成上传.xlsx导入默认 5MB 上限导出当前筛选结果按可见列投影不拉全表字段。需要业务校验比如价格必须大于 0就在OnBeforeImportAsync里加权限过滤框架已经做了。文件产品和服务的图片字段用的是AdminFileInputAdminFileInput bind-Valueitem.Image DisplayText产品图片 /上传后的文件自动落在uploads/{租户}/{yyyy/MM/dd}/校验扩展名 Magic Number超过MaxImageWidth时缩放转 WebPKeepOriginalFile false时删原图。客户只要在后台点选择…上传一张图剩下的都是框架的事。六、第五步给新闻加两级审批新闻是要公司对外发布的内容所以演示项目给它配了两级审批builder.AddEasyAdminBlazor(newEasyAdminBlazorOptions{...}).AddEasyAdminBlazorApproval(o{// 演示随笔走两级审批 —— 部门负责人 - 管理员角色o.Flows.Add(newApprovalFlowConfig{BillTypetypeof(Article).FullName!,// 默认策略审批通过后锁定不能再修改// 想体验改后重审、且只对关键字段重审打开下面两行// AllowEditAfterApproved true,// ResubmitFields [nameof(Article.Title), nameof(Article.Content)],Levels[newApprovalLevelConfig{Level1,Name部门主管审批,KindApproverKind.OrgLeader,Offset1},newApprovalLevelConfig{Level2,Name管理员审批,KindApproverKind.Role,ValueAdministrator}]});})编辑器里加一个审批选项卡TabItem TextCommonLocalizer[审批] ApprovalActions TItemArticle BillModel OnChangedOnApprovalChanged / /TabItem列表里加一列状态TableColumn bind-Fieldcontext.ApprovalStatus Filterabletrue Template Contextvv.Row.ApprovalStatusText/Template /TableColumn因为实体是ApprovalEntityFullAdminTable会自动接管审批中的单据不能改也不能删。演示项目还在启动时把管理员所在部门的负责人设成了管理员本人方便体验部门负责人这一级// 演示审批把管理员所在组织的负责人设为管理员本人便于体验部门负责人审批节点if(adminUserisnotnulladminUser.OrgId0fsql.SelectSysOrg().Where(aa.IdadminUser.OrgIda.ResponsibleUserIdnull).Any()){fsql.UpdateSysOrg().Where(aa.IdadminUser.OrgId).Set(aa.ResponsibleUserId,(long?)adminUser.Id).ExecuteAffrows();}注意新版本默认不允许自己审自己。演示里把负责人设成管理员本人、审批人又是管理员角色在这种配置下自审默认被跳过需要显式打开AllowSelfApproval才能自审见第 16 篇。七、第六步前台和后台共用一套实体演示项目的另一个亮点前台 Razor Pages 直接注入仓储读同一批实体不存在后台改了前台读不到的问题。首页publicclassIndexModel(IBaseRepositoryProductproductRepository,IBaseRepositoryServiceserviceRepository,IBaseRepositoryArticlearticleRepository):PageModel{publicListServiceServices{get;set;}[];publicListProductProducts{get;set;}[];publicListArticleNews{get;set;}[];publicasyncTaskOnGet(){ServicesawaitserviceRepository.Select.Where(xx.IsEnable).OrderBy(xx.Sort).OrderByDescending(xx.CreatedTime).Take(3).ToListAsync();ProductsawaitproductRepository.Select.Where(xx.IsOnSale).OrderByDescending(xx.CreatedTime).Take(6).ToListAsync();NewsawaitarticleRepository.Select.Where(xx.IsAudit).OrderByDescending(xx.CreatedTime).Take(5).ToListAsync();}}产品列表页带分页publicclassProductsModel(IBaseRepositoryProductproductRepository):PageModel{publicListProductProducts{get;set;}[];publicintTotalCount{get;set;}publicasyncTaskOnGet(intpageIndex1,intpageSize12){varselectproductRepository.Select.Where(xx.IsOnSale).OrderByDescending(xx.CreatedTime);TotalCount(int)awaitselect.CountAsync();Productsawaitselect.Skip((pageIndex-1)*pageSize).Take(pageSize).ToListAsync();}}这就是前后台同一个技术栈的实际收益实体、ORM、查询语法完全复用不需要为前台再写一套 API 和 DTO。八、第七步配置与部署appsettings.json 里真正要改的东西{ConnectionStrings:{Default:Data Sourceeasyadminblazor.db},FileSettings:{DirectoryName:uploads,DateTimeDirectory:yyyy/MM/dd,KeepOriginalFile:false,MaxImageWidth:800,WebpQuality:80},Security:{MaxUploadSize:10485760,MaxRemoteRedirects:3,LoginFailureLimit:5,AntiforgeryEnabled:true,RateLimitEnabled:true},Scheduler:{TimeZoneId:Asia/Shanghai}}生产环境把连接串换成 MySQL / SQL Server并把AutoSyncStructure关掉改由发布脚本管表结构FreeSqlBuilderaa.UseConnectionString(DataType.Sqlite,configuration[ConnectionStrings:default]).UseMonitorCommand(cmdSystem.Console.WriteLine($[{DateTime.Now.ToString(HH:mm:ss)}]{cmd.CommandText}\r\n))//监听SQL语句.UseAutoSyncStructure(env.IsDevelopment())演示项目用的是env.IsDevelopment()开发环境自动建表生产环境不自动改结构。这是最推荐的写法。上线检查清单检查项说明AdminRouteSecret换成只有自己人知道的安全码别用默认值AesKey换成长随机串不要用示例值UseAutoSyncStructure生产关闭走发布脚本默认管理员密码首次登录会强制改密确认这条流程没被改坏数据库备份至少每日备份EntityFull是软删除误删可恢复但不是备份替代品文件目录权限应用账号对wwwroot/uploads有写权限不要把上传目录暴露成可执行Redis多实例部署必须配单实例可用内存缓存HTTPS后台必须走 HTTPSAdminRouteSecret只是路径混淆不是安全边界日志配置 Serilog见doc/Serilog日志配置.md并确认日志落盘计划任务确认任务页可见装了 Scheduler 扩展、时区配置正确审批确认审批人组织负责人 / 角色已经维护否则提交时会提示未找到审批人详细的 IIS 部署步骤会单独写一篇第 28 篇。九、这个项目实际写了多少代码统计口径EasyAdminBlazor.Demo里后台业务模块的实体 页面 编辑模板按文件总行数计含注释与空行。模块实体页面/模板合计产品 Product5864122服务 Service5864122留言 Feedback522072随笔 Article135126261专栏 Classify371754评论 Comment433376合计38332470715 个文件、707 行完成了 6 个模块的后台 —— 并且里面还包含了审批接入、Excel 导入导出、文件上传、富文本、草稿这些额外能力。具体怎么得出的、和传统分层怎么对比见下一篇《一个后台项目到底能省多少代码》。十、小结用 EasyAdminBlazor 做一个后台项目的顺序可以固化下来1. 需求 → 模块清单 2. 数据库设计 → 实体EntityFull / ApprovalEntityFull 3. 每个模块AdminTable 页面 EditTemplate 4. 建菜单 角色授权功能权限 5. 需要行级隔离的实体实现 IDataPermission UseDataPermission 6. 需要批量录入ShowImportButton / ShowExportButton 7. 需要图片/附件AdminFileInput 8. 需要审核换审批基类 注册流程 审批选项卡 9. 前台页面复用同一套实体和仓储 10. 配置连接串/文件/安全/时区→ 部署 → 上线检查清单演示项目把这条链路完整走了一遍而且代码是可以直接跑的。接下一个项目时把实体换掉、页面复制改字段就是交付的起点。如果你正在用 .NET 10 Blazor 接后台项目可以拿 EasyAdminBlazor 的演示项目当脚手架它本身就是企业官网 内容后台的完整案例前台后台共用一套实体权限、数据权限、审批、Excel、文件都已接好。文档https://easyadmin.wang-zhan.com.cn/doc源码https://gitee.com/gudufy/EasyAdminBlazor