From ae79ca9aa13458ed51f913ae2c282886b1962335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=9F=B3=E5=A4=B4?= Date: Wed, 6 Nov 2024 00:00:35 +0800 Subject: [PATCH] =?UTF-8?q?EntityController=E6=8F=90=E5=8F=96=E5=85=B1?= =?UTF-8?q?=E6=80=A7=E4=BB=A3=E7=A0=81=EF=BC=8CMVC=E4=B8=8EAPI=E5=85=B1?= =?UTF-8?q?=E7=94=A8=E4=BA=8EEntityController2=EF=BC=8C=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4=E5=B7=A5=E4=BD=9C=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NewLife.Cube/Common/EntityController.cs | 579 ++------------ NewLife.Cube/Common/EntityController2.cs | 238 ++++++ NewLife.Cube/Common/EntityTreeController.cs | 2 +- .../Common/ReadOnlyEntityController.cs | 733 ------------------ .../Common/ReadOnlyEntityController2.cs | 2 +- NewLife.CubeNC/Common/EntityController.cs | 285 +------ .../Common/ReadOnlyEntityController.cs | 3 - NewLife.CubeNC/NewLife.CubeNC.csproj | 1 + 8 files changed, 325 insertions(+), 1518 deletions(-) create mode 100644 NewLife.Cube/Common/EntityController2.cs diff --git a/NewLife.Cube/Common/EntityController.cs b/NewLife.Cube/Common/EntityController.cs index 2a7196a4..032a61b0 100644 --- a/NewLife.Cube/Common/EntityController.cs +++ b/NewLife.Cube/Common/EntityController.cs @@ -5,29 +5,17 @@ using NewLife.Data; using NewLife.Log; using NewLife.Reflection; +using NewLife.Remoting; using XCode; using XCode.Membership; namespace NewLife.Cube; -/// 实体控制器基类 -/// -public class EntityController : EntityController where TEntity : Entity, new() { } - /// 实体控制器基类 /// 实体类型 /// 数据模型,用于接口数据传输 -public class EntityController : ReadOnlyEntityController where TEntity : Entity, new() +public partial class EntityController : ReadOnlyEntityController where TEntity : Entity, new() { - #region 属性 - private String CacheKey => $"CubeView_{typeof(TEntity).FullName}"; - #endregion - - #region 构造 - /// 实例化 - public EntityController() => PageSetting.IsReadOnly = false; - #endregion - #region 默认Action /// 删除数据 /// @@ -37,30 +25,22 @@ namespace NewLife.Cube; [HttpDelete("/[area]/[controller]")] public virtual ApiResponse Delete([Required] String id) { + var act = "删除"; var entity = FindData(id); - var rs = false; - var err = ""; try { - if (Valid(entity, DataObjectMethodType.Delete, true)) - { - OnDelete(entity); - - rs = true; - } - else - err = "验证失败"; + act = ProcessDelete(entity); + + return new ApiResponse(0, $"{act}成功!", entity); } catch (Exception ex) { - err = ex.GetTrue().Message; + var code = ex is ApiException ae ? ae.Code : 500; + var err = ex.GetTrue().Message; WriteLog("Delete", false, err); - } - if (rs) - return new ApiResponse(0, "删除成功!", entity); - else - return new ApiResponse(500, "删除失败!" + err, entity); + return new ApiResponse(code, $"{act}失败!" + err, entity); + } } /// 添加数据 @@ -72,15 +52,14 @@ public virtual ApiResponse Delete([Required] String id) public virtual async Task> Insert(TModel model) { // 实例化实体对象,然后拷贝 - if (model is not TEntity entity) - { - entity = Factory.Create(false) as TEntity; + if (model is TEntity entity) return await Insert(entity); - if (model is IModel src) - entity.CopyFrom(src, true); - else - entity.Copy(model); - } + entity = Factory.Create(false) as TEntity; + + if (model is IModel src) + entity.CopyFrom(src, true); + else + entity.Copy(model); return await Insert(entity); } @@ -95,49 +74,35 @@ public virtual async Task> Insert(TEntity entity) if (Factory.Unique.IsIdentity && entity[Factory.Unique.Name].ToInt() != 0) throw new Exception("我们约定添加数据时路由id部分默认没有数据,以免模型绑定器错误识别!"); - var rs = false; - var err = ""; try { - if (Valid(entity, DataObjectMethodType.Insert, true)) - { - //SaveFiles(entity); + if (!Valid(entity, DataObjectMethodType.Insert, true)) + throw new Exception("验证失败"); - OnInsert(entity); + OnInsert(entity); - // 先插入再保存附件,主要是为了在附件表关联业务对象主键 - var fs = await SaveFiles(entity); - if (fs.Count > 0) OnUpdate(entity); + // 先插入再保存附件,主要是为了在附件表关联业务对象主键 + var fs = await SaveFiles(entity); + if (fs.Count > 0) OnUpdate(entity); - if (LogOnChange) LogProvider.Provider.WriteLog("Insert", entity); + if (LogOnChange) LogProvider.Provider.WriteLog("Insert", entity); - rs = true; - } - else - err = "验证失败"; + return new ApiResponse(0, "添加成功!", entity); } catch (Exception ex) { - err = ex.Message; - ModelState.AddModelError((ex as ArgumentException)?.ParamName ?? "", ex.Message); - } + var code = ex is ApiException ae ? ae.Code : 500; + var msg = ex.Message; - var msg = ""; - if (!rs) - { - WriteLog("Add", false, err); + WriteLog("Add", false, msg); - msg = SysConfig.Develop ? ("添加失败!" + err) : "添加失败!"; + msg = SysConfig.Develop ? ("添加失败!" + msg) : "添加失败!"; // 添加失败,ID清零,否则会显示保存按钮 - entity[Entity.Meta.Unique.Name] = 0; + entity[Factory.Unique.Name] = 0; - return new ApiResponse(500, msg, null); + return new ApiResponse(code, msg, entity); } - - msg = "添加成功!"; - - return new ApiResponse(0, msg, entity); } /// 更新数据 @@ -149,19 +114,18 @@ public virtual async Task> Insert(TEntity entity) public virtual async Task> Update(TModel model) { // 实例化实体对象,然后拷贝 - if (model is not TEntity entity) - { - var uk = Factory.Unique; - var key = model is IModel ext ? ext[uk.Name] : model.GetValue(uk.Name); + if (model is TEntity entity) return await Update(entity); - // 先查出来,再拷贝。这里没有考虑脏数据的问题,有可能拷贝后并没有脏数据 - entity = FindData(key); + var uk = Factory.Unique; + var key = model is IModel ext ? ext[uk.Name] : model.GetValue(uk.Name); - if (model is IModel src) - entity.CopyFrom(src, true); - else - entity.Copy(model, false, uk.Name); - } + // 先查出来,再拷贝。这里没有考虑脏数据的问题,有可能拷贝后并没有脏数据 + entity = FindData(key); + + if (model is IModel src) + entity.CopyFrom(src, true); + else + entity.Copy(model, false, uk.Name); return await Update(entity); } @@ -172,352 +136,30 @@ public virtual async Task> Update(TModel model) [NonAction] public virtual async Task> Update(TEntity entity) { - var rs = false; - var err = ""; try { - if (Valid(entity, DataObjectMethodType.Update, true)) - { - await SaveFiles(entity); + if (!Valid(entity, DataObjectMethodType.Update, true)) + throw new Exception("验证失败"); + + await SaveFiles(entity); - OnUpdate(entity); + OnUpdate(entity); - rs = true; - } - else - err = "验证失败"; + return new ApiResponse(0, "保存成功!", entity); } catch (Exception ex) { - err = ex.Message; + var code = ex is ApiException ae ? ae.Code : 500; + var err = ex.Message; ModelState.AddModelError((ex as ArgumentException)?.ParamName ?? "", ex.Message); - } - - Object id = null; - if (Factory.Unique != null) id = entity[Factory.Unique.Name]; - var msg = ""; - if (!rs) - { WriteLog("Edit", false, err); - msg = SysConfig.Develop ? ("保存失败!" + err) : "保存失败!"; - - return new ApiResponse(500, msg, null); - } - else - { - msg = "保存成功!"; + err = SysConfig.Develop ? ("保存失败!" + err) : "保存失败!"; - return new ApiResponse(0, msg, entity); + return new ApiResponse(code, err, null); } } - - /// 保存所有上传文件 - /// 实体对象 - /// 上传目录。为空时默认UploadPath配置 - /// - protected virtual async Task> SaveFiles(TEntity entity, String uploadPath = null) - { - var rs = new List(); - - if (!Request.HasFormContentType) return rs; - - var files = Request.Form.Files; - var fields = Factory.Fields; - foreach (var fi in fields) - { - var dc = fi.Field; - if (dc.IsAttachment()) - { - // 允许一次性上传多个文件到服务端 - var list = new List(); - foreach (var file in files) - { - if (file.Name.EqualIgnoreCase(fi.Name, fi.Name + "_attachment")) - { - var att = await SaveFile(entity, file, uploadPath, null); - if (att != null) - { - var url = ViewHelper.GetAttachmentUrl(att); - list.Add(url); - rs.Add(url); - } - } - } - - if (list.Count > 0) entity.SetItem(fi.Name, list.Join(";")); - } - } - - return rs; - } - - /// 保存单个文件 - /// 实体对象 - /// 文件 - /// 上传目录,默认使用UploadPath配置 - /// 文件名,如若指定则忽略前面的目录 - /// - protected virtual async Task SaveFile(TEntity entity, IFormFile file, String uploadPath, String fileName) - { - if (fileName.IsNullOrEmpty()) fileName = file.FileName; - - using var span = DefaultTracer.Instance?.NewSpan(nameof(SaveFile), fileName ?? file.FileName); - - var id = Factory.Unique != null ? entity[Factory.Unique] : null; - var att = new Attachment - { - Category = typeof(TEntity).Name, - Key = id + "", - Title = entity + "", - //FileName = fileName ?? file.FileName, - ContentType = file.ContentType, - Size = file.Length, - Enable = true, - UploadTime = DateTime.Now, - }; - - if (id != null) - { - var ss = GetControllerAction(); - att.Url = $"/{ss[0]}/{ss[1]}/Detail/{id}"; - } - - var rs = false; - var msg = ""; - try - { - rs = await att.SaveFile(file.OpenReadStream(), uploadPath, fileName); - } - catch (Exception ex) - { - rs = false; - msg = ex.Message; - span?.SetError(ex, att); - - throw; - } - finally - { - // 写日志 - var type = entity.GetType(); - var log = LogProvider.Provider.CreateLog(type, "上传", rs, $"上传 {file.FileName} ,目录 {uploadPath} ,保存为 {att.FilePath} " + msg, 0, null, UserHost); - log.LinkID = id.ToLong(); - log.SaveAsync(); - } - - return att; - } - - ///// 批量启用 - ///// 主键集合 - ///// 操作原因 - ///// - //[EntityAuthorize(PermissionFlags.Update)] - //[HttpPost] - //public virtual ActionResult EnableSelect(String keys, String reason) => EnableOrDisableSelect(true, reason); - - ///// 批量禁用 - ///// 主键集合 - ///// 操作原因 - ///// - //[EntityAuthorize(PermissionFlags.Update)] - //[HttpPost] - //public virtual ActionResult DisableSelect(String keys, String reason) => EnableOrDisableSelect(false, reason); - - /// - /// 批量启用或禁用 - /// - /// 启用/禁用 - /// 操作原因 - /// - protected virtual ActionResult EnableOrDisableSelect(Boolean isEnable, String reason) - { - var count = 0; - var ids = GetRequest("keys").SplitAsInt(); - var fields = Factory.AllFields; - if (ids.Length > 0 && fields.Any(f => f.Name.EqualIgnoreCase("enable"))) - { - var log = LogProvider.Provider; - foreach (var id in ids) - { - var entity = Factory.Find("ID", id); - if (entity != null && entity["Enable"].ToBoolean() != isEnable) - { - entity.SetItem("Enable", isEnable); - - log.WriteLog("Update", entity); - log.WriteLog(entity.GetType(), isEnable ? "Enable" : "Disable", true, reason); - - entity.Update(); - - Interlocked.Increment(ref count); - } - } - } - - return Json(0, $"共{(isEnable ? "启用" : "禁用")}[{count}]个"); - } - #endregion - - #region 高级Action - ///// 导入Excel - ///// 当前采用前端解析的excel,表头第一行数据无效,从第二行开始处理 - ///// - //[EntityAuthorize(PermissionFlags.Insert)] - //[DisplayName("导入Excel")] - //[HttpPost] - //public virtual ActionResult ImportExcel(String data) - //{ - // if (String.IsNullOrWhiteSpace(data)) return Json(500, null, $"“{nameof(data)}”不能为 null 或空白。"); - // try - // { - // var fact = Factory; - // var dal = fact.Session.Dal; - // var type = Activator.CreateInstance(fact.EntityType); - // var json = new JsonParser(data); - // var dataList = json.Decode() as IList; - - - // //解析json - // //var dataList = JArray.Parse(data); - // var errorString = String.Empty; - // Int32 okSum = 0, fiSum = 0; - - // //using var tran = Entity.Meta.CreateTrans(); - // foreach (var itemD in dataList) - // { - // var item = itemD.ToDictionary(); - // if (item[fact.Fields[1].Name].ToString() == fact.Fields[1].DisplayName) //判断首行是否为标体列 - // continue; - - // //检查主字段是否重复 - // if (Entity.Find(fact.Master.Name, item[fact.Master.Name].ToString()) == null) - // { - // //var entity = item.ToJson().ToJsonEntity(fact.EntityType); - // var entity = fact.Create(); - - // foreach (var fieldsItem in fact.Fields) - // { - // if (!item.ContainsKey(fieldsItem.Name)) - // { - // if (!fieldsItem.IsNullable) - // fieldsItem.FromExcelToEntity(item, entity); - // } - // else - // fieldsItem.FromExcelToEntity(item, entity); - // } - - // if (fact.FieldNames.Contains("CreateTime")) - // entity["CreateTime"] = DateTime.Now; - - // if (fact.FieldNames.Contains("CreateIP")) - // entity["CreateIP"] = "--"; - - // okSum += fact.Session.Insert(entity); - // } - // else - // { - // errorString += $"
{item[fact.Master.Name]}重复"; - // fiSum++; - // } - // } - - // //tran.Commit(); - - // WriteLog("导入Excel", true, $"导入Excel[{data}]({dataList.Count()}行)成功!"); - - // return Json(0, $"导入成功:({okSum}行),失败({fiSum}行)!{errorString}"); - // } - // catch (Exception ex) - // { - // XTrace.WriteException(ex); - - // WriteLog("导入Excel", false, ex.GetMessage()); - - // return Json(500, ex.GetMessage(), ex); - // } - //} - #endregion - - #region 批量删除 - ///// 删除选中 - ///// - //[EntityAuthorize(PermissionFlags.Delete)] - //[DisplayName("删除选中")] - //[HttpPost] - //public virtual ActionResult DeleteSelect() - //{ - // var count = 0; - // var keys = SelectKeys; - // if (keys != null && keys.Length > 0) - // { - // using var tran = Entity.Meta.CreateTrans(); - // var list = new List(); - // foreach (var item in keys) - // { - // var entity = Entity.FindByKey(item); - // if (entity != null) - // { - // // 验证数据权限 - // if (Valid(entity, DataObjectMethodType.Delete, true)) list.Add(entity); - - // count++; - // } - // } - // list.Delete(); - // tran.Commit(); - // } - // return JsonRefresh($"共删除{count}行数据"); - //} - - ///// 删除全部 - ///// - //[EntityAuthorize(PermissionFlags.Delete)] - //[DisplayName("删除全部")] - //[HttpPost] - //public virtual ActionResult DeleteAll() - //{ - // var url = Request.GetReferer(); - - // var count = 0; - // var p = Session[CacheKey] as Pager; - // p = new Pager(p); - // if (p != null) - // { - // // 循环多次删除 - // for (var i = 0; i < 10; i++) - // { - // p.PageIndex = i + 1; - // p.PageSize = 100_000; - // // 不要查记录数 - // p.RetrieveTotalCount = false; - - // var list = SearchData(p).ToList(); - // if (list.Count == 0) break; - - // count += list.Count; - // //list.Delete(); - // using var tran = Entity.Meta.CreateTrans(); - // var list2 = new List(); - // foreach (var entity in list) - // { - // // 验证数据权限 - // if (Valid(entity, DataObjectMethodType.Delete, true)) list2.Add(entity); - // } - // list2.Delete(); - // tran.Commit(); - // } - // } - - // if (Request.IsAjaxRequest()) - // return JsonRefresh($"共删除{count}行数据"); - // else if (!url.IsNullOrEmpty()) - // return Redirect(url); - // else - // return RedirectToAction("Index"); - //} #endregion #region 实体操作重载 @@ -536,123 +178,4 @@ protected virtual ActionResult EnableOrDisableSelect(Boolean isEnable, String re /// protected virtual Int32 OnDelete(TEntity entity) => entity.Delete(); #endregion - - #region 同步/还原 - ///// 同步数据 - ///// - //[EntityAuthorize(PermissionFlags.Insert)] - //[DisplayName("同步{type}")] - //[HttpPost] - //public async Task Sync() - //{ - // //if (id.IsNullOrEmpty()) return RedirectToAction(nameof(Index)); - - // // 读取系统配置 - // var ps = Parameter.FindAllByUserID(ManageProvider.User.ID); // UserID=0 && Category=Sync - // ps = ps.Where(e => e.Category == "Sync").ToList(); - // var server = ps.FirstOrDefault(e => e.Name == "Server")?.Value; - // var token = ps.FirstOrDefault(e => e.Name == "Token")?.Value; - // var models = ps.FirstOrDefault(e => e.Name == "Models")?.Value; - - // if (server.IsNullOrEmpty()) throw new ArgumentNullException("未配置 Sync:Server"); - // if (token.IsNullOrEmpty()) throw new ArgumentNullException("未配置 Sync:Token"); - // if (models.IsNullOrEmpty()) throw new ArgumentNullException("未配置 Sync:Models"); - - // var mds = models.Split(","); - - // //// 创建实体工厂 - // //var etype = mds.FirstOrDefault(e => e.Replace(".", "_") == id); - // //var fact = etype.GetTypeEx()?.AsFactory(); - // //if (fact == null) throw new ArgumentNullException(nameof(id), "未找到模型 " + id); - - // // 找到控制器,以识别动作地址 - // var cs = GetControllerAction(); - // var ctrl = cs[0].IsNullOrEmpty() ? cs[1] : $"{cs[0]}/{cs[1]}"; - // if (!mds.Contains(ctrl)) throw new InvalidOperationException($"[{ctrl}]未配置为允许同步 Sync:Models"); - - // // 创建客户端,准备发起请求 - // var url = server.EnsureEnd("/") + $"{ctrl}/Json/{token}?PageSize=100000"; - - // var http = new HttpClient - // { - // BaseAddress = new Uri(url) - // }; - - // var sw = Stopwatch.StartNew(); - - // var list = await http.InvokeAsync(HttpMethod.Get, null); - - // sw.Stop(); - - // var fact = Factory; - // XTrace.WriteLine("[{0}]共同步数据[{1:n0}]行,耗时{2:n0}ms,数据源:{3}", fact.EntityType.FullName, list.Length, sw.ElapsedMilliseconds, url); - - // var arrType = fact.EntityType.MakeArrayType(); - // if (list.Length > 0) - // { - // XTrace.WriteLine("[{0}]准备覆盖写入[{1}]行数据", fact.EntityType.FullName, list.Length); - // using var tran = fact.Session.CreateTrans(); - - // // 清空 - // try - // { - // fact.Session.Truncate(); - // } - // catch (Exception ex) { XTrace.WriteException(ex); } - - // // 插入 - // //ms.All(e => { e.AllChilds = new List(); return true; }); - // fact.AllowInsertIdentity = true; - // //ms.Insert(); - // //var empty = typeof(List<>).MakeGenericType(fact.EntityType).CreateInstance(); - // foreach (IEntity entity in list) - // { - // if (entity is IEntityTree tree) tree.AllChilds.Clear(); - - // entity.Insert(); - // } - // fact.AllowInsertIdentity = false; - - // tran.Commit(); - // } - - // return Index(); - //} - - ///// 从服务器本地目录还原 - ///// - //[EntityAuthorize(PermissionFlags.Insert)] - //[DisplayName("还原")] - //[HttpPost] - //public virtual ActionResult Restore() - //{ - // try - // { - // var fact = Factory; - // var dal = fact.Session.Dal; - - // var name = GetType().Name.TrimEnd("Controller"); - // var fileName = $"{name}_*.gz"; - - // var di = NewLife.Setting.Current.BackupPath.GetBasePath().AsDirectory(); - // //var fi = di?.GetFiles(fileName)?.LastOrDefault(); - // var fi = di?.GetFiles(fileName)?.OrderByDescending(e => e.Name).FirstOrDefault(); - // if (fi == null || !fi.Exists) throw new XException($"找不到[{fileName}]的备份文件"); - - // var rs = dal.Restore(fi.FullName, fact.Table.DataTable); - - // WriteLog("恢复", true, $"恢复[{fileName}]({rs:n0}行)成功!"); - - // return Json(0, $"恢复[{fileName}]({rs:n0}行)成功!"); - // } - // catch (Exception ex) - // { - // XTrace.WriteException(ex); - - // WriteLog("恢复", false, ex.GetMessage()); - - // return Json(500, null, ex); - // } - //} - #endregion } \ No newline at end of file diff --git a/NewLife.Cube/Common/EntityController2.cs b/NewLife.Cube/Common/EntityController2.cs new file mode 100644 index 00000000..d341ae4c --- /dev/null +++ b/NewLife.Cube/Common/EntityController2.cs @@ -0,0 +1,238 @@ +using System.ComponentModel; +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using NewLife.Cube.Common; +using NewLife.Cube.Entity; +using NewLife.Cube.Extensions; +using NewLife.Cube.ViewModels; +using NewLife.Data; +using NewLife.Log; +using NewLife.Reflection; +using NewLife.Remoting; +using NewLife.Serialization; +using NewLife.Web; +using XCode; +using XCode.Configuration; +using XCode.Membership; + +namespace NewLife.Cube; + +/// 实体控制器基类 +/// +public class EntityController : EntityController where TEntity : Entity, new() { } + +/// 实体控制器基类 +/// +/// +public partial class EntityController : ReadOnlyEntityController where TEntity : Entity, new() +{ + #region 构造 + /// 实例化 + public EntityController() => PageSetting.IsReadOnly = false; + #endregion + + #region 默认Action + private String ProcessDelete(TEntity entity) + { + // 假删除与还原 + var act = "删除"; + var fi = GetDeleteField(); + if (fi != null) + { + var restore = GetRequest("restore").ToBoolean(); + entity.SetItem(fi.Name, !restore); + if (restore) act = "恢复"; + + if (!Valid(entity, DataObjectMethodType.Update, true)) + throw new Exception("验证失败"); + + OnUpdate(entity); + } + else + { + if (!Valid(entity, DataObjectMethodType.Delete, true)) + throw new Exception("验证失败"); + + OnDelete(entity); + } + + return act; + } + + private static FieldItem GetDeleteField() => Factory.Fields.FirstOrDefault(e => e.Name.EqualIgnoreCase("Deleted", "IsDelete", "IsDeleted") && e.Type == typeof(Boolean)); + + /// 保存所有上传文件 + /// 实体对象 + /// 上传目录。为空时默认UploadPath配置 + /// + protected virtual async Task> SaveFiles(TEntity entity, String uploadPath = null) + { + var rs = new List(); + var list = new List(); + + if (!Request.HasFormContentType) return list; + + var files = Request.Form.Files; + var fields = Factory.Fields; + foreach (var fi in fields) + { + var dc = fi.Field; + if (dc.IsAttachment()) + { + // 允许一次性上传多个文件到服务端 + foreach (var file in files) + { + if (file.Name.EqualIgnoreCase(fi.Name, fi.Name + "_attachment")) + { + var att = await SaveFile(entity, file, uploadPath, null); + if (att != null) + { + var url = ViewHelper.GetAttachmentUrl(att); + list.Add(url); + rs.Add(url); + } + } + } + + if (list.Count > 0) + { + entity.SetItem(fi.Name, list.Join(";")); + list.Clear(); + } + } + } + + return rs; + } + + /// 保存单个文件 + /// 实体对象 + /// 文件 + /// 上传目录,默认使用UploadPath配置 + /// 文件名,如若指定则忽略前面的目录 + /// + protected virtual async Task SaveFile(TEntity entity, IFormFile file, String uploadPath, String fileName) + { + if (fileName.IsNullOrEmpty()) fileName = file.FileName; + + using var span = DefaultTracer.Instance?.NewSpan(nameof(SaveFile), new { name = file.Name, fileName, uploadPath }); + + var id = Factory.Unique != null ? entity[Factory.Unique] : null; + var att = new Attachment + { + Category = typeof(TEntity).Name, + Key = id + "", + Title = entity + "", + //FileName = fileName ?? file.FileName, + ContentType = file.ContentType, + Size = file.Length, + Enable = true, + UploadTime = DateTime.Now, + }; + + if (id != null) + { + var ss = GetControllerAction(); + att.Url = $"/{ss[0]}/{ss[1]}/Detail/{id}"; + } + + var rs = false; + var msg = ""; + try + { + rs = await att.SaveFile(file.OpenReadStream(), uploadPath, fileName); + } + catch (Exception ex) + { + rs = false; + msg = ex.Message; + span?.SetError(ex, att); + + throw; + } + finally + { + // 写日志 + var type = entity.GetType(); + var log = LogProvider.Provider.CreateLog(type, "上传", rs, $"上传 {file.FileName} ,目录 {uploadPath} ,保存为 {att.FilePath} " + msg, 0, null, UserHost); + log.LinkID = id.ToLong(); + log.SaveAsync(); + } + + return att; + } + + /// + /// 批量启用或禁用 + /// + /// 启用/禁用 + /// 操作原因 + /// + protected virtual Int32 EnableOrDisableSelect(Boolean isEnable, String reason) + { + var count = 0; + var ids = GetRequest("keys").SplitAsInt(); + var fields = Factory.AllFields; + if (ids.Length > 0 && fields.Any(f => f.Name.EqualIgnoreCase("enable"))) + { + var log = LogProvider.Provider; + foreach (var id in ids) + { + var entity = Factory.Find("ID", id); + if (entity != null && entity["Enable"].ToBoolean() != isEnable) + { + entity.SetItem("Enable", isEnable); + + log.WriteLog("Update", entity); + log.WriteLog(entity.GetType(), isEnable ? "Enable" : "Disable", true, reason); + + entity.Update(); + + Interlocked.Increment(ref count); + } + } + } + + return count; + } + #endregion + + #region 实体操作重载 + /// 添加实体对象 + /// + /// + protected virtual Int32 OnInsert(TEntity entity) => entity.Insert(); + + /// 更新实体对象 + /// + /// + protected virtual Int32 OnUpdate(TEntity entity) + { + if (Request.HasFormContentType) + { + // 遍历表单字段,部分字段可能有扩展 + foreach (var item in EditFormFields) + { + if (item is FormField ef && ef.GetExpand != null) + { + // 获取参数对象,展开参数,从表单字段接收参数 + var p = ef.GetExpand(entity); + if (p != null && p is not String && !(entity as IEntity).Dirtys[ef.Name]) + { + // 保存参数对象 + if (FieldCollection.ReadForm(p, Request.Form, ef.Name + "_")) + entity.SetItem(ef.Name, p.ToJson(true)); + } + } + } + } + + return entity.Update(); + } + + /// 删除实体对象 + /// + /// + protected virtual Int32 OnDelete(TEntity entity) => entity.Delete(); + #endregion +} \ No newline at end of file diff --git a/NewLife.Cube/Common/EntityTreeController.cs b/NewLife.Cube/Common/EntityTreeController.cs index 81fda536..df87b159 100644 --- a/NewLife.Cube/Common/EntityTreeController.cs +++ b/NewLife.Cube/Common/EntityTreeController.cs @@ -137,7 +137,7 @@ public ActionResult Down(Int32 id) /// protected static TEntity FindByID(Int32 id) { - var key = EntityTree.Meta.Unique.Name; + var key = Factory.Unique.Name; return EntityTree.Meta.Cache.Find(e => (Int32)e[key] == id); } } \ No newline at end of file diff --git a/NewLife.Cube/Common/ReadOnlyEntityController.cs b/NewLife.Cube/Common/ReadOnlyEntityController.cs index 040bd4a4..10e4111d 100644 --- a/NewLife.Cube/Common/ReadOnlyEntityController.cs +++ b/NewLife.Cube/Common/ReadOnlyEntityController.cs @@ -1,16 +1,11 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using NewLife.Common; using NewLife.Cube.ViewModels; -using NewLife.Log; -using NewLife.Reflection; using NewLife.Web; using XCode; using XCode.Membership; -using XCode.Model; namespace NewLife.Cube; @@ -60,7 +55,6 @@ public virtual ApiListResponse Index() var list = SearchData(p); - //return Json(0, null, OnFilter(list.Cast(), ViewKinds.List).ToList(), new { pager = p, stat = p.State }); return new ApiListResponse { Data = list.ToList(), @@ -83,735 +77,8 @@ public virtual ApiResponse Detail([Required] String id) // 验证数据权限 Valid(entity, DataObjectMethodType.Select, false); - //return Json(0, null, OnFilter(entity, ViewKinds.Detail)); return new ApiResponse { Data = entity }; } - - ///// 清空全表数据 - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("清空")] - //[HttpPost] - //public virtual ActionResult Clear() - //{ - // var url = Request.GetReferer(); - - // var p = Session[CacheKey] as Pager; - // p = new Pager(p); - // if (p != null && p.Params.Count > 0) return Json(500, "当前带有查询参数,为免误解,禁止全表清空!"); - - // try - // { - // var count = Entity.Meta.Session.Truncate(); - - // WriteLog("清空数据", true, $"共删除{count}行数据"); - - // if (Request.IsAjaxRequest()) - // return JsonRefresh($"共删除{count}行数据"); - // else if (!url.IsNullOrEmpty()) - // return Redirect(url); - // else - // return RedirectToAction("Index"); - // } - // catch (Exception ex) - // { - // WriteLog("清空数据", false, ex.GetMessage()); - - // throw; - // } - //} - #endregion - - #region 数据接口 - ///// Json接口 - ///// 令牌 - ///// 分页 - ///// - //[AllowAnonymous] - //[DisplayName("Json接口")] - //[HttpGet] - //public virtual ActionResult Json(String token, Pager p) - //{ - // try - // { - // var issuer = ValidToken(token); - - // // 需要总记录数来分页 - // p.RetrieveTotalCount = true; - - // var list = SearchData(p); - - // // Json输出 - // return Json(0, null, list, new { issuer, pager = p }); - // } - // catch (Exception ex) - // { - // return Json(0, null, ex); - // } - //} - - ///// 验证令牌是否有效 - ///// - ///// - //protected virtual String ValidToken(String token) - //{ - // if (token.IsNullOrEmpty()) token = GetRequest("token"); - // if (token.IsNullOrEmpty()) token = GetRequest("key"); - - // var app = App.FindBySecret(token); - // if (app != null) - // { - // if (!app.Enable) throw new XException("非法授权!"); - - // return app?.ToString(); - // } - // else - // { - // var ut = UserToken.Valid(token, UserHost); - // var user = ut.User; - - // // 定位菜单页面 - // var menu = ManageProvider.Menu.FindByFullName(GetType().FullName); - - // // 判断权限 - // if (menu == null || !user.Has(menu, PermissionFlags.Detail)) throw new Exception($"该用户[{user}]无权访问[{menu}]"); - - // // 锁定页面 - // if (!ut.Url.IsNullOrEmpty()) - // { - // var url = ut.Url; - // if (url.Contains("?")) url = url.Substring(null, "?"); - // if (!url.StartsWithIgnoreCase(menu.Url.TrimStart("~"))) throw new Exception($"该令牌[{user}]无权访问[{menu}],仅限于[{url}]"); - // } - - // // 设置当前用户,用于数据权限控制 - // HttpContext.Items["userId"] = user.ID; - // HttpContext.Items["CurrentUser"] = user; - - // return user?.ToString(); - // } - //} - - ///// Xml接口 - ///// 令牌 - ///// 分页 - ///// - //[AllowAnonymous] - //[DisplayName("Xml接口")] - //[HttpGet] - //public virtual ActionResult Xml(String token, Pager p) - //{ - // var xml = ""; - // try - // { - // var issuer = ValidToken(token); - - // // 需要总记录数来分页 - // p.RetrieveTotalCount = true; - - // var list = SearchData(p) as IList; - - // var rs = new Root { Result = false, Data = list, Pager = p, Issuer = issuer }; - - // xml = rs.ToXml(null, false, false); - // } - // catch (Exception ex) - // { - // var rs = new { result = false, data = ex.GetTrue().Message }; - // xml = rs.ToXml(null, false, false); - // } - - // return Content(xml, "application/xml"); - //} - - //private class Root - //{ - // public Boolean Result { get; set; } - // public IList Data { get; set; } - // public Pager Pager { get; set; } - // public String Issuer { get; set; } - //} - - ///// Csv接口 - ///// 令牌 - ///// 分页 - ///// - //[AllowAnonymous] - //[DisplayName("Excel接口")] - //[HttpGet] - //public virtual async Task Csv(String token, Pager p) - //{ - // var issuer = ValidToken(token); - - // //// 需要总记录数来分页 - // //p.RetrieveTotalCount = true; - - // var list = SearchData(p); - - // // 准备需要输出的列 - // var fs = Factory.Fields.ToList(); - - // var rs = Response; - // var headers = rs.Headers; - // headers[HeaderNames.ContentEncoding] = "UTF8"; - // //headers[HeaderNames.ContentType] = "application/vnd.ms-excel"; - - // await OnExportCsv(fs, list, rs.Body); - - // return new EmptyResult(); - //} - - ///// Csv接口 - ///// 令牌 - ///// 分页 - ///// - //[AllowAnonymous] - //[DisplayName("Excel接口")] - //[HttpGet] - //public virtual async Task Excel(String token, Pager p) - //{ - // var issuer = ValidToken(token); - - // var list = SearchData(p); - - // // 准备需要输出的列 - // var fs = new List(); - // foreach (var fi in Factory.AllFields) - // { - // if (Type.GetTypeCode(fi.Type) == TypeCode.Object) continue; - // if (!fi.IsDataObjectField) - // { - // var pi = Factory.EntityType.GetProperty(fi.Name); - // if (pi != null && pi.GetCustomAttribute() != null) continue; - // } - - // fs.Add(fi); - // } - - // // 基本属性与扩展属性对调顺序 - // for (var i = 0; i < fs.Count; i++) - // { - // var fi = fs[i]; - // if (fi.OriField != null) - // { - // var k = fs.IndexOf(fi.OriField); - // if (k >= 0) - // { - // fs[i] = fs[k]; - // fs[k] = fi; - // } - // } - // } - - // var rs = Response; - // var headers = rs.Headers; - // headers[HeaderNames.ContentEncoding] = "UTF8"; - // //headers[HeaderNames.ContentType] = "application/vnd.ms-excel"; - - // await OnExportExcel(fs, list, rs.Body); - - // return new EmptyResult(); - //} - #endregion - - #region 导出Xml/Json/Excel/Csv - ///// 导出Xml - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("导出")] - //[HttpGet] - //public virtual ActionResult ExportXml() - //{ - // var obj = OnExportXml(); - // var xml = ""; - // if (obj is IEntity) - // xml = (obj as IEntity).ToXml(); - // else if (obj is IList) - // xml = (obj as IList).ToXml(); - // else if (obj is IEnumerable list) - // xml = list.ToList().ToXml(); - - // SetAttachment(null, ".xml", true); - - // return Content(xml, "text/xml", Encoding.UTF8); - //} - - ///// 要导出Xml的对象 - ///// - //protected virtual Object OnExportXml() => ExportData(); - - ///// 设置附件响应方式 - ///// - ///// - ///// 包含时间戳 - //protected virtual void SetAttachment(String name, String ext, Boolean includeTime) - //{ - // if (name.IsNullOrEmpty()) name = GetType().GetDisplayName(); - // if (name.IsNullOrEmpty()) name = Factory.EntityType.GetDisplayName(); - // if (name.IsNullOrEmpty()) name = Factory.Table.DataTable.DisplayName; - // if (name.IsNullOrEmpty()) name = GetType().Name.TrimEnd("Controller"); - // if (!ext.IsNullOrEmpty()) ext = ext.EnsureStart("."); - - // if (includeTime) name += $"_{DateTime.Now:yyyyMMddHHmmss}"; - - // name += ext; - // name = HttpUtility.UrlEncode(name, Encoding.UTF8); - - // Response.Headers.Add("Content-Disposition", "Attachment;filename=" + name); - //} - - ///// 导出Json - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("导出")] - //[HttpGet] - //public virtual ActionResult ExportJson() - //{ - // var json = OnExportJson().ToJson(true); - - // SetAttachment(null, ".json", true); - - // return Content(json, "application/json", Encoding.UTF8); - //} - - ///// 要导出Json的对象 - ///// - //protected virtual Object OnExportJson() => ExportData().ToList(); - - ///// 导出Excel - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("导出")] - //[HttpGet] - //public virtual async Task ExportExcel() - //{ - // // 准备需要输出的列 - // var fs = new List(); - // foreach (var fi in Factory.AllFields) - // { - // if (Type.GetTypeCode(fi.Type) == TypeCode.Object) continue; - // if (!fi.IsDataObjectField) - // { - // var pi = Factory.EntityType.GetProperty(fi.Name); - // if (pi != null && pi.GetCustomAttribute() != null) continue; - // } - - // fs.Add(fi); - // } - - // // 基本属性与扩展属性对调顺序 - // for (var i = 0; i < fs.Count; i++) - // { - // var fi = fs[i]; - // if (fi.OriField != null) - // { - // var k = fs.IndexOf(fi.OriField); - // if (k >= 0) - // { - // fs[i] = fs[k]; - // fs[k] = fi; - // } - // } - // } - - // // 要导出的数据超大时,启用流式输出 - // if (Factory.Session.Count > 100_000) - // { - // var p = Session[CacheKey] as Pager; - // p = new Pager(p) - // { - // PageSize = 1, - // RetrieveTotalCount = true - // }; - // SearchData(p); - // } - - // SetAttachment(null, ".xls", true); - - // var rs = Response; - // var headers = rs.Headers; - // headers[HeaderNames.ContentEncoding] = "UTF8"; - // headers[HeaderNames.ContentType] = "application/vnd.ms-excel"; - - // var data = ExportData(); - // await OnExportExcel(fs, data, rs.Body); - - // return new EmptyResult(); - //} - - ///// 导出Excel模板 - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("导出模板")] - //[HttpGet] - //public virtual async Task ExportExcelTemplate() - //{ - // // 准备需要输出的列 - // var fs = new List(); - // foreach (var fi in Factory.AllFields) - // { - // if (Type.GetTypeCode(fi.Type) == TypeCode.Object) continue; - // if (!fi.IsDataObjectField) - // { - // var pi = Factory.EntityType.GetProperty(fi.Name); - // if (pi != null && pi.GetCustomAttribute() != null) continue; - // } - - // //模板隐藏这几个字段 - // if (fi.Name.EqualIgnoreCase("CreateUserID", "CreateUser", "CreateTime", "CreateIP", - // "UpdateUserID", "UpdateUser", "UpdateTime", "UpdateIP", "Enable") || fi.Description.IsNullOrEmpty()) - // { - // continue; - // } - - // fs.Add(fi); - // } - - // // 基本属性与扩展属性对调顺序 - // for (var i = 0; i < fs.Count; i++) - // { - // var fi = fs[i]; - // if (fi.OriField != null) - // { - // var k = fs.IndexOf(fi.OriField); - // if (k >= 0) - // { - // fs[i] = fs[k]; - // fs[k] = fi; - // } - // } - // } - - // // 要导出的数据超大时,启用流式输出 - // if (Factory.Session.Count > 100_000) - // { - // var p = Session[CacheKey] as Pager; - // p = new Pager(p) - // { - // PageSize = 1, - // RetrieveTotalCount = true - // }; - // SearchData(p); - // } - - // SetAttachment(null, ".xls", true); - - // var rs = Response; - // var headers = rs.Headers; - // headers[HeaderNames.ContentEncoding] = "UTF8"; - // headers[HeaderNames.ContentType] = "application/vnd.ms-excel"; - - // var data = ExportData(1); - // await OnExportExcel(fs, data, rs.Body); - - // return new EmptyResult(); - //} - - ///// 导出Excel,可重载修改要输出的列 - ///// 字段列表 - ///// 数据集 - ///// 输出流 - //protected virtual async ValueTask OnExportExcel(List fs, IEnumerable list, Stream output) - //{ - // await using var csv = new CsvFile(output, true); - - // // 列头 - // var headers = new List(); - // foreach (var fi in fs) - // { - // var name = fi.DisplayName; - // if (name.IsNullOrEmpty()) name = fi.Description; - // if (name.IsNullOrEmpty()) name = fi.Name; - - // // 第一行以ID开头的csv文件,容易被识别为SYLK文件 - // if (name == "ID" && fi == fs[0]) name = "Id"; - // headers.Add(name); - // } - // await csv.WriteLineAsync(headers); - - // // 内容 - // foreach (var entity in list) - // { - // await csv.WriteLineAsync(fs.Select(e => entity[e.Name])); - // } - //} - - ///// 导出Csv - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("导出")] - //[HttpGet] - //public virtual async Task ExportCsv() - //{ - // // 准备需要输出的列 - // var fs = Factory.Fields.ToList(); - - // if (Factory.Session.Count > 100_000) - // { - // var p = Session[CacheKey] as Pager; - // p = new Pager(p) - // { - // PageSize = 1, - // RetrieveTotalCount = true - // }; - // SearchData(p); - // } - - // var name = GetType().Name.TrimEnd("Controller"); - // SetAttachment(name, ".csv", true); - - // var rs = Response; - // var headers = rs.Headers; - // headers[HeaderNames.ContentEncoding] = "UTF8"; - // headers[HeaderNames.ContentType] = "application/vnd.ms-excel"; - - // //// 允许同步IO,便于CsvFile刷数据Flush - // //var ft = HttpContext.Features.Get(); - // //if (ft != null) ft.AllowSynchronousIO = true; - - // var data = ExportData(); - // await OnExportCsv(fs, data, rs.Body); - - // return new EmptyResult(); - //} - - ///// 导出Csv,可重载修改要输出的列 - ///// 字段列表 - ///// 数据集 - ///// 输出流 - //protected virtual async ValueTask OnExportCsv(List fs, IEnumerable list, Stream output) - //{ - // await using var csv = new CsvFile(output, true); - - // // 列头 - // var headers = fs.Select(e => e.Name).ToArray(); - // if (headers[0] == "ID") headers[0] = "Id"; - // await csv.WriteLineAsync(headers); - - // // 内容 - // foreach (var entity in list) - // { - // await csv.WriteLineAsync(fs.Select(e => entity[e.Name])); - // } - //} - #endregion - - #region 备份/还原/导出/分享 - ///// 备份到服务器本地目录 - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("备份")] - //[HttpGet] - //public virtual ActionResult Backup() - //{ - // try - // { - // var fact = Factory; - // if (fact.Session.Count > 10_000_000) throw new XException($"数据量[{fact.Session.Count:n0}>10_000_000],禁止备份!"); - - // var dal = fact.Session.Dal; - - // var name = GetType().Name.TrimEnd("Controller"); - // var fileName = $"{name}_{DateTime.Now:yyyyMMddHHmmss}.gz"; - // var bak = NewLife.Setting.Current.BackupPath.CombinePath(fileName).GetBasePath(); - // bak.EnsureDirectory(true); - - // var rs = dal.Backup(fact.Table.DataTable, bak); - - // WriteLog("备份", true, $"备份[{fileName}]({rs:n0}行)成功!"); - - // return Json(0, $"备份[{fileName}]({rs:n0}行)成功!"); - // } - // catch (Exception ex) - // { - // XTrace.WriteException(ex); - - // WriteLog("备份", false, ex.GetMessage()); - - // return Json(500, null, ex); - // } - //} - - ///// 备份导出 - ///// 备份并下载 - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("导出")] - //[HttpGet] - //public virtual async Task BackupAndExport() - //{ - // var fact = Factory; - // if (fact.Session.Count > 10_000_000) throw new XException($"数据量[{fact.Session.Count:n0}>10_000_000],禁止备份!"); - - // var dal = fact.Session.Dal; - - // var name = GetType().Name.TrimEnd("Controller"); - // SetAttachment(name, ".gz", true); - - // // 允许同步IO,便于刷数据Flush - // var ft = HttpContext.Features.Get(); - // if (ft != null) ft.AllowSynchronousIO = true; - - // var ms = Response.Body; - // try - // { - // await using var gs = new GZipStream(ms, CompressionLevel.Optimal, true); - // var count = dal.Backup(fact.Table.DataTable, gs); - - // WriteLog("备份导出", true, $"备份[{name}]({count:n0}行)成功!"); - - // return new EmptyResult(); - // } - // catch (Exception ex) - // { - // XTrace.WriteException(ex); - - // WriteLog("备份导出", false, ex.GetMessage()); - - // return Json(500, null, ex); - // } - //} - - ///// 分享数据 - ///// - ///// 为当前url创建用户令牌 - ///// - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("分享{type}")] - //[HttpGet] - //public virtual ActionResult Share() - //{ - // // 当前用户所有令牌 - // var userId = ManageProvider.User.ID; - // var list = UserToken.Search(null, userId, true, DateTime.Now, DateTime.MinValue, null); - - // var p = Session[CacheKey] as Pager; - // p = new Pager(p) - // { - // RetrieveTotalCount = false, - // }; - - // // 构造url - // var cs = GetControllerAction(); - // var url = cs[0].IsNullOrEmpty() ? $"/{cs[1]}" : $"/{cs[0]}/{cs[1]}"; - // var sb = p.GetBaseUrl(true, true, true); - // if (sb.Length > 0) url += "?" + sb; - - // // 如果该url已存在,则延长有效期 - // var ut = list.FirstOrDefault(e => e.Url.EqualIgnoreCase(url)); - // ut ??= new UserToken { UserID = userId, Url = url }; - - // if (ut.Token.IsNullOrEmpty()) ut.Token = Rand.NextString(8); - // ut.Enable = true; - // ut.Expire = DateTime.Now.AddSeconds(Setting.Current.ShareExpire); - // ut.Save(); - - // //var url2 = $"/Admin/UserToken?q={ut.Token}"; - - // //return Json(0, "分享成功!" + url, null, new { url = url2, time = 3 }); - - // return RedirectToAction("Index", "UserToken", new { area = "Admin", q = ut.Token }); - //} - #endregion - - #region 模版Action - ///// 生成列表 - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("生成列表")] - //[HttpGet] - //public ActionResult MakeList() - //{ - // if (!SysConfig.Current.Develop) throw new InvalidOperationException("仅支持开发模式下使用!"); - - // // 找到项目根目录 - // var root = GetProjectRoot(); - - // // 视图路径,Areas/区域/Views/控制器/_List_Data.cshtml - // var cs = GetControllerAction(); - // var vpath = $"Areas/{cs[0]}/Views/{cs[1]}/_List_Data.cshtml"; - // if (!root.IsNullOrEmpty()) vpath = root.EnsureEnd("/") + vpath; - - // _ = ViewHelper.MakeListView(typeof(TEntity), vpath, ListFields); - - // WriteLog("生成列表", true, vpath); - - // return RedirectToAction("Index"); - //} - - ///// 生成表单 - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("生成表单")] - //[HttpGet] - //public ActionResult MakeForm() - //{ - // if (!SysConfig.Current.Develop) throw new InvalidOperationException("仅支持开发模式下使用!"); - - // // 找到项目根目录 - // var root = GetProjectRoot(); - - // // 视图路径,Areas/区域/Views/控制器/_Form_Body.cshtml - // var cs = GetControllerAction(); - // var vpath = $"Areas/{cs[0]}/Views/{cs[1]}/_Form_Body.cshtml"; - // if (!root.IsNullOrEmpty()) vpath = root.EnsureEnd("/") + vpath; - - // _ = ViewHelper.MakeFormView(typeof(TEntity), vpath, EditFormFields); - - // WriteLog("生成表单", true, vpath); - - // return RedirectToAction("Index"); - //} - - ///// 生成搜索 - ///// - //[EntityAuthorize(PermissionFlags.Detail)] - //[DisplayName("生成搜索")] - //[HttpGet] - //public ActionResult MakeSearch() - //{ - // if (!SysConfig.Current.Develop) throw new InvalidOperationException("仅支持开发模式下使用!"); - - // // 找到项目根目录 - // var root = GetProjectRoot(); - - // // 视图路径,Areas/区域/Views/控制器/_List_Search.cshtml - // var cs = GetControllerAction(); - // var vpath = $"Areas/{cs[0]}/Views/{cs[1]}/_List_Search.cshtml"; - // if (!root.IsNullOrEmpty()) vpath = root.EnsureEnd("/") + vpath; - - // _ = ViewHelper.MakeSearchView(typeof(TEntity), vpath, ListFields); - - // WriteLog("生成搜索", true, vpath); - - // return RedirectToAction("Index"); - //} - - //private String GetProjectRoot() - //{ - // var asm = GetType().Assembly; - // var name = asm.GetName().Name; - - // // core程序出现这种情况:bin/Debug/netcoreapp3.1 - // // 因此添加"../../../" - // var ps = new[] { "./", "../../", "../../" + name, "../../../", "../../../" + name }; - // String err = null; - // foreach (var item in ps) - // { - // var dir = item.AsDirectory(); - // err += dir + ";"; - // if (!dir.Exists) continue; - // var fis = dir.GetFiles("*.csproj", SearchOption.TopDirectoryOnly); - // if (fis != null && fis.Length > 0) return item; - // } - - // // 找不到项目根目录,就用当前目录,因为某些项目文件名和输出名可能不一致 - // return "./"; - - // //err = $"遍历以下路径均找不到项目路径,请检查项目路径:{err}"; - // //throw new InvalidOperationException(err); - //} #endregion #region 列表字段和表单字段 diff --git a/NewLife.Cube/Common/ReadOnlyEntityController2.cs b/NewLife.Cube/Common/ReadOnlyEntityController2.cs index 9b4ea2d1..7da220ea 100644 --- a/NewLife.Cube/Common/ReadOnlyEntityController2.cs +++ b/NewLife.Cube/Common/ReadOnlyEntityController2.cs @@ -28,7 +28,7 @@ public partial class ReadOnlyEntityController public SysConfig SysConfig { get; set; } /// 当前列表页的查询条件缓存Key - private static String CacheKey => $"CubeView_{typeof(TEntity).FullName}"; + protected static String CacheKey => $"CubeView_{typeof(TEntity).FullName}"; #endregion #region 构造 diff --git a/NewLife.CubeNC/Common/EntityController.cs b/NewLife.CubeNC/Common/EntityController.cs index ca1a47ee..d64b0574 100644 --- a/NewLife.CubeNC/Common/EntityController.cs +++ b/NewLife.CubeNC/Common/EntityController.cs @@ -17,24 +17,11 @@ namespace NewLife.Cube; -/// 实体控制器基类 -/// -public class EntityController : EntityController where TEntity : Entity, new() { } - /// 实体控制器基类 /// /// -public class EntityController : ReadOnlyEntityController where TEntity : Entity, new() +public partial class EntityController : ReadOnlyEntityController where TEntity : Entity, new() { - #region 属性 - private String CacheKey => $"CubeView_{typeof(TEntity).FullName}"; - #endregion - - #region 构造 - /// 实例化 - public EntityController() => PageSetting.IsReadOnly = false; - #endregion - #region 默认Action /// 删除 /// @@ -43,42 +30,18 @@ namespace NewLife.Cube; [DisplayName("删除{type}")] public virtual ActionResult Delete(String id) { - var url = Request.GetReferer(); - var act = "删除"; var entity = FindData(id); - var rs = false; - var err = ""; try { - // 假删除与还原 - var fi = GetDeleteField(); - if (fi != null) - { - var restore = GetRequest("restore").ToBoolean(); - entity.SetItem(fi.Name, !restore); - if (restore) act = "恢复"; - - if (Valid(entity, DataObjectMethodType.Update, true)) - OnUpdate(entity); - else - err = "验证失败"; - } - else - { - if (Valid(entity, DataObjectMethodType.Delete, true)) - OnDelete(entity); - else - err = "验证失败"; - } + act = ProcessDelete(entity); - rs = true; + if (Request.IsAjaxRequest()) return JsonRefresh($"{act}成功!"); } catch (Exception ex) { - err = ex.GetTrue().Message; + var err = ex.GetTrue().Message; WriteLog("Delete", false, err); - //if (LogOnChange) LogProvider.Provider.WriteLog("Delete", entity, err); if (Request.IsAjaxRequest()) return JsonRefresh($"{act}失败!{err}"); @@ -86,15 +49,11 @@ public virtual ActionResult Delete(String id) throw; } - if (Request.IsAjaxRequest()) - return JsonRefresh(rs ? $"{act}成功!" : $"{act}失败!{err}"); - else if (!url.IsNullOrEmpty()) - return Redirect(url); - else - return RedirectToAction("Index"); - } + var url = Request.GetReferer(); + if (!url.IsNullOrEmpty()) return Redirect(url); - private static FieldItem GetDeleteField() => Factory.Fields.FirstOrDefault(e => e.Name.EqualIgnoreCase("Deleted", "IsDelete", "IsDeleted") && e.Type == typeof(Boolean)); + return RedirectToAction("Index"); + } /// 表单,添加/修改 /// @@ -106,7 +65,7 @@ public virtual ActionResult Add() // 填充QueryString参数 var qs = Request.Query; - foreach (var item in Entity.Meta.Fields) + foreach (var item in Factory.Fields) { var v = qs[item.Name]; if (v.Count > 0) entity[item.Name] = v[0]; @@ -157,42 +116,32 @@ public virtual async Task Add(TModel model) if (Factory.Unique.IsIdentity && entity[Factory.Unique.Name].ToInt() != 0) throw new Exception("我们约定添加数据时路由id部分默认没有数据,以免模型绑定器错误识别!"); - var rs = false; - var err = ""; try { - if (Valid(entity, DataObjectMethodType.Insert, true)) - { - //SaveFiles(entity); - - OnInsert(entity); + if (!Valid(entity, DataObjectMethodType.Insert, true)) + throw new Exception("验证失败"); - var fs = await SaveFiles(entity); - if (fs.Count > 0) OnUpdate(entity); + OnInsert(entity); - if (LogOnChange) LogProvider.Provider.WriteLog("Insert", entity); + var fs = await SaveFiles(entity); + if (fs.Count > 0) OnUpdate(entity); - rs = true; - } - else - err = "验证失败"; + if (LogOnChange) LogProvider.Provider.WriteLog("Insert", entity); } catch (Exception ex) { - err = ex.Message; + var code = ex is ApiException ae ? ae.Code : 500; + var err = ex.Message; ModelState.AddModelError((ex as ArgumentException)?.ParamName ?? "", ex.Message); - } - if (!rs) - { WriteLog("Add", false, err); ViewBag.StatusMessage = SysConfig.Develop ? ("添加失败!" + err) : "添加失败!"; // 添加失败,ID清零,否则会显示保存按钮 - entity[Entity.Meta.Unique.Name] = 0; + entity[Factory.Unique.Name] = 0; - if (IsJsonRequest) return Json(500, ViewBag.StatusMessage); + if (IsJsonRequest) return Json(code, ViewBag.StatusMessage); ViewBag.Fields = OnGetFields(ViewKinds.AddForm, entity); @@ -271,16 +220,14 @@ public virtual async Task Edit(TModel model) var err = ""; try { - if (Valid(entity, DataObjectMethodType.Update, true)) - { - await SaveFiles(entity); + if (!Valid(entity, DataObjectMethodType.Update, true)) + throw new Exception("验证失败"); - OnUpdate(entity); + await SaveFiles(entity); - rs = true; - } - else - err = "验证失败"; + OnUpdate(entity); + + rs = true; } catch (Exception ex) { @@ -319,153 +266,26 @@ public virtual async Task Edit(TModel model) return View("EditForm", entity); } - /// 保存所有上传文件 - /// 实体对象 - /// 上传目录。为空时默认UploadPath配置 - /// - protected virtual async Task> SaveFiles(TEntity entity, String uploadPath = null) - { - var rs = new List(); - var list = new List(); - - if (!Request.HasFormContentType) return list; - - var files = Request.Form.Files; - var fields = Factory.Fields; - foreach (var fi in fields) - { - var dc = fi.Field; - if (dc.IsAttachment()) - { - // 允许一次性上传多个文件到服务端 - foreach (var file in files) - { - if (file.Name.EqualIgnoreCase(fi.Name, fi.Name + "_attachment")) - { - var att = await SaveFile(entity, file, uploadPath, null); - if (att != null) - { - var url = ViewHelper.GetAttachmentUrl(att); - list.Add(url); - rs.Add(url); - } - } - } - - if (list.Count > 0) - { - entity.SetItem(fi.Name, list.Join(";")); - list.Clear(); - } - } - } - - return rs; - } - - /// 保存单个文件 - /// 实体对象 - /// 文件 - /// 上传目录,默认使用UploadPath配置 - /// 文件名,如若指定则忽略前面的目录 - /// - protected virtual async Task SaveFile(TEntity entity, IFormFile file, String uploadPath, String fileName) - { - if (fileName.IsNullOrEmpty()) fileName = file.FileName; - - using var span = DefaultTracer.Instance?.NewSpan(nameof(SaveFile), new { name = file.Name, fileName, uploadPath }); - - var id = Factory.Unique != null ? entity[Factory.Unique] : null; - var att = new Attachment - { - Category = typeof(TEntity).Name, - Key = id + "", - Title = entity + "", - //FileName = fileName ?? file.FileName, - ContentType = file.ContentType, - Size = file.Length, - Enable = true, - UploadTime = DateTime.Now, - }; - - if (id != null) - { - var ss = GetControllerAction(); - att.Url = $"/{ss[0]}/{ss[1]}/Detail/{id}"; - } - - var rs = false; - var msg = ""; - try - { - rs = await att.SaveFile(file.OpenReadStream(), uploadPath, fileName); - } - catch (Exception ex) - { - rs = false; - msg = ex.Message; - span?.SetError(ex, att); - - throw; - } - finally - { - // 写日志 - var type = entity.GetType(); - var log = LogProvider.Provider.CreateLog(type, "上传", rs, $"上传 {file.FileName} ,目录 {uploadPath} ,保存为 {att.FilePath} " + msg, 0, null, UserHost); - log.LinkID = id.ToLong(); - log.SaveAsync(); - } - - return att; - } - /// 批量启用 /// 主键集合 /// 操作原因 /// [EntityAuthorize(PermissionFlags.Update)] - public virtual ActionResult EnableSelect(String keys, String reason) => EnableOrDisableSelect(true, reason); + public virtual ActionResult EnableSelect(String keys, String reason) + { + var count = EnableOrDisableSelect(true, reason); + return JsonRefresh($"共启用[{count}]个"); + } /// 批量禁用 /// 主键集合 /// 操作原因 /// [EntityAuthorize(PermissionFlags.Update)] - public virtual ActionResult DisableSelect(String keys, String reason) => EnableOrDisableSelect(false, reason); - - /// - /// 批量启用或禁用 - /// - /// 启用/禁用 - /// 操作原因 - /// - protected virtual ActionResult EnableOrDisableSelect(Boolean isEnable, String reason) + public virtual ActionResult DisableSelect(String keys, String reason) { - var count = 0; - var ids = GetRequest("keys").SplitAsInt(); - var fields = Factory.AllFields; - if (ids.Length > 0 && fields.Any(f => f.Name.EqualIgnoreCase("enable"))) - { - var log = LogProvider.Provider; - foreach (var id in ids) - { - var entity = Factory.Find("ID", id); - if (entity != null && entity["Enable"].ToBoolean() != isEnable) - { - entity.SetItem("Enable", isEnable); - - log.WriteLog("Update", entity); - log.WriteLog(entity.GetType(), isEnable ? "Enable" : "Disable", true, reason); - - entity.Update(); - - Interlocked.Increment(ref count); - } - } - } - - return JsonRefresh($"共{(isEnable ? "启用" : "禁用")}[{count}]个"); + var count = EnableOrDisableSelect(false, reason); + return JsonRefresh($"共禁用[{count}]个"); } #endregion @@ -760,45 +580,6 @@ public virtual ActionResult DeleteAll() } #endregion - #region 实体操作重载 - /// 添加实体对象 - /// - /// - protected virtual Int32 OnInsert(TEntity entity) => entity.Insert(); - - /// 更新实体对象 - /// - /// - protected virtual Int32 OnUpdate(TEntity entity) - { - if (Request.HasFormContentType) - { - // 遍历表单字段,部分字段可能有扩展 - foreach (var item in EditFormFields) - { - if (item is FormField ef && ef.GetExpand != null) - { - // 获取参数对象,展开参数,从表单字段接收参数 - var p = ef.GetExpand(entity); - if (p != null && p is not String && !(entity as IEntity).Dirtys[ef.Name]) - { - // 保存参数对象 - if (FieldCollection.ReadForm(p, Request.Form, ef.Name + "_")) - entity.SetItem(ef.Name, p.ToJson(true)); - } - } - } - } - - return entity.Update(); - } - - /// 删除实体对象 - /// - /// - protected virtual Int32 OnDelete(TEntity entity) => entity.Delete(); - #endregion - #region 同步/还原 /// 同步数据 /// diff --git a/NewLife.CubeNC/Common/ReadOnlyEntityController.cs b/NewLife.CubeNC/Common/ReadOnlyEntityController.cs index 4e65f831..8eb8952a 100644 --- a/NewLife.CubeNC/Common/ReadOnlyEntityController.cs +++ b/NewLife.CubeNC/Common/ReadOnlyEntityController.cs @@ -15,7 +15,6 @@ using NewLife.Cube.ViewModels; using NewLife.Data; using NewLife.Log; -using NewLife.Reflection; using NewLife.Security; using NewLife.Serialization; using NewLife.Web; @@ -23,7 +22,6 @@ using XCode; using XCode.Configuration; using XCode.Membership; -using XCode.Model; namespace NewLife.Cube; @@ -103,7 +101,6 @@ public virtual ActionResult Index(Pager p = null) p ??= ViewBag.Page as Pager; // 缓存数据,用于后续导出 - //SetSession(CacheKey, p); Session[CacheKey] = p; return IndexView(p); diff --git a/NewLife.CubeNC/NewLife.CubeNC.csproj b/NewLife.CubeNC/NewLife.CubeNC.csproj index 76259069..63cb538f 100644 --- a/NewLife.CubeNC/NewLife.CubeNC.csproj +++ b/NewLife.CubeNC/NewLife.CubeNC.csproj @@ -61,6 +61,7 @@ +