ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C# + EasyUI 实现后台管理增删改查、导出Excel与文件上传

C# + EasyUI 实现后台管理增删改查、导出Excel与文件上传 简介这是一份基于C#与EasyUI的Web开发示例面向有基础的.NET开发者演示如何在MVC项目中集成EasyUI完成后台管理页面的常见交互。项目采用VS2013编译数据库使用sqlserver2014核心围绕单张用户表实现新增、修改、删除、分页、导出Excel及图片上传等功能覆盖日常业务系统开发的高频需求单表结构也让示例更易读懂。分页默认使用sqlserver2012引入的关键字分页写法同时在UserInfoDAL.cs中预留了针对2005/2008版本的getPage2005分页方法方便不同数据库环境下直接切换参考。资源以RAR压缩包形式提供包体约28.54MB内含项目源码及配套文件可在VS中转换后打开运行适合希望快速理解EasyUI与后端交互逻辑的读者对照练习。目前已有218人学习下载可作为MVCEasyUI二次开发的入门参照。1. C# EasyUI做增删改查导出上传一条能省掉一半开发时间的路子一个后台管理系统百分之六十的页面其实就是一张业务表的增删改查外加导出和上传。C# 加 EasyUI 的组合就是专门用来把这类页面快速拼出来的后端用 C# 出接口前端用 EasyUI 的 datagrid、dialog、form 三个组件把新增、修改、删除、导出 Excel、上传文件串成一条线。这篇文章不聊泛泛的架构直接从页面骨架讲到你也能照抄的后端处理再讲那些能让你改到半夜的坑。适合正要接内部管理系统的开发也适合想从 jQuery 手写交互里抽身的新手。2. 用 C# EasyUI 把新增、修改、删除三个动作串起来2.1 页面骨架datagrid 数据表格和工具栏三个按钮先搭一个最标准的用户管理页。EasyUI 的用法是“在 HTML 上写配置”所以页面里基本看不到一堆 JS 初始化代码数据表格是一个table标签工具栏是一个div两者通过toolbar:#toolbar关联。table iddg classeasyui-datagrid stylewidth:100%;height:500px >div iddlg classeasyui-dialog stylewidth:380px;padding:15px >$(#btnAdd).click(function () { $(#fm).form(clear); $(#Id).val(0); // 0 表示新增 $(#dlg).dialog({ title: 新增 }).dialog(open); }); $(#btnEdit).click(function () { var row $(#dg).datagrid(getSelected); if (!row) { $.messager.alert(提示, 请先选择一行); return; } $(#fm).form(load, row); // 把选中行数据按 name 回填 $(#dlg).dialog({ title: 修改 }).dialog(open); }); $(#btnSave).click(function () { $(#fm).form(submit, { url: /api/user/save, onSubmit: function () { return $(this).form(validate); }, success: function (res) { var data JSON.parse(res); if (data.code 0) { $(#dlg).dialog(close); $(#dg).datagrid(reload); $.messager.show({ title: 提示, msg: 保存成功 }); } else { $.messager.alert(错误, data.msg); } } }); }); $(#btnCancel).click(function () { $(#dlg).dialog(close); });这里有一个新手很容易忽略的点form(load, row)是按表单里name属性回填的所以日期字段CreateTime如果后端返回的是/Date(1710000000000)/这种格式EasyUI 的 datebox 根本认不出来回填后输入框会显示空白。解决方式在后端C# 序列化日期时统一转成yyyy-MM-dd别让 JSON 返回它默认的日期格式。保存按钮用的是form(submit)它本质上是先序列化表单再发 AJAX 请求。EasyUI 的validate方法能拦截空值前提是输入框配置了required:true。这里账号字段已经配了角色和创建时间如果想必填也要把>$(#btnDel).click(function () { var row $(#dg).datagrid(getSelected); if (!row) { $.messager.alert(提示, 请先选择一行); return; } $.messager.confirm(确认, 确定删除这条记录吗, function (ok) { if (!ok) return; $.ajax({ url: /api/user/del, type: POST, data: { id: row.Id }, success: function (res) { if (res.code 0) { $(#dg).datagrid(reload); } else { $.messager.alert(错误, res.msg); } } }); }); });删除前加messager.confirm是血泪经验内部系统里误删一条数据恢复成本远高于那一下点击。后端也要兜底不能把防误删的压力全压在弹窗确认上。2.3 后端 C# 接收保存和删除请求接口、校验、事务不管前端怎么变C# 这边的接口要稳定。列表接口返回给 datagrid 的数据必须包含total和rows两个字段这是 EasyUI 分页渲染的硬性约定。[HttpGet] public JsonResult List(int page, int rows) { using (var db new MyDbContext()) { var total db.Users.Count(); var list db.Users .OrderByDescending(u u.Id) .Skip((page - 1) * rows) .Take(rows) .Select(u new { u.Id, u.UserName, u.RoleName, u.CreateTime }) .ToList(); return Json(new { total total, rows list }, JsonRequestBehavior.AllowGet); } }Skip和Take是分页的两个关键方法。EF 会把它们翻译成 SQL 里的OFFSET和FETCH NEXT而不是把全表数据捞到内存里再截取。注意page和rows是 datagrid 默认传过来的参数名如果你把pageSize改叫limit那接口参数也要同步调整否则永远只拿到第一页。保存接口我习惯把新增和修改合在一起判断标准就是Id是否为 0[HttpPost] public JsonResult Save(UserDto input) { if (!ModelState.IsValid) { var msg string.Join(;, ModelState.Values .SelectMany(v v.Errors) .Select(e e.ErrorMessage)); return Json(new { code 1, msg }); } using (var db new MyDbContext()) { if (input.Id 0) { var entity new User { UserName input.UserName.Trim(), RoleName input.RoleName, CreateTime DateTime.Now }; db.Users.Add(entity); } else { var entity db.Users.Find(input.Id); if (entity null) return Json(new { code 1, msg 记录不存在 }); entity.UserName input.UserName.Trim(); entity.RoleName input.RoleName; } db.SaveChanges(); return Json(new { code 0, msg ok }); } }ModelState.IsValid是 C# 这边的第一道校验。比如UserName标了[Required]传空值进来就在这里拦住返回给前端一个友好提示。这里Trim()很重要用户在输入框里多敲一个空格用户名就多了个看不见的字符后面做登录、导出、查重全都可能出问题。删除接口要留意事务。单表删除其实不需要事务但如果在删除用户的同时还要删除它的角色关联表、操作日志表就必须保证要么全成功要么全失败[HttpPost] public JsonResult Delete(int id) { using (var db new MyDbContext()) using (var tx db.Database.BeginTransaction()) { var entity db.Users.Find(id); if (entity null) return Json(new { code 1, msg 记录不存在 }); db.Users.Remove(entity); db.SaveChanges(); tx.Commit(); return Json(new { code 0 }); } }很多做过 WebForm 时代的同学会习惯自己在 SQL 里拼DELETE FROM Users WHERE Id id这个习惯在 C# 项目里建议扔掉。EF 的参数化查询能防 SQL 注入手工拼接字符串哪怕你做了类型转换也拦不住所有边界情况。3. 导出 Excel我推荐先做服务端导出再做前端导出3.1 为什么放弃“页面表格转 Excel”而用服务端生成EasyUI 的 datagrid 本身没有自带的 Excel 导出方法网上搜到的大部分方案都是遍历页面表格的 DOM把每个单元格的文本用逗号或制表符拼成一个 CSV 文件。这种方案有两个躲不开的问题第一分页的时候只能导出当前页你要导出全量数据就得先把所有页都加载出来页面直接卡死第二一旦数据里出现英文逗号、换行符、超长数字CSV 的列就会错位数字还会被 Excel 自动截断成科学计数法。所以常规做法是服务端导出。C# 这边直接查数据库把要导出的数据先放进内存再用 Excel 处理库生成文件最后以流的形式返回给浏览器。这样做的好处是导出无数据量焦虑能带样式、格式、多表头而且前端只需要一条window.open。这时候就该轮到 Excel 处理框架上场了。C# 生态里常用的是 NPOI 和 EPPlus两者都能在不开 Office 的情况下生成 .xlsx 文件。项目里如果已经有依赖直接复用没有的话用你熟的那个就行。3.2 在 C# 里导出一个 .xlsx 的最小可用代码下面这段用 NPOI 生成一个最简单的 Excel 文件。NPOI 的命名空间是NPOI.XSSF.UserModelXSSFWorkbook对应 .xlsx 格式HSSFWorkbook对应旧版 .xls 格式。using NPOI.XSSF.UserModel; using NPOI.SS.UserModel; public ActionResult ExportExcel() { var list GetUserList(); // 查全量数据不要用分页接口 var workbook new XSSFWorkbook(); var sheet workbook.CreateSheet(用户列表); IRow header sheet.CreateRow(0); header.CreateCell(0).SetCellValue(ID); header.CreateCell(1).SetCellValue(账号); header.CreateCell(2).SetCellValue(角色); header.CreateCell(3).SetCellValue(创建时间); for (int i 0; i list.Count; i) { IRow row sheet.CreateRow(i 1); row.CreateCell(0).SetCellValue(list[i].Id); row.CreateCell(1).SetCellValue(list[i].UserName); row.CreateCell(2).SetCellValue(list[i].RoleName); row.CreateCell(3).SetCellValue(list[i].CreateTime.ToString(yyyy-MM-dd HH:mm:ss)); } for (int i 0; i 4; i) sheet.AutoSizeColumn(i); var ms new MemoryStream(); workbook.Write(ms); return File(ms.ToArray(), application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, 用户列表.xlsx); }这段代码有几个关键点。CreateRow(0)是第一行也就是表头CreateCell(0)是这一行里的第一个单元格。循环里 row 从 1 开始是为了绕开表头。AutoSizeColumn会根据内容宽度自动调整列宽但它在数据量大的时候比较慢几百行无所谓几万行就会明显卡顿后面会说替代方案。return File是 MVC 里返回文件流的标准方式三个参数分别是字节数组、Content-Type、下载文件名。浏览器收到这个响应后会自动触发下载。前端触发导出只需要这样$(#btnExport).click(function () { window.open(/api/user/export); });如果导出还需要带筛选条件比如只导出某个角色就用 GET 拼接参数window.open(/api/user/export?roleadmin)后端在GetUserList()里接住条件再查。3.3 文件名、列宽、日期格式和流关闭四个细节第一个细节是文件名编码。return File直接传中文文件名在大部分浏览器里能正常下载但偶尔在 IE 或某些国产浏览器里会变成一串百分号编码。我一般会显式处理一下中文名用System.Net.Mime.ContentDisposition封装响应头var cd new System.Net.Mime.ContentDisposition { FileName HttpUtility.UrlEncode(用户列表.xlsx), Inline false }; Response.AppendHeader(Content-Disposition, cd.ToString());第二个细节是列宽。AutoSizeColumn好用但性能差大数据量导出时会发现 CPU 飙高、响应很慢。更可控的做法是手工指定列宽SetColumnWidth的第二个参数单位是 1/256 个字符宽度sheet.SetColumnWidth(0, 10 * 256); // ID 列宽约 10 个字符 sheet.SetColumnWidth(1, 20 * 256); // 账号列 sheet.SetColumnWidth(2, 12 * 256); sheet.SetColumnWidth(3, 22 * 256);第三个细节是日期格式。如果直接SetCellValue(DateTime.Now)Excel 里显示的可能是05/06/2024 08:30:00或者一串数字取决于操作系统区域设置。要统一显示格式需要给日期单元格设置数据格式ICellStyle dateStyle workbook.CreateCellStyle(); dateStyle.DataFormat workbook.CreateDataFormat().GetFormat(yyyy-mm-dd hh:mm:ss); var dateCell row.CreateCell(3); dateCell.CellStyle dateStyle; dateCell.SetCellValue(list[i].CreateTime);注意 NPOI 的格式字符串里mm是分钟HH才是 24 小时制hh是 12 小时制。时间如果是下午三点用hh显示出来是 03 而不是 15很多人导出的 Excel 时间少了 12 个小时问题就出在这。第四个细节是流关闭。上面示例用var ms new MemoryStream()没有显式释放但workbook.Write(ms)之后你把ms.ToArray()传给File框架会自动处理响应流内存流等页面请求结束也会被 GC 回收。如果你在真实项目里自己写了using (var ms new MemoryStream())注意别在using块结束后再去读ms.ToArray()否则拿到的是一个被释放的流导出会直接翻车。3.4 大数据量导出用异步和分批查询避免卡死当导出数据超过几万行前端等一个同步请求可能直接超时IIS 默认的请求超时只有 90 秒数据量大一点根本等不住。我的做法是两个方向同时下手。第一个方向是限制导出行数。导出前先查一次COUNT(*)超过比如 5 万行就提示用户缩小范围让用户自己去加筛选条件。内部系统里很少有人真的需要一次性导出一百万行就算真有打开 Excel 也会卡成幻灯片。第二个方向是异步导出。C# 里可以用Task.Run把耗时的查询和文件生成放到后台线程前端先显示“正在生成”等后台处理完再提供一个下载链接。这里会用到 C# 委托或者轮询去通知前端。如果想做得简单一点直接在导出接口里分页查数据库每页 5000 行边查边写 Excel避免一次性ToList()把几十万条数据压进内存public async TaskActionResult ExportExcelAsync() { var workbook new XSSFWorkbook(); var sheet workbook.CreateSheet(用户列表); int rowIndex 0; for (int page 1; ; page) { var pageData await db.Users .OrderBy(u u.Id) .Skip((page - 1) * 5000) .Take(5000) .Select(...) .ToListAsync(); if (pageData.Count 0) break; foreach (var item in pageData) { IRow row sheet.CreateRow(rowIndex); // 写单元格 } } ... }async加await的好处是请求线程在等待数据库查询时不会被一直占着IIS 能把线程让给其他请求。这是一个常用但很多人懒得做的优化。4. 上传文件EasyUI 只负责选文件C# 才是把关人4.1 前端 filebox 组件与 FormData 提交方式EasyUI 的上传入口一般用easyui-filebox组件。它不是一个原生文件控件而是一个文本框加一个选择按钮的组合配合 LinkButton 做上传动作。input idfileBox classeasyui-filebox namefile >$(#btnUpload).click(function () { var file $(#fileBox).filebox(getValue); if (!file) { $.messager.alert(提示, 请选择文件); return; } var fileInput $(#fileBox).filebox(files)[0]; if (!fileInput) { $.messager.alert(错误, 读取文件失败请重新选择); return; } var formData new FormData(); formData.append(file, fileInput); $.ajax({ url: /api/file/upload, type: POST, data: formData, contentType: false, processData: false, success: function (res) { if (res.code 0) { $.messager.show({ title: 提示, msg: 上传成功 }); // 把返回的路径回显到页面上 $(#fileUrl).val(res.url); } else { $.messager.alert(错误, res.msg); } } }); });这里最容易被忽略的是contentType:false和processData:false两个配置。jQuery 默认会把对象变成keyvalue的查询字符串但 FormData 需要原样交给浏览器处理所以必须关掉 jQuery 的干预。如果你用老版本的 jQuery可能还需要cache:false。filebox(files)[0]在不同 EasyUI 版本里兼容性有点玄学有的版本要写成$(#fileBox).filebox(files)的返回值有的版本需要绕到底层 file input 取。如果发现files[0]取不到就退一步var fileInput $(#fileBox).next().find(input[typefile])[0].files[0];但那是因为 EasyUI 渲染层级变化了本质上还是拿input typefile的 DOM 文件对象。4.2 C# 后端处理 Request.Files大小、类型、重命名、目录后端接收文件的通用入口是Request.Files。注意前端formData.append(file, fileInput)这里的file就是后端读取时的 key。[HttpPost] public JsonResult Upload() { var file Request.Files[file]; if (file null || file.ContentLength 0) return Json(new { code 1, msg 没有接收到文件 }); int maxSize 10 * 1024 * 1024; if (file.ContentLength maxSize) return Json(new { code 1, msg 文件超过10M限制 }); string ext Path.GetExtension(file.FileName).ToLower(); var allowed new Liststring { .xlsx, .xls, .jpg, .png, .pdf }; if (!allowed.Contains(ext)) return Json(new { code 1, msg 文件类型不允许 }); string dir Server.MapPath(~/Upload); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); string newName DateTime.Now.ToString(yyyyMMddHHmmss) _ Guid.NewGuid().ToString(N).Substring(0, 8) ext; string savePath Path.Combine(dir, newName); file.SaveAs(savePath); return Json(new { code 0, url /Upload/ newName }); }Path.GetExtension拿到的扩展名可能和用户看到的文件名不一致比如user.txt.exe会拿到.exe所以扩展名白名单比黑名单可靠得多。任何不在白名单里的都直接拒绝包括空扩展名。重命名这里用时间加随机串是为了避免两个问题一是用户上传的文件名里含中文或特殊字符保存后有可能触发路径问题二是如果直接把用户文件名落盘下一个用户上传同名文件会覆盖旧文件。用Guid基本不可能重复再叠加时间戳就更稳。还有一个安全细节值得写进代码里。不要用原始文件名拼路径防止../../evil.aspx这类路径穿越。上面用了Path.Combine加随机新文件名已经规避了这个问题。4.3 上传成功之后回显、删除旧文件、关联业务记录上传文件本身不是终点文件要和业务数据关联。最常用的做法是上传成功后只返回一个相对路径Upload/xxx.xlsx前端把它放进隐藏字段等保存表单时一起提交到数据库。input typehidden nameAttachUrl idfileUrl value /如果是图片上传上传成功后要立即回显预览图。可以直接把返回的url赋给img的src但要注意目录权限。IIS 里如果没给 Upload 目录配置静态文件读取权限浏览器会报 404图片显示不出来。这一点在部署环节经常被漏掉开发环境跑得好好的发布到服务器就挂。修改业务记录时如果用户上传了新文件旧文件就要考虑删掉。删除的时机非常重要我习惯先保存数据库记录成功之后再删旧文件顺序不能反。如果你先删了旧文件数据库保存失败页面上显示的还是旧文件的下载链接但文件已经没了用户会看到 404。// 先保存新记录 db.SaveChanges(); // 再删旧文件 if (!string.IsNullOrEmpty(oldAttachUrl)) { string oldPath Server.MapPath(~/ oldAttachUrl); if (System.IO.File.Exists(oldPath)) System.IO.File.Delete(oldPath); }如果业务需求是保留历史版本那就不要删旧文件而是把文件路径存成一对多的子表这样每一次修改都有迹可查用户误传了文件也能翻回去下载之前的版本。5. C# EasyUI 联调最常见的坑我都替你踩过5.1 现象工具栏按钮点了没反应新增、修改、删除三个按钮放在 toolbar 里页面加载完成后点击没有任何响应打开浏览器控制台发现要么没报错要么报了xxx is not a function。原因一般是两个。第一个是 JS 代码在 DOM 渲染完成之前绑定了事件EasyUI 的组件是页面加载后才初始化的你的$(#btnAdd).click()执行时按钮还没渲染出来事件自然绑不上。解决方法是把脚本放到$(function () { ... })里面或者直接放到页面底部。第二个原因是 EasyUI 版本兼容。老版本里 LinkButton 渲染后的 DOM 结构和新版不同plain:true之后外层包了一层样式容器直接绑定#btnAdd虽然匹配得到a本身但按钮点击区域实际是被一个新生成的span覆盖的。我一般用事件委托把点击绑定到 toolbar 容器上$(#toolbar).on(click, #btnAdd, function () { ... });这样即使 EasyUI 内部 DOM 怎么重组事件都不会丢。5.2 现象修改时表单回填不了日期框显示空白选中一行点修改用户名和角色都正常回填了创建时间输入框却是空的。打开网络面板看接口返回的 JSON日期字段是CreateTime:\/Date(1710000000000)\/这种格式。原因是 C# 的 JavaScriptSerializer 默认把DateTime序列化成这种带毫秒时间戳的字符串EasyUI 的 datebox 只认yyyy-MM-dd这种文本格式空字符串还算好的有的版本会直接报格式错误。解决方式有两种。一种是在 controller 里用JsonConvert设置日期格式var settings new JsonSerializerSettings { DateFormatString yyyy-MM-dd HH:mm:ss }; return Json(JsonConvert.SerializeObject(data, settings));另一种是给日期字段定义一个专门的字符串属性去接收格式化结果比如在 DTO 里写public string CreateTimeText CreateTime?.ToString(yyyy-MM-dd HH:mm:ss);然后在 datagrid 的列字段里写field:CreateTimeText。这个方法看着笨但在老项目里最不容易误伤其他逻辑。5.3 现象Excel 导出下来打不开或者中文全乱码导出接口返回一个 .xlsx 文件下载后双击提示文件损坏或者导出的是 CSV用 Excel 打开中文全是âæ¥之类的乱码。先说文件损坏。很多人在 MVC 里直接写return Content(csvText, application/vnd.ms-excel)把 CSV 文本当响应返回。CSV 本质是文本但 Excel 打开时对分隔符和编码非常敏感。更稳妥的方式是让 C# 生成真正的 .xlsx用 NPOI 或 EPPlus 写进 MemoryStream 再return File而不是手动拼字符串。再说乱码。如果是 CSVExcel 默认按系统 ANSI 编码打开你输出的 UTF-8 字节流在中文 Windows 上会乱码。解决办法是给 CSV 加 UTF-8 BOM 头var bytes Encoding.UTF8.GetPreamble() .Concat(Encoding.UTF8.GetBytes(csvText)) .ToArray(); return File(bytes, text/csv, 导出.csv);如果你用 NPOI 导出 .xlsx一般不会遇到中文乱码因为 xlsx 内部就是 XML编码是固定的。更多的问题是列内容超长被截断或者 Excel 把长数字当成科学计数法。数字列要么设置单元格格式为文本要么存成字符串row.CreateCell(0).SetCellValue(item.Id.ToString());5.4 现象上传稍大的文件就提示“超过最大请求长度”本地测试传个 10MB 的压缩包没问题传到服务器上 2MB 就弹 500日志里写着Maximum request length exceeded。原因是 ASP.NET 和 IIS 对请求体大小有两个独立的限制只改一个不够。在web.config里要同时改这两处system.web httpRuntime maxRequestLength10485760 / /system.web system.webServer security requestFiltering requestLimits maxAllowedContentLength10485760 / /requestFiltering /security /system.webServermaxRequestLength单位是 KBmaxAllowedContentLength单位是字节两个值都要大于你要支持的文件大小。IIS 的这个限制在请求进入应用之前就会拦截所以你连断点都打不上只能看 IIS 日志。还要注意服务器上的临时目录空间。文件超过一定大小ASP.NET 会把请求体缓存到临时目录如果服务器 C 盘空间不足一样会报错。5.5 现象保存时没报错但数据变成新增了或者串行了点修改表单里也显示了旧数据保存后数据库里新旧两条记录同时存在或者原记录被覆盖了。原因基本都出在Id字段。修改回填后表单里的隐藏Id字段没有值或者值不是数据库主键。EasyUI 的form(load, row)只回填表单里有name的字段如果你的隐藏字段写的是id而列表返回的是Id大小写对不上回填就失效。检查方法很简单保存前在控制台打印一下console.log($(#fm).serialize());如果Id是空那无论如何都会走新增分支。有时候 datagrid 列字段是id小写表单输入框name是Id大写EasyUI 内部用字典匹配不区分大小写在某些版本里能用在另一些版本里就失效。最好的办法是列表 SQL 和 DTO 统一用相同的大小写前端隐藏字段和列字段也保持一致不要一会儿Id一会儿id。6. 给这套增删改查再补三件事才算真正写完如果你照着前面的步骤做完了新增、修改、删除、导出 Excel、上传整套流程系统已经能用了。但以我做过这么多内部系统的经验还要再补三件事不然上线后你会在半夜被电话叫醒。第一件事是统一返回值。前端所有 AJAX 请求都判断res.code 0那这个res就必须在所有接口里保持一致。不要这个接口返回{ code: 0 }那个接口返回{ success: true }写前端的人每接一个接口就要看一次文档迟早会漏。我在项目里会定义一个简单的返回类public class ApiResult { public int Code { get; set; } public string Msg { get; set; } public object Data { get; set; } public static ApiResult Ok(object data null) new ApiResult { Code 0, Msg ok, Data data }; public static ApiResult Fail(string msg) new ApiResult { Code 1, Msg msg }; }所有接口统一return Json(ApiResult.Ok(data))或者return Json(ApiResult.Fail(记录不存在))。前端只需处理一种结构后续加日志、加权限都好办。第二件事是操作日志。增删改查别看简单每一条数据都是用户产生的。谁在什么时间改了哪条记录最好在 Save 和 Delete 方法里留痕。不用做复杂的设计一张日志表 一个日志方法就够了public void WriteLog(string action, string detail, int operatorId) { // 插入 OperationLog 表 }如果不想侵入业务代码可以考虑 AOP 或过滤器去统一拦截但小项目手写也完全够用。日志的价值不在于日常查看在于出问题那天你能快速定位是谁改了数据。第三件事是对耗时操作做异步。导出 Excel、上传大文件、批量删除这些操作能async就async能Task.Run就配合委托去通知前端。C# 的Task和delegate这两样东西做上位机开发的时候可能天天用做 Web 开发反而被很多人忽略。其实 IOCP 线程池的调度方式恰恰最适合这种需要线程切换的 IO 场景。我的习惯是每写完一个页面都问自己三个问题如果用户重复点了两次新增按钮会不会产生脏数据如果保存失败日志里能不能看到失败原因如果这个接口响应超过 10 秒用户会看到什么把这三个问题都想清楚再简单的增删改查也不会翻车。这套 C# 加 EasyUI 的组合我用过很多年说不上惊艳但胜在稳定、好招人、好维护。前端不追求花哨后端不搞复杂架构把每一个动作的输入、输出、异常处理都写清楚就已经身份了。希望帮到你。本文还有配套的精品资源点击获取
返回列表