mvc版和SPA版的控制器代码完全分离

This commit is contained in:
大石头 2023-02-23 14:10:07 +08:00
parent d790d02f9f
commit e222db12f4
54 changed files with 5406 additions and 750 deletions

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>区域特性。兼容netcore</summary>
public class AreaAttribute : Attribute
{
/// <summary>实例化区域特性</summary>
/// <param name="areaName"></param>
public AreaAttribute(String areaName) { }
}
}

View File

@ -1,341 +1,96 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.WebPages;
using NewLife.Cube.Controllers;
using NewLife.Cube.Precompiled;
using NewLife.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using NewLife.Cube.Entity;
using NewLife.Cube.Membership;
using NewLife.Log;
using NewLife.Model;
using NewLife.Reflection;
using NewLife.Threading;
using NewLife.Web;
using XCode;
using XCode.Membership;
namespace NewLife.Cube
{
/// <summary>区域注册基类</summary>
/// <summary>区域特性基类</summary>
/// <remarks>
/// 提供以下功能:
/// 1区域名称。从类名中截取。其中DisplayName特性作为菜单中文名。
/// 2静态构造注册一次视图引擎、绑定提供者、过滤器
/// 3注册区域默认路由
/// </remarks>
public abstract class AreaRegistrationBase : AreaRegistration
public class AreaBase : AreaAttribute
{
/// <summary>区域名称</summary>
public override String AreaName { get; }
/// <summary>预编译引擎集合。便于外部设置属性</summary>
public static PrecompiledViewAssembly[] PrecompiledEngines { get; private set; }
/// <summary>所有区域类型</summary>
public static Type[] Areas { get; private set; }
private static readonly ConcurrentDictionary<Type, Type> _areas = new();
/// <summary>实例化区域注册</summary>
public AreaRegistrationBase() => AreaName = GetType().Name.TrimEnd("AreaRegistration");
public AreaBase(String areaName) : base(areaName) => RegisterArea(GetType());
static AreaRegistrationBase()
/// <summary>注册区域,每个继承此区域特性的类的静态构造函数都调用此方法,以进行相关注册</summary>
public static void RegisterArea<T>() where T : AreaBase => RegisterArea(typeof(T));
/// <summary>注册区域,每个继承此区域特性的类的静态构造函数都调用此方法,以进行相关注册</summary>
public static void RegisterArea(Type areaType)
{
XTrace.WriteLine("{0} Start 初始化魔方 {0}", new String('=', 32));
Assembly.GetExecutingAssembly().WriteVersion();
if (!_areas.TryAdd(areaType, areaType)) return;
var ioc = ObjectContainer.Current;
var services = ioc.BuildServiceProvider();
//#if !NET4
// // 外部管理提供者需要手工覆盖
// ioc.AddSingleton<IManageProvider, DefaultManageProvider>();
//#endif
if (ManageProvider.Provider == null) ManageProvider.Provider = new DefaultManageProvider();
var ns = areaType.Namespace + ".Controllers";
var areaName = areaType.Name.TrimEnd("Area");
XTrace.WriteLine("开始注册权限管理区域[{0}],控制器命名空间[{1}]", areaName, ns);
// 遍历所有引用了AreaRegistrationBase的程序集
var list = new List<PrecompiledViewAssembly>();
foreach (var asm in FindAllArea())
// 更新区域名集合
var rs = CubeService.AreaNames?.ToList() ?? new List<String>();
if (!rs.Contains(areaName))
{
XTrace.WriteLine("注册区域视图程序集:{0}", asm.FullName);
list.Add(new PrecompiledViewAssembly(asm));
rs.Add(areaName);
CubeService.AreaNames = rs.ToArray();
}
PrecompiledEngines = list.ToArray();
var engine = new CompositePrecompiledMvcEngine(PrecompiledEngines);
XTrace.WriteLine("注册复合预编译引擎,共有视图程序集{0}个", list.Count);
//ViewEngines.Engines.Insert(0, engine);
// 预编译引擎滞后,让其它引擎先工作
ViewEngines.Engines.Add(engine);
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
// 注册绑定提供者
//ioc.Register<IModelBinderProvider, EntityModelBinderProvider>("Entity");
//ioc.Register<IModelBinderProvider, PagerModelBinderProvider>("Pager");
var providers = ModelBinderProviders.BinderProviders;
//var prv = ioc.Resolve<IModelBinderProvider>("Entity");
var prv = services.GetService(typeof(EntityModelBinderProvider)) as IModelBinderProvider;
if (prv == null) prv = new EntityModelBinderProvider();
if (prv != null)
{
XTrace.WriteLine("注册实体模型绑定器:{0}", prv.GetType().FullName);
providers.Add(prv);
}
//prv = ioc.Resolve<IModelBinderProvider>("Pager");
var prv2 = services.GetService(typeof(PagerModelBinderProvider)) as IModelBinderProvider;
if (prv2 == null) prv2 = new PagerModelBinderProvider();
if (prv2 != null)
{
XTrace.WriteLine("注册页面模型绑定器:{0}", prv2.GetType().FullName);
providers.Add(prv2);
}
// 注册过滤器
//ioc.Register<HandleErrorAttribute, MvcHandleErrorAttribute>();
//ioc.Register<AuthorizeAttribute, EntityAuthorizeAttribute>("Cube");
var filters = GlobalFilters.Filters;
//var f1 = ioc.Resolve<HandleErrorAttribute>();
var f1 = services.GetService(typeof(HandleErrorAttribute)) as HandleErrorAttribute;
if (f1 != null)
{
XTrace.WriteLine("注册异常过滤器:{0}", f1.GetType().FullName);
filters.Add(f1);
}
//var f2 = ioc.Resolve<AuthorizeAttribute>();
var f2 = services.GetService(typeof(AuthorizeAttribute)) as AuthorizeAttribute;
if (f2 == null) f2 = new EntityAuthorizeAttribute();
if (f2 != null)
{
XTrace.WriteLine("注册授权过滤器:{0}", f2.GetType().FullName);
if (f2 is EntityAuthorizeAttribute eaa) eaa.IsGlobal = true;
filters.Add(f2);
}
//foreach (var item in ioc.ResolveAll(typeof(AuthorizeAttribute)))
//{
// var auth = item.Instance;
// XTrace.WriteLine("注册[{0}]授权过滤器:{1}", item.Identity, auth.GetType().FullName);
// if (auth is EntityAuthorizeAttribute eaa) eaa.IsGlobal = true;
// filters.Add(auth);
//}
// 从数据库或者资源文件加载模版页面的例子
//HostingEnvironment.RegisterVirtualPathProvider(new ViewPathProvider());
//var routes = RouteTable.Routes;
//routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//routes.MapMvcAttributeRoutes();
//routes.MapRoute(
// name: "Virtual",
// url: "{*viewName}",
// defaults: new { controller = "Frontend", action = "Default" },
// constraints: new { controller = "Frontend", action = "Default" }
//);
var routes = RouteTable.Routes;
if (routes["Cube"] == null)
{
// 为魔方注册默认首页启动魔方站点时能自动跳入后台同时为Home预留默认过度视图页面
routes.MapRoute(
name: "CubeHome",
url: "CubeHome/{action}/{id}",
defaults: new { controller = "CubeHome", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { typeof(CubeHomeController).Namespace }
);
routes.MapRoute(
name: "Cube",
url: "Cube/{action}/{id}",
defaults: new { controller = "Cube", action = "Info", id = UrlParameter.Optional },
namespaces: new[] { typeof(CubeController).Namespace }
);
}
if (routes["Sso"] == null)
{
routes.MapRoute(
name: "Sso",
url: "Sso/{action}/{id}",
defaults: new { controller = "Sso", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { typeof(CubeHomeController).Namespace }
);
}
// 自动检查并下载魔方资源
ThreadPoolX.QueueUserWorkItem(CheckContent);
XTrace.WriteLine("{0} End 初始化魔方 {0}", new String('=', 32));
}
/// <summary>遍历所有引用了AreaRegistrationBase的程序集</summary>
/// <returns></returns>
static List<Assembly> FindAllArea()
{
var list = new List<Assembly>();
Areas = typeof(AreaRegistrationBase).GetAllSubclasses().ToArray();
//Areas = typeof(AreaRegistration).GetAllSubclasses(false).ToArray();
//// 子级类库可以不需要建立区域注册,而直接进行模板覆盖
//Areas = typeof(WebViewPage).GetAllSubclasses(false).ToArray();
foreach (var item in Areas)
{
var asm = item.Assembly;
if (!list.Contains(asm))
{
list.Add(asm);
//yield return asm;
}
}
// 子级类库可以不需要建立区域注册,而直接进行模板覆盖
var rs = typeof(WebViewPage).GetAllSubclasses().ToArray();
foreach (var item in rs)
{
var asm = item.Assembly;
if (!list.Contains(asm))
{
list.Add(asm);
}
}
// 为了能够实现模板覆盖,程序集相互引用需要排序,父程序集在前
list.Sort((x, y) =>
{
if (x == y) return 0;
if (x != null && y == null) return 1;
if (x == null && y != null) return -1;
//return x.GetReferencedAssemblies().Any(e => e.FullName == y.FullName) ? 1 : -1;
// 对程序集引用进行排序时不能使用全名当魔方更新而APP没有重新编译时版本的不同将会导致全名不同无法准确进行排序
var yname = y.GetName().Name;
return x.GetReferencedAssemblies().Any(e => e.Name == yname) ? 1 : -1;
});
return list;
}
static void CheckContent()
{
// 释放ico图标
var ico = "favicon.ico";
var ico2 = ico.GetFullPath();
if (!File.Exists(ico2)) Assembly.GetExecutingAssembly().ReleaseFile(ico, ico2);
// 检查魔方样式
var js = "~/Content/Cube.js".GetFullPath();
var css = "~/Content/Cube.css".GetFullPath();
if (File.Exists(js) && File.Exists(css))
{
// 判断脚本时间
var dt = DateTime.MinValue;
var ss = File.ReadAllLines(js);
for (var i = 0; i < 5; i++)
{
if (DateTime.TryParse(ss[i].TrimStart("//").Trim(), out dt)) break;
}
// 要求脚本最小更新时间
if (dt >= "2020-02-04 00:00:00".ToDateTime()) return;
}
var url = NewLife.Setting.Current.PluginServer;
if (url.IsNullOrEmpty()) return;
var wc = new WebClientX()
{
Log = XTrace.Log
};
wc.DownloadLinkAndExtract(url, "Cube_Content", "~/Content".GetFullPath(), true);
}
/// <summary>注册区域</summary>
/// <param name="context"></param>
public override void RegisterArea(AreaRegistrationContext context)
{
var ns = GetType().Namespace + ".Controllers";
XTrace.WriteLine("开始注册权限管理区域[{0}],控制器命名空间[{1}]", AreaName, ns);
// 注册本区域默认路由
// Json输出需要配置web.config
//context.MapRoute(
// AreaName + "_Data",
// AreaName + "/{controller}.json/",
// new { controller = "Index", action = "Index", id = UrlParameter.Optional, output = "json" },
// new[] { ns }
//);
// Json输出不需要配置web.config
//context.MapRoute(
// AreaName + "_Json",
// AreaName + "/{controller}Json/{action}/{id}",
// new { controller = "Index", action = "Export", id = UrlParameter.Optional, output = "json" },
// new[] { ns }
//);
//context.MapRoute(
// AreaName + "_Detail",
// AreaName + "/{controller}/{id}",
// new { controller = "Index", action = "Detail" },
// new[] { ns }
//);
//context.MapRoute(
// AreaName + "_Detail_Json",
// AreaName + "/{controller}/{id}/Json",
// new { controller = "Index", action = "Detail", output = "json" },
// new { id = @"\d+" },
// new[] { ns }
//);
//context.MapRoute(
// AreaName + "_Json",
// AreaName + "/{controller}/Json",
// new { controller = "Index", action = "Index", output = "json" },
// new[] { ns }
//);
// 本区域默认配置
context.MapRoute(
AreaName,
AreaName + "/{controller}/{action}/{id}",
new { controller = "Index", action = "Index", id = UrlParameter.Optional },
new[] { ns }
);
//var routes = context.Routes;
//if (routes["Cube"] == null)
//{
// // 为魔方注册默认首页启动魔方站点时能自动跳入后台同时为Home预留默认过度视图页面
// routes.MapRoute(
// name: "Cube",
// url: "{controller}/{action}/{id}",
// defaults: new { controller = "CubeHome", action = "Index", id = UrlParameter.Optional },
// namespaces: new[] { typeof(CubeHomeController).Namespace }
// );
//}
// 所有已存在文件的请求都交给Mvc处理比如Admin目录
//routes.RouteExistingFiles = true;
// 自动检查并添加菜单
ThreadPoolX.QueueUserWorkItem(ScanController);
var task = Task.Run(() =>
{
using var span = DefaultTracer.Instance?.NewSpan(nameof(ScanController), areaType.FullName);
try
{
ScanController(areaType);
}
catch (Exception ex)
{
span?.SetError(ex, null);
XTrace.WriteException(ex);
}
});
task.Wait(5_000);
}
/// <summary>自动扫描控制器,并添加到菜单</summary>
/// <remarks>默认操作当前注册区域的下一级Controllers命名空间</remarks>
protected virtual void ScanController()
protected static void ScanController(Type areaType)
{
#if DEBUG
XTrace.WriteLine("{0}.ScanController", GetType().Name.TrimEnd("AreaRegistration"));
#endif
var areaName = areaType.Name.TrimEnd("Area");
XTrace.WriteLine("start------初始化[{0}]的菜单体系------start", areaName);
var mf = ManageProvider.Menu;
if (mf == null) return;
using var tran = (mf as IEntityFactory).Session.CreateTrans();
// 初始化数据库
_ = Menu.Meta.Count;
_ = ModelTable.Meta.Count;
_ = ModelColumn.Meta.Count;
XTrace.WriteLine("初始化[{0}]的菜单体系", AreaName);
mf.ScanController(AreaName, GetType().Assembly, GetType().Namespace + ".Controllers");
//using var tran = (mf as IEntityFactory).Session.CreateTrans();
//var menus = mf.ScanController(areaName, areaType.Assembly, areaType.Namespace + ".Controllers");
var menus = MenuHelper.ScanController(mf, areaName, areaType);
// 更新区域名称为友好中文名
var menu = mf.Root.FindByPath(AreaName);
var menu = mf.Root.FindByPath(areaName);
if (menu != null && menu.DisplayName.IsNullOrEmpty())
{
var dis = GetType().GetDisplayName();
var des = GetType().GetDescription();
var dis = areaType.GetDisplayName();
var des = areaType.GetDescription();
if (!dis.IsNullOrEmpty()) menu.DisplayName = dis;
if (!des.IsNullOrEmpty()) menu.Remark = des;
@ -343,25 +98,44 @@ namespace NewLife.Cube
(menu as IEntity).Update();
}
tran.Commit();
//tran.Commit();
//// 扫描模型表
//ScanModel(areaName, menus);
// 再次检查菜单权限因为上面的ScanController里开启菜单权限检查时菜单可能还没有生成
var task = Task.Run(() =>
{
//Thread.Sleep(1000);
XTrace.WriteLine("新增了菜单,需要检查权限。二次检查,双重保障");
typeof(Role).Invoke("CheckRole");
});
task.Wait(5_000);
XTrace.WriteLine("end---------初始化[{0}]的菜单体系---------end", areaName);
}
private static ICollection<String> _areas;
private static ICollection<String> _namespaces;
/// <summary>判断控制器是否归属于魔方管辖</summary>
/// <param name="controller"></param>
/// <param name="controllerActionDescriptor"></param>
/// <returns></returns>
public static Boolean Contains(IController controller)
public static Boolean Contains(ControllerActionDescriptor controllerActionDescriptor)
{
// 判断控制器是否在管辖范围之内,不拦截其它控制器的异常信息
var ns = controller.GetType().Namespace;
// 判断控制器是否在管辖范围之内
var controller = controllerActionDescriptor.ControllerTypeInfo;
var ns = controller.Namespace;
if (!ns.EndsWith(".Controllers")) return false;
if (_areas == null) _areas = new HashSet<String>(Areas.Select(e => e.Namespace));
_namespaces ??= new HashSet<String>(_areas.Keys.Select(e => e.Namespace));
// 该控制器父级命名空间必须有对应的区域注册类,才会拦截其异常
ns = ns.TrimEnd(".Controllers");
//return Areas.Any(e => e.Namespace == ns);
return _areas.Contains(ns);
return _namespaces.Contains(ns);
}
/// <summary>获取所有区域</summary>
/// <returns></returns>
public static ICollection<Type> GetAreas() => _areas.Keys;
}
}

View File

@ -1,87 +0,0 @@
using System;
using System.Collections;
using System.Text.RegularExpressions;
using NewLife.Data;
using XCode;
using XCode.Configuration;
namespace NewLife.Cube
{
/// <summary>获取数据源委托</summary>
/// <param name="entity"></param>
/// <param name="field"></param>
/// <returns></returns>
public delegate IDictionary DataSourceDelegate(IEntity entity, FieldItem field);
/// <summary>数据可见委托</summary>
/// <param name="entity"></param>
/// <param name="field"></param>
/// <returns></returns>
public delegate Boolean DataVisibleDelegate(IEntity entity, FieldItem field);
/// <summary>数据字段。用于定制数据列</summary>
public class DataField
{
#region
/// <summary>名称</summary>
public String Name { get; set; }
/// <summary>前缀名称。放在某字段之前</summary>
public String BeforeName { get; set; }
/// <summary>后缀名称。放在某字段之后</summary>
public String AfterName { get; set; }
/// <summary>显示名</summary>
public String DisplayName { get; set; }
/// <summary>链接</summary>
public String Url { get; set; }
/// <summary>标题。数据单元格上的提示文字</summary>
public String Title { get; set; }
/// <summary>头部文字</summary>
public String Header { get; set; }
/// <summary>头部链接。一般是排序</summary>
public String HeaderUrl { get; set; }
/// <summary>头部标题。数据移上去后显示的文字</summary>
public String HeaderTitle { get; set; }
/// <summary>数据动作。设为action时走ajax请求</summary>
public String DataAction { get; set; }
/// <summary>多选数据源</summary>
public DataSourceDelegate DataSource { get; set; }
/// <summary>是否显示</summary>
public DataVisibleDelegate DataVisible { get; set; }
#endregion
#region
private static readonly Regex _reg = new Regex(@"{(\w+)}", RegexOptions.Compiled);
/// <summary>针对指定实体对象计算DisplayName替换其中变量</summary>
/// <param name="data"></param>
/// <returns></returns>
public virtual String GetDisplayName(IExtend data)
{
if (DisplayName.IsNullOrEmpty()) return null;
return _reg.Replace(DisplayName, m => data[m.Groups[1].Value + ""] + "");
}
/// <summary>针对指定实体对象计算url替换其中变量</summary>
/// <param name="data"></param>
/// <returns></returns>
public virtual String GetUrl(IExtend data)
{
if (Url.IsNullOrEmpty()) return null;
return _reg.Replace(Url, m => data[m.Groups[1].Value + ""] + "");
}
#endregion
}
}

View File

@ -1,121 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using NewLife.Common;
using NewLife.Log;
using XCode.DataAccessLayer;
using XCode.Membership;
namespace NewLife.Cube
{
/// <summary>页面查询执行时间模块</summary>
public class DbRunTimeModule : IHttpModule
{
#region IHttpModule Members
void IHttpModule.Dispose() { }
/// <summary>初始化模块,准备拦截请求。</summary>
/// <param name="context"></param>
void IHttpModule.Init(HttpApplication context)
{
if (!Enable) return;
context.BeginRequest += (s, e) => OnInit();
context.PostReleaseRequestState += (s, e) => OnEnd();
}
#endregion
/// <summary>执行时间字符串</summary>
public static String DbRunTimeFormat { get; set; } = "查询{0}次,执行{1}次,耗时{2:n0}毫秒";
const String _QueryTimes = "DAL.QueryTimes";
const String _ExecuteTimes = "DAL.ExecuteTimes";
/// <summary>初始化模块,准备拦截请求。</summary>
void OnInit()
{
var ctx = HttpContext.Current;
ctx.Items[_QueryTimes] = DAL.QueryTimes;
ctx.Items[_ExecuteTimes] = DAL.ExecuteTimes;
// 设计时收集执行的SQL语句
//if (SysConfig.Current.Develop) ctx.Items["XCode_SQLList"] = new List<String>();
if (SysConfig.Current.Develop)
{
var list = new List<String>();
ctx.Items["XCode_SQLList"] = list;
DAL.LocalFilter = sql => list.Add(sql);
}
ManageProvider.UserHost = ctx.Request.RequestContext.HttpContext.GetUserHost();
}
void OnEnd()
{
DAL.LocalFilter = null;
ManageProvider.UserHost = null;
}
private static Boolean _tip;
/// <summary>获取执行时间和查询次数等信息</summary>
/// <returns></returns>
public static String GetInfo()
{
var ctx = HttpContext.Current;
var ts = DateTime.Now - ctx.Timestamp;
if (!ctx.Items.Contains(_QueryTimes) || !ctx.Items.Contains(_ExecuteTimes))
{
//throw new XException("设计错误需要在web.config中配置{0}", typeof(DbRunTimeModule).FullName);
if (!_tip)
{
_tip = true;
XTrace.WriteLine("设计错误需要在web.config中配置{0}", typeof(DbRunTimeModule).FullName);
}
return null;
}
var StartQueryTimes = (Int32)ctx.Items[_QueryTimes];
var StartExecuteTimes = (Int32)ctx.Items[_ExecuteTimes];
var inf = String.Format(DbRunTimeFormat, DAL.QueryTimes - StartQueryTimes, DAL.ExecuteTimes - StartExecuteTimes, ts.TotalMilliseconds);
// 设计时收集执行的SQL语句
if (SysConfig.Current.Develop)
{
var list = ctx.Items["XCode_SQLList"] as List<String>;
if (list != null && list.Count > 0) inf += "<br />" + list.Select(e => HttpUtility.HtmlEncode(e)).Join("<br />" + Environment.NewLine);
}
return inf;
}
private static Boolean? _Enable;
/// <summary>是否启用显示运行时间</summary>
public static Boolean Enable
{
get
{
if (_Enable == null) _Enable = Setting.Current.ShowRunTime;
return _Enable.Value;
}
}
//String GetIP(HttpContext ctx)
//{
// var req = ctx.Request;
// if (req == null) return null;
// var ip = (String)ctx.Items["UserHostAddress"];
// if (ip.IsNullOrEmpty()) ip = req.ServerVariables["HTTP_X_FORWARDED_FOR"];
// if (ip.IsNullOrEmpty()) ip = req.ServerVariables["X-Real-IP"];
// if (ip.IsNullOrEmpty()) ip = req.ServerVariables["X-Forwarded-For"];
// if (ip.IsNullOrEmpty()) ip = req.ServerVariables["REMOTE_ADDR"];
// if (ip.IsNullOrEmpty()) ip = req.UserHostName;
// if (ip.IsNullOrEmpty()) ip = req.UserHostAddress;
// return ip;
//}
}
}

View File

@ -1,78 +0,0 @@
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using NewLife.Log;
namespace NewLife.Cube
{
/// <summary>拦截错误的特性</summary>
public class MvcHandleErrorAttribute : HandleErrorAttribute
{
private static HashSet<String> NotFoundFiles = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
/// <summary>拦截异常</summary>
/// <param name="ctx"></param>
public override void OnException(ExceptionContext ctx)
{
if (ctx.ExceptionHandled) return;
//XTrace.WriteException(ctx.Exception);
var ex = ctx.Exception?.GetTrue();
if (ex != null)
{
// 避免反复出现缺少文件
if (ex is HttpException hex && (UInt32)hex.ErrorCode == 0x80004005)
{
var url = HttpContext.Current.Request.RawUrl + "";
if (!NotFoundFiles.Contains(url))
NotFoundFiles.Add(url);
else
ex = null;
}
// 拦截没有权限
if (ex is NoPermissionException nex)
{
ctx.Result = ctx.Controller.NoPermission(nex);
ctx.ExceptionHandled = true;
}
if (ex != null) XTrace.WriteException(ex);
}
if (ctx.ExceptionHandled) return;
// 判断控制器是否在管辖范围之内,不拦截其它控制器的异常信息
if (Setting.Current.CatchAllException || AreaRegistrationBase.Contains(ctx.Controller))
{
ctx.ExceptionHandled = true;
var ctrl = "";
var act = "";
if (ctx.RouteData.Values.ContainsKey("controller")) ctrl = ctx.RouteData.Values["controller"] + "";
if (ctx.RouteData.Values.ContainsKey("action")) act = ctx.RouteData.Values["action"] + "";
if (ctx.RequestContext.HttpContext.Request.IsAjaxRequest())
{
if (act.IsNullOrEmpty()) act = "操作";
ctx.Result = ControllerHelper.JsonTips("[{0}]失败!{1}".F(act, ex.Message));
}
else
{
var vr = new ViewResult
{
ViewName = "CubeError"
};
vr.ViewBag.Context = ctx;
var vd = vr.ViewData = ctx.Controller.ViewData;
vd.Model = new HandleErrorInfo(ex, ctrl, act);
ctx.Result = vr;
}
}
base.OnException(ctx);
}
}
}

View File

@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using NewLife.Log;
namespace NewLife.Cube
{
/// <summary>自定义视图引擎。为了让系统优先查找当前区域目录</summary>
public class RazorViewEngineX : RazorViewEngine
{
/// <summary>实例化修改Areas搜索逻辑</summary>
public RazorViewEngineX()
{
var list = new List<String>();
//list.Add("~/{2}/Views/{1}/{0}.cshtml");
list.Add("~/{2}/Views/Shared/{0}.cshtml");
list.Add("~/Areas/{2}/Views/{1}/{0}.cshtml");
list.Add("~/Areas/{2}/Views/Shared/{0}.cshtml");
var arr = list.ToArray();
AreaViewLocationFormats = arr;
AreaMasterLocationFormats = arr;
AreaPartialViewLocationFormats = arr;
}
/// <summary>注册需要搜索的目录路径</summary>
/// <param name="engines"></param>
public static void Register(ViewEngineCollection engines)
{
// 如果没有注册,则注册
var ve = engines.FirstOrDefault(e => e is RazorViewEngineX) as RazorViewEngineX;
if (ve == null)
{
// 干掉旧引擎,使用新引擎
var ve2 = engines.FirstOrDefault(e => e is RazorViewEngine);
engines.Remove(ve2);
ve = new RazorViewEngineX();
engines.Insert(0, ve);
XTrace.WriteLine("注册视图引擎:{0}", ve.GetType().FullName);
}
}
}
}

View File

@ -1,61 +0,0 @@
using System;
using System.Web.Mvc;
using NewLife.Log;
using NewLife.Web;
namespace NewLife.Cube
{
/// <summary>SSL特性</summary>
public class RequireSslAttribute : FilterAttribute, IAuthorizationFilter
{
#region
#endregion
#region
/// <summary>验证时</summary>
/// <param name="filterContext"></param>
public virtual void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));
// 强制SSL
var cfg = Setting.Current;
if (cfg.SslMode < SslModes.HomeOnly) return;
var req = filterContext.HttpContext.Request;
if (!req.IsSecureConnection && !req.IsLocal && !req.IsAjaxRequest() && req.HttpMethod.EqualIgnoreCase("GET"))
HandleNonHttpsRequest(filterContext);
}
/// <summary>拦截非Http请求</summary>
/// <param name="filterContext"></param>
protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
var req = filterContext.HttpContext.Request;
// 有可能前端访问的是https经反向代理后变成http
var uri = req.GetRawUrl();
if (uri.Scheme.StartsWith("https")) return;
var url = "https://" + uri.Host + req.RawUrl;
filterContext.Result = new RedirectResult(url);
}
#endregion
#region
private static Boolean _inited;
/// <summary>注册全局过滤器</summary>
public void Register()
{
if (_inited) return;
_inited = true;
// 注册过滤器
XTrace.WriteLine("注册SSL过滤器{0}", GetType().FullName);
var filters = GlobalFilters.Filters;
filters.Add(this);
}
#endregion
}
}

View File

@ -0,0 +1,442 @@
using System.Security.Principal;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Net.Http.Headers;
using NewLife.Common;
using NewLife.Cube.Entity;
using NewLife.Cube.Extensions;
using NewLife.Cube.Services;
using NewLife.Cube.Web;
using NewLife.Log;
using NewLife.Model;
using NewLife.Serialization;
using XCode;
using XCode.Membership;
using IServiceCollection = Microsoft.Extensions.DependencyInjection.IServiceCollection;
using JwtBuilder = NewLife.Web.JwtBuilder;
namespace NewLife.Cube;
/// <inheritdoc />
public class ManageProvider2 : ManageProvider
{
#region
internal static IHttpContextAccessor Context;
/// <summary>
/// 节点路由
/// </summary>
public static IEndpointRouteBuilder EndpointRoute { get; set; }
#endregion
#region
/// <summary>保存于Session的凭证</summary>
public String SessionKey { get; set; } = "Admin";
#endregion
///// <summary>当前管理提供者</summary>
//public new static IManageProvider Provider => ObjectContainer.Current.ResolveInstance<IManageProvider>();
#region IManageProvider
/// <summary>获取当前用户</summary>
/// <param name="context"></param>
/// <returns></returns>
public override IManageUser GetCurrent(IServiceProvider context = null)
{
var ctx = (ModelExtension.GetService<IHttpContextAccessor>(context) ?? Context)?.HttpContext;
if (ctx == null) return null;
try
{
if (ctx.Items["CurrentUser"] is IManageUser user) return user;
var session = ctx.Items["Session"] as IDictionary<String, Object>;
user = session?[SessionKey] as IManageUser;
ctx.Items["CurrentUser"] = user;
return user;
}
catch (InvalidOperationException ex)
{
// 这里捕获一下防止初始化应用中session还没初始化好报的异常
// 这里有个问题就是这里的ctx会有两个不同的值
XTrace.WriteException(ex);
return null;
}
}
/// <summary>设置当前用户</summary>
/// <param name="user"></param>
/// <param name="context"></param>
public override void SetCurrent(IManageUser user, IServiceProvider context = null)
{
var ctx = (ModelExtension.GetService<IHttpContextAccessor>(context) ?? Context)
?.HttpContext;
if (ctx == null) return;
ctx.Items["CurrentUser"] = user;
var session = ctx.Items["Session"] as IDictionary<String, Object>;
if (session == null) return;
var key = SessionKey;
// 特殊处理注销
if (user == null)
{
session.Remove(key);
session.Remove("userId");
}
else
{
session[key] = user;
session["userId"] = user.ID;
}
}
/// <summary>登录</summary>
/// <param name="name"></param>
/// <param name="password"></param>
/// <param name="remember">是否记住密码</param>
/// <returns></returns>
public override IManageUser Login(String name, String password, Boolean remember)
{
IManageUser user = null;
// OAuth密码模式登录
var oauths = OAuthConfig.GetValids(GrantTypes.Password);
if (oauths.Count > 0)
user = LoginByOAuth(oauths[0], name, password);
else
user = base.Login(name, password, remember);
user = CheckAgent(user) as User;
Current = user;
// 过期时间
var set = Setting.Current;
var expire = TimeSpan.FromMinutes(0);
if (remember && user != null)
{
expire = TimeSpan.FromDays(365);
}
else
{
if (set.SessionTimeout > 0)
expire = TimeSpan.FromSeconds(set.SessionTimeout);
}
// 保存Cookie
var context = Context?.HttpContext;
this.SaveCookie(user, expire, context);
return user;
}
private SsoClient _client;
private IManageUser LoginByOAuth(OAuthConfig oa, String username, String password)
{
_client ??= new SsoClient
{
Server = oa.Server,
AppId = oa.AppId,
Secret = oa.Secret,
SecurityKey = oa.SecurityKey,
};
//var ti = _client.GetToken(username, password).Result;
//var ui = _client.GetUser(ti.AccessToken).Result as User;
var ui = _client.UserAuth(username, password).Result;
var set = Setting.Current;
var log = LogProvider.Provider;
// 仅验证登录,不要角色信息
if (FindByName(username) is not User user)
{
if (!set.AutoRegister && !oa.AutoRegister)
{
log.WriteLog(typeof(User), "SSO登录", false, $"无法找到[{username}],且没有打开自动注册", 0, username);
throw new XException($"无法找到[{username}],且没有打开自动注册");
}
user = new User
{
Code = ui["usercode"] as String,
Name = username,
DisplayName = ui["nickname"] as String,
Enable = true,
RegisterTime = DateTime.Now,
};
// 新注册用户采用魔方默认角色
var defRole = oa.AutoRole;
if (defRole.IsNullOrEmpty()) defRole = set.DefaultRole;
user.RoleID = Role.GetOrAdd(defRole).ID;
}
user.Logins++;
user.LastLogin = DateTime.Now;
user.Save();
log.WriteLog(user.GetType(), "OAuth登录", true, $"用户[{user}]使用[{username}]登录[{_client.AppId}]成功!" + Environment.NewLine + ui.ToJson());
return user;
}
/// <summary>检查委托代理</summary>
/// <param name="user"></param>
/// <returns></returns>
public IManageUser CheckAgent(IManageUser user)
{
if (user == null) return user;
// 查找该用户是否有可用待立项,按照创建代理的先后顺序
var list = PrincipalAgent.GetAllValidByAgentId(user.ID);
if (list.Count == 0) return user;
// 脏数据检查
foreach (var item in list)
{
// 没有次数或者已过期,则禁用
if (item.Enable && (item.Times == 0 || item.Expire.Year > 2000 && item.Expire < DateTime.Now))
{
item.Enable = false;
item.Update();
}
}
// 查找一个可用项
var pa = list.FirstOrDefault(e => e.Enable);
if (pa == null || pa.Principal == null) return user;
var roles = pa.Principal?.Roles;
if (roles != null && roles.Any(e => e.IsSystem))
{
pa.Enable = false;
pa.Remark = "安全起见,不得代理系统管理员";
pa.Update();
LogProvider.Provider.WriteLog("用户", "代理", false, $"安全起见,[{pa.AgentName}]不得代理系统管理员[{pa.PrincipalName}]的身份权限", pa.AgentId, pa.AgentName);
return user;
}
pa.Times--;
if (pa.Times == 0) pa.Enable = false;
pa.Update();
LogProvider.Provider.WriteLog("用户", "委托", true, $"委托[{pa.AgentName}]使用[{pa.PrincipalName}]的身份权限", pa.PrincipalId, pa.PrincipalName);
LogProvider.Provider.WriteLog("用户", "代理", true, $"[{pa.AgentName}]代理使用[{pa.PrincipalName}]的身份权限", pa.AgentId, pa.AgentName);
return pa.Principal as IManageUser;
}
/// <summary>注销</summary>
public override void Logout()
{
if (Current is User user) UserService.ClearOnline(user);
// 注销时销毁所有Session
var context = Context?.HttpContext;
var session = context.Items["Session"] as IDictionary<String, Object>;
session?.Clear();
// 销毁Cookie
this.SaveCookie(null, TimeSpan.Zero, context);
base.Logout();
}
#endregion
}
/// <summary>管理提供者助手</summary>
public static class ManagerProviderHelper
{
/// <summary>设置当前用户</summary>
/// <param name="provider">提供者</param>
/// <param name="context">Http上下文兼容NetCore</param>
public static void SetPrincipal(this IManageProvider provider, IServiceProvider context = null)
{
var ctx = ModelExtension.GetService<IHttpContextAccessor>(context)?.HttpContext;
if (ctx == null) return;
var user = provider.GetCurrent(context);
if (user == null) return;
if (user is not IIdentity id || ctx.User?.Identity == id) return;
// 角色列表
var roles = new List<String>();
if (user is IUser user2) roles.AddRange(user2.Roles.Select(e => e + ""));
var up = new GenericPrincipal(id, roles.ToArray());
ctx.User = up;
Thread.CurrentPrincipal = up;
}
/// <summary>尝试登录。如果Session未登录则借助Cookie</summary>
/// <param name="provider">提供者</param>
/// <param name="context">Http上下文兼容NetCore</param>
public static IManageUser TryLogin(this IManageProvider provider, HttpContext context)
{
var serviceProvider = context?.RequestServices;
// 判断当前登录用户
var user = provider.GetCurrent(serviceProvider);
if (user == null)
{
// 尝试从Cookie登录
user = provider.LoadCookie(true, context);
if (user != null) provider.SetCurrent(user, serviceProvider);
}
// 设置前端当前用户
if (user != null) provider.SetPrincipal(serviceProvider);
return user;
}
/// <summary>生成令牌</summary>
/// <returns></returns>
public static JwtBuilder GetJwt()
{
var set = Setting.Current;
// 生成令牌
var ss = set.JwtSecret?.Split(':');
if (ss == null || ss.Length < 2) throw new InvalidOperationException("未设置JWT算法和密钥");
var jwt = new JwtBuilder
{
Algorithm = ss[0],
Secret = ss[1],
};
return jwt;
}
#region Cookie
/// <summary>从Cookie加载用户信息</summary>
/// <param name="provider">提供者</param>
/// <param name="autologin">是否自动登录</param>
/// <param name="context">Http上下文兼容NetCore</param>
/// <returns></returns>
public static IManageUser LoadCookie(this IManageProvider provider, Boolean autologin, HttpContext context)
{
var key = $"token-{SysConfig.Current.Name}";
var req = context?.Request;
var token = req?.Cookies[key];
// 尝试从url中获取token
if (token.IsNullOrEmpty() || token.Split(".").Length != 3) token = req?.Query["token"];
if (token.IsNullOrEmpty() || token.Split(".").Length != 3) token = req?.Query["jwtToken"];
// 尝试从头部获取token
if (token.IsNullOrEmpty() || token.Split(".").Length != 3) token = req?.Headers[HeaderNames.Authorization];
if (token.IsNullOrEmpty() || token.Split(".").Length != 3) return null;
token = token.Replace("Bearer ", "", StringComparison.OrdinalIgnoreCase);
var jwt = GetJwt();
if (!jwt.TryDecode(token, out var msg))
{
XTrace.WriteLine("令牌无效:{0}, token={1}", msg, token);
return null;
}
var user = jwt.Subject;
if (user.IsNullOrEmpty()) return null;
// 判断有效期
if (jwt.Expire < DateTime.Now)
{
XTrace.WriteLine("令牌过期:{0} {1}", jwt.Expire, token);
return null;
}
var u = provider.FindByName(user);
if (u == null || !u.Enable) return null;
// 保存登录信息。如果是json请求不用记录自动登录
if (autologin && u is IAuthUser mu && !req.IsAjaxRequest())
{
mu.SaveLogin(null);
LogProvider.Provider.WriteLog("用户", "自动登录", true, $"{user} Time={jwt.IssuedAt} Expire={jwt.Expire} Token={token}", u.ID, u + "", ip: context.GetUserHost());
}
return u;
}
/// <summary>保存用户信息到Cookie</summary>
/// <param name="provider">提供者</param>
/// <param name="user">用户</param>
/// <param name="expire">过期时间</param>
/// <param name="context">Http上下文兼容NetCore</param>
public static void SaveCookie(this IManageProvider provider, IManageUser user, TimeSpan expire, HttpContext context)
{
var res = context?.Response;
if (res == null) return;
var option = new CookieOptions();
option.SameSite = (Microsoft.AspNetCore.Http.SameSiteMode)Setting.Current.SameSiteMode;
var token = "";
if (user != null)
{
// 令牌有效期默认2小时
var exp = DateTime.Now.Add(expire.TotalSeconds > 0 ? expire : TimeSpan.FromHours(2));
var jwt = GetJwt();
jwt.Subject = user.Name;
jwt.Expire = exp;
token = jwt.Encode(null);
if (expire.TotalSeconds > 0) option.Expires = DateTimeOffset.Now.Add(expire);
}
else
{
option.Expires = DateTimeOffset.MinValue;
}
var key = $"token-{SysConfig.Current.Name}";
res.Cookies.Append(key, token, option);
context.Items["jwtToken"] = token;
}
#endregion
/// <summary>
/// 添加管理提供者
/// </summary>
/// <param name="service"></param>
public static void AddManageProvider(this IServiceCollection service)
{
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
service.TryAddSingleton<IManageProvider, ManageProvider2>();
}
/// <summary>
/// 使用管理提供者
/// </summary>
/// <param name="app"></param>
public static void UseManagerProvider(this IApplicationBuilder app)
{
XTrace.WriteLine("初始化ManageProvider");
var provider = app.ApplicationServices;
ManageProvider.Provider = ModelExtension.GetService<IManageProvider>(provider);
//ManageProvider2.EndpointRoute = (IEndpointRouteBuilder)app.Properties["__EndpointRouteBuilder"];
ManageProvider2.Context = ModelExtension.GetService<IHttpContextAccessor>(provider);
// 初始化数据库
//_ = Role.Meta.Count;
EntityFactory.InitConnection("Membership");
EntityFactory.InitConnection("Log");
EntityFactory.InitConnection("Cube");
}
}

View File

@ -0,0 +1,264 @@
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NewLife.Log;
using NewLife.Reflection;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Membership;
/// <summary>
/// 菜单助手
/// </summary>
public static class MenuHelper
{
/// <summary>扫描命名空间下的控制器并添加为菜单</summary>
/// <param name="menuFactory">菜单工厂</param>
/// <param name="rootName">根菜单名称,所有菜单附属在其下</param>
/// <param name="areaType">区域类型</param>
/// <returns></returns>
public static IList<IMenu> ScanController(this IMenuFactory menuFactory, String rootName, Type areaType)
{
var nameSpace = areaType.Namespace.EnsureEnd(".Controllers");
using var span = DefaultTracer.Instance?.NewSpan(nameof(ScanController), rootName);
var list = new List<IMenu>();
// 所有控制器
var types = areaType.Assembly.GetTypes();
var controllerTypes = types.Where(e => e.Name.EndsWith("Controller") && e.Namespace == nameSpace).ToList();
if (controllerTypes.Count == 0) return list;
// 如果根菜单不存在,则添加
var r = menuFactory.Root;
var root = menuFactory.FindByFullName(nameSpace);
root ??= r.FindByPath(rootName);
//if (root == null) root = r.Childs.FirstOrDefault(e => e.Name.EqualIgnoreCase(rootName));
//if (root == null) root = r.Childs.FirstOrDefault(e => e.Url.EqualIgnoreCase("~/" + rootName));
if (root == null)
{
root = r.Add(rootName, null, nameSpace, "/" + rootName);
list.Add(root);
var att = areaType.GetCustomAttribute<MenuAttribute>();
if (att != null && (!root.Visible || !root.Necessary))
{
root.Sort = att.Order;
root.Visible = att.Visible;
root.Icon = att.Icon;
}
}
if (root.FullName != nameSpace) root.FullName = nameSpace;
(root as IEntity).Save();
// 菜单迁移 Admin->Cube
IMenu adminRoot = null;
if (rootName == "Cube") adminRoot = r.FindByPath("Admin");
var ms = new List<IMenu>();
// 遍历该程序集所有类型
foreach (var type in controllerTypes)
{
var name = type.Name.TrimEnd("Controller");
var url = root.Url + "/" + name;
var node = root;
// 添加Controller
var controller = node.FindByPath(name);
// 旧菜单迁移到新菜单
if (adminRoot != null)
{
if (controller == null)
{
controller = adminRoot.FindByPath(name);
if (controller != null)
{
controller.ParentID = root.ID;
controller.Url = url;
}
}
else
{
var controller2 = adminRoot.FindByPath(name);
if (controller2 is IEntity entity) entity.Delete();
}
}
if (controller == null)
{
controller = menuFactory.FindByUrl(url);
controller ??= node.Add(name, type.GetDisplayName(), type.FullName, url);
}
controller.Url = url;
controller.FullName = type.FullName;
if (controller.Remark.IsNullOrEmpty()) controller.Remark = type.GetDescription();
ms.Add(controller);
list.Add(controller);
// 获取动作
var acts = ScanActionMenu(type, controller);
if (acts != null && acts.Count > 0)
{
// 可选权限子项
controller.Permissions.Clear();
// 添加该类型下的所有Action作为可选权限子项
foreach (var item in acts)
{
var method = item.Key;
var dn = method.GetDisplayName();
if (!dn.IsNullOrEmpty()) dn = dn.Replace("{type}", (controller as Menu)?.FriendName);
var pmName = !dn.IsNullOrEmpty() ? dn : method.Name;
if (item.Value <= (Int32)PermissionFlags.Delete) pmName = ((PermissionFlags)item.Value).GetDescription();
controller.Permissions[item.Value] = pmName;
}
}
// 反射调用控制器的方法来获取动作。作为过渡,将来取消
var func = type.GetMethodEx("ScanActionMenu");
if (func != null)
{
// 由于控制器使用IOC无法直接实例化控制器需要给各个参数传入空
var ctor = type.GetConstructors()?.FirstOrDefault();
if (ctor != null)
{
var ctrl = ctor.Invoke(new Object[ctor.GetParameters().Length]);
//var ctrl = type.CreateInstance();
acts = func.As<Func<IMenu, IDictionary<MethodInfo, Int32>>>(ctrl).Invoke(controller);
if (acts != null && acts.Count > 0)
{
// 可选权限子项
controller.Permissions.Clear();
// 添加该类型下的所有Action作为可选权限子项
foreach (var item in acts)
{
var method = item.Key;
var dn = method.GetDisplayName();
if (!dn.IsNullOrEmpty()) dn = dn.Replace("{type}", (controller as Menu)?.FriendName);
var pmName = !dn.IsNullOrEmpty() ? dn : method.Name;
if (item.Value <= (Int32)PermissionFlags.Delete) pmName = ((PermissionFlags)item.Value).GetDescription();
controller.Permissions[item.Value] = pmName;
}
}
}
}
var att = type.GetCustomAttribute<MenuAttribute>();
if (att != null)
{
if (controller.Icon.IsNullOrEmpty()) controller.Icon = att.Icon;
}
// 排序
if (controller.Sort == 0)
{
if (att != null)
{
if (!root.Visible || !root.Necessary)
{
controller.Sort = att.Order;
controller.Visible = att.Visible;
}
}
else
{
var pi = type.GetPropertyEx("MenuOrder");
if (pi != null) controller.Sort = pi.GetValue(null).ToInt();
}
}
}
var rs = 0;
for (var i = 0; i < ms.Count; i++)
{
rs += (ms[i] as IEntity).Save();
}
// 如果新增了菜单,需要检查权限
if (rs > 0)
{
var task = Task.Run(() =>
{
XTrace.WriteLine("新增了菜单,需要检查权限");
//var fact = ManageProvider.GetFactory<IRole>();
var fact = typeof(Role).AsFactory();
fact.EntityType.Invoke("CheckRole");
});
task.Wait(5_000);
}
return list;
}
/// <summary>获取可用于生成权限菜单的Action集合</summary>
/// <param name="controllerType">控制器类型</param>
/// <param name="menu">该控制器所在菜单</param>
/// <returns></returns>
private static IDictionary<MethodInfo, Int32> ScanActionMenu(Type controllerType, IMenu menu)
{
var dic = new Dictionary<MethodInfo, Int32>();
//var factory = type.GetProperty("Factory", BindingFlags.Static | BindingFlags.Public | BindingFlags.GetProperty) as IEntityFactory;
var pi = controllerType.GetPropertyEx("Factory");
var factory = pi?.GetValue(null, null) as IEntityFactory;
//if (factory == null) return dic;
// 设置显示名
if (menu.DisplayName.IsNullOrEmpty() && factory != null)
{
menu.DisplayName = factory.Table.DataTable.DisplayName;
menu.Visible = true;
//menu.Save();
}
// 添加该类型下的所有Action
foreach (var method in controllerType.GetMethods())
{
if (method.IsStatic || !method.IsPublic) continue;
if (!method.ReturnType.As<ActionResult>() && !method.ReturnType.As<Task<ActionResult>>()) continue;
//if (method.GetCustomAttribute<HttpPostAttribute>() != null) continue;
if (method.GetCustomAttribute<AllowAnonymousAttribute>() != null) continue;
var attAuth = method.GetCustomAttribute<EntityAuthorizeAttribute>();
// 添加菜单
var attMenu = method.GetCustomAttribute<MenuAttribute>();
if (attMenu != null)
{
// 添加系统信息菜单
var name = method.Name;
var m2 = menu.Parent.Childs.FirstOrDefault(_ => _.Name == name);
m2 ??= menu.Parent.Add(name, method.GetDisplayName(), $"{controllerType.FullName}.{name}", $"{menu.Url}/{name}");
if (m2.Sort == 0) m2.Sort = attMenu.Order;
if (m2.Icon.IsNullOrEmpty()) m2.Icon = attMenu.Icon;
if (m2.FullName.IsNullOrEmpty()) m2.FullName = $"{controllerType.FullName}.{name}";
if (attAuth != null) m2.Permissions[(Int32)attAuth.Permission] = attAuth.Permission.GetDescription();
if (m2 is IEntity entity) entity.Update();
}
else
{
//var attAuth = method.GetCustomAttribute<EntityAuthorizeAttribute>();
if (attAuth != null && attAuth.Permission > PermissionFlags.None) dic.Add(method, (Int32)attAuth.Permission);
}
}
// 只写实体类过滤掉添删改权限
if (factory != null && factory.Table.DataTable.InsertOnly)
{
var arr = new[] { PermissionFlags.Insert, PermissionFlags.Update, PermissionFlags.Delete }.Select(e => (Int32)e).ToArray();
dic = dic.Where(e => !arr.Contains(e.Value)).ToDictionary(e => e.Key, e => e.Value);
}
return dic;
}
}

View File

@ -0,0 +1,192 @@
using System.Collections;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.Serialization;
using System.Xml.Serialization;
using NewLife.Collections;
using XCode;
using XCode.Configuration;
namespace NewLife.Cube.ViewModels;
/// <summary>获取数据源委托</summary>
/// <param name="entity"></param>
/// <returns></returns>
public delegate IDictionary DataSourceDelegate(Object entity);
/// <summary>数据可见委托</summary>
/// <param name="entity"></param>
/// <returns></returns>
public delegate Boolean DataVisibleDelegate(Object entity);
/// <summary>数据字段</summary>
public class DataField
{
#region
/// <summary>名称</summary>
public String Name { get; set; }
/// <summary>显示名</summary>
public String DisplayName { get; set; }
/// <summary>描述</summary>
public String Description { get; set; }
/// <summary>类别</summary>
public String Category { get; set; }
/// <summary>属性类型</summary>
[IgnoreDataMember]
public Type Type { get; set; }
///// <summary>数据类型</summary>
//public String DataType { get; set; }
/// <summary>元素类型。image,file,html,singleSelect,multipleSelect</summary>
public String ItemType { get; set; }
/// <summary>长度</summary>
public Int32 Length { get; set; }
/// <summary>精度</summary>
public Int32 Precision { get; set; }
/// <summary>位数</summary>
public Int32 Scale { get; set; }
/// <summary>允许空</summary>
public Boolean Nullable { get; set; }
/// <summary>主键</summary>
public Boolean PrimaryKey { get; set; }
/// <summary>只读</summary>
public Boolean Readonly { get; set; }
///// <summary>排序</summary>
//public Int32 Sort { get; set; }
/// <summary>原始字段</summary>
public FieldItem Field { get; set; }
/// <summary>映射字段</summary>
public String MapField { get; set; }
/// <summary>映射提供者</summary>
[XmlIgnore, IgnoreDataMember]
public MapProvider MapProvider { get; set; }
/// <summary>多选数据源</summary>
public DataSourceDelegate DataSource { get; set; }
/// <summary>是否显示</summary>
public DataVisibleDelegate DataVisible { get; set; }
/// <summary>扩展属性</summary>
[XmlIgnore, IgnoreDataMember]
public IDictionary<String, String> Properties { get; set; } = new NullableDictionary<String, String>(StringComparer.OrdinalIgnoreCase);
#endregion
#region
/// <summary>已重载</summary>
/// <returns></returns>
public override String ToString() => $"{Name} {DisplayName} {Type.Name}";
#endregion
#region
///// <summary>实例化</summary>
//public DataField() { }
/// <summary>从FieldItem填充</summary>
/// <param name="field"></param>
public virtual void Fill(FieldItem field)
{
Field = field;
var dc = field.Field;
//var pi = field.GetValue("_Property", false) as PropertyInfo;
var pi = field.Property;
Name = field.Name;
DisplayName = field.DisplayName;
Description = field.Description;
Category = pi?.GetCustomAttribute<CategoryAttribute>()?.Category + "";
Type = field.Type;
//DataType = field.Type.Name;
Length = field.Length;
Nullable = field.IsNullable;
PrimaryKey = field.PrimaryKey;
Readonly = field.ReadOnly;
if (field.Map != null)
{
MapField = field.Map.Name;
MapProvider = field.Map.Provider;
}
if (dc != null)
{
ItemType = dc.ItemType;
Precision = dc.Precision;
Scale = dc.Scale;
if (dc.Properties != null)
{
foreach (var item in dc.Properties)
{
Properties[item.Key] = item.Value;
}
}
}
}
/// <summary>克隆</summary>
/// <returns></returns>
public virtual DataField Clone()
{
//var df = GetType().CreateInstance() as DataField;
//df.Name = Name;
//df.DisplayName = DisplayName;
//df.Description = Description;
//df.Category = Category;
//df.Type = Type;
//df.DataType = DataType;
//df.ItemType = ItemType;
//df.Length = Length;
//df.Precision = Precision;
//df.Scale = Scale;
//df.Nullable = Nullable;
//df.PrimaryKey = PrimaryKey;
//df.Readonly = Readonly;
//df.MapField = MapField;
//df.MapProvider = MapProvider;
//df.DataSource = DataSource;
//df.Properties = Properties;
//return df;
return MemberwiseClone() as DataField;
}
/// <summary>是否大文本字段</summary>
/// <returns></returns>
public virtual Boolean IsBigText() => Type == typeof(String) && (Length < 0 || Length >= 300 || Length >= 200 && Name.EqualIgnoreCase("Remark", "Description", "Comment"));
#endregion
#region
private readonly List<Object> _services = new();
/// <summary>添加服务</summary>
/// <typeparam name="TService"></typeparam>
/// <param name="service"></param>
public virtual void AddService<TService>(TService service) => _services.Add(service);
/// <summary>获取服务</summary>
/// <typeparam name="TService"></typeparam>
/// <returns></returns>
public virtual TService GetService<TService>() => (TService)_services.FirstOrDefault(e => e is TService);
#endregion
}

View File

@ -0,0 +1,17 @@
using System;
namespace NewLife.Cube.ViewModels
{
/// <summary>错误模型</summary>
public class ErrorModel
{
/// <summary>请求标识</summary>
public String RequestId { get; set; }
/// <summary>资源地址</summary>
public Uri Uri { get; set; }
/// <summary>异常信息</summary>
public Exception Exception { get; set; }
}
}

View File

@ -0,0 +1,310 @@
using System.Reflection;
using NewLife.Cube.ViewModels;
using XCode;
using XCode.Configuration;
namespace NewLife.Cube;
/// <summary>分组可见委托</summary>
/// <param name="entity"></param>
/// <param name="group"></param>
/// <returns></returns>
public delegate Boolean GroupVisibleDelegate(IEntity entity, String group);
/// <summary>字段集合</summary>
public class FieldCollection : List<DataField>
{
#region
/// <summary>类型</summary>
public String Kind { get; set; }
/// <summary>工厂</summary>
public IEntityFactory Factory { get; set; }
/// <summary>需要隐藏的分组名</summary>
public ICollection<String> HiddenGroups { get; } = new HashSet<String>();
/// <summary>是否显示分组</summary>
public GroupVisibleDelegate GroupVisible { get; set; }
#endregion
#region
/// <summary>实例化一个字段集合</summary>
/// <param name="kind"></param>
public FieldCollection(String kind) => Kind = kind;
/// <summary>使用工厂实例化一个字段集合</summary>
/// <param name="factory"></param>
/// <param name="kind"></param>
public FieldCollection(IEntityFactory factory, String kind)
{
Kind = kind;
Factory = factory;
//AddRange(Factory.Fields);
if (factory != null)
{
foreach (var item in factory.Fields)
{
Add(item);
}
switch (kind)
{
case "AddForm":
SetRelation(true);
//RemoveCreateField();
RemoveUpdateField();
break;
case "EditForm":
SetRelation(true);
break;
case "Detail":
SetRelation(true);
break;
case "Form":
SetRelation(true);
break;
case "List":
default:
SetRelation(false);
break;
}
}
}
#endregion
#region
/// <summary>为指定字段创建数据字段,可以为空</summary>
/// <param name="field"></param>
/// <returns></returns>
public DataField Create(FieldItem field)
{
DataField df = Kind switch
{
"AddForm" => new FormField(),
"EditForm" => new FormField(),
"Detail" => new FormField(),
"Form" => new FormField(),
"List" => new ListField(),
_ => throw new NotImplementedException(),
};
//df.Sort = Count + 1;
//df.Sort = Count == 0 ? 1 : (this[Count - 1].Sort + 1);
if (field != null) df.Fill(field);
return df;
}
/// <summary>为指定字段创建数据字段</summary>
/// <param name="field"></param>
/// <returns></returns>
public DataField Add(FieldItem field)
{
var df = Create(field);
Add(df);
return df;
}
/// <summary>设置扩展关系</summary>
/// <param name="isForm">是否表单使用</param>
/// <returns></returns>
public FieldCollection SetRelation(Boolean isForm)
{
var type = Factory.EntityType;
// 扩展属性
foreach (var pi in type.GetProperties())
{
// 处理带有Map特性的扩展属性
var map = pi.GetCustomAttribute<MapAttribute>();
if (map != null) Replace(map.Name, pi.Name);
}
if (!isForm)
{
// 长字段和密码字段不显示
NoPass();
}
return this;
}
private void NoPass()
{
for (var i = Count - 1; i >= 0; i--)
{
var fi = this[i];
if (fi.Type == typeof(String) && fi.MapField.IsNullOrEmpty())
{
if (fi.Length <= 0 || fi.Length > 1000 ||
fi.Name.EqualIgnoreCase("password", "pass", "pwd", "Secret"))
{
RemoveAt(i);
}
}
}
}
#endregion
#region
/// <summary>查找指定字段</summary>
/// <param name="name"></param>
/// <returns></returns>
public Int32 FindIndex(String name) => FindIndex(e => e.Name.EqualIgnoreCase(name));
/// <summary>从AllFields中添加字段可以是扩展属性</summary>
/// <param name="name"></param>
/// <returns></returns>
public FieldCollection AddField(String name)
{
var fi = Factory.AllFields.FirstOrDefault(e => e.Name.EqualIgnoreCase(name));
if (fi != null) Add(fi);
return this;
}
/// <summary>删除字段</summary>
/// <param name="names"></param>
/// <returns></returns>
public FieldCollection RemoveField(params String[] names)
{
foreach (var item in names)
{
if (!item.IsNullOrEmpty()) RemoveAll(e => e.Name.EqualIgnoreCase(item));
}
return this;
}
/// <summary>操作字段列表,把旧项换成新项</summary>
/// <param name="oriName"></param>
/// <param name="newName"></param>
/// <returns></returns>
public FieldCollection Replace(String oriName, String newName)
{
var idx = FindIndex(e => e.Name.EqualIgnoreCase(oriName));
if (idx < 0) return this;
var fi = Factory.AllFields.FirstOrDefault(e => e.Name.EqualIgnoreCase(newName));
if (fi == null) return this;
// 如果本身就存在目标项,则删除
var idx2 = FindIndex(e => e.Name.EqualIgnoreCase(fi.Name));
if (idx2 >= 0) RemoveAt(idx2);
this[idx] = Create(fi);
return this;
}
#endregion
#region /
/// <summary>设置是否显示创建信息</summary>
/// <returns></returns>
public FieldCollection RemoveCreateField()
{
RemoveAll(e => e.Name.EqualIgnoreCase("CreateUserID", "CreateUser", "CreateTime", "CreateIP"));
return this;
}
/// <summary>设置是否显示更新信息</summary>
/// <returns></returns>
public FieldCollection RemoveUpdateField()
{
RemoveAll(e => e.Name.EqualIgnoreCase("UpdateUserID", "UpdateUser", "UpdateTime", "UpdateIP"));
return this;
}
/// <summary>设置是否显示备注信息</summary>
/// <returns></returns>
public FieldCollection RemoveRemarkField()
{
RemoveAll(e => e.Name.EqualIgnoreCase("Remark", "Description"));
return this;
}
#endregion
#region
/// <summary>添加定制字段,插入指定列前后</summary>
/// <param name="name"></param>
/// <param name="beforeName"></param>
/// <param name="afterName"></param>
/// <returns></returns>
public DataField AddDataField(String name, String beforeName = null, String afterName = null)
{
if (name.IsNullOrEmpty()) throw new ArgumentNullException(nameof(name));
var fi = Factory.AllFields.FirstOrDefault(e => e.Name.EqualIgnoreCase(name));
// 有可能fi为空创建一个所有字段都为空的field
var field = Create(fi);
if (field.Name.IsNullOrEmpty()) field.Name = name;
if (!beforeName.IsNullOrEmpty())
{
var idx = FindIndex(beforeName);
if (idx >= 0)
Insert(idx, field);
else
Add(field);
}
else if (!afterName.IsNullOrEmpty())
{
var idx = FindIndex(afterName);
if (idx >= 0)
Insert(idx + 1, field);
else
Add(field);
}
else
Add(field);
return field;
}
/// <summary>添加定制字段,插入指定列前后</summary>
/// <param name="name"></param>
/// <param name="beforeName"></param>
/// <param name="afterName"></param>
/// <returns></returns>
public ListField AddListField(String name, String beforeName = null, String afterName = null) => AddDataField(name, beforeName, afterName) as ListField;
/// <summary>获取指定名称的定制字段</summary>
/// <param name="name"></param>
/// <returns></returns>
public DataField GetField(String name) => this.FirstOrDefault(e => name.EqualIgnoreCase(e.Name, e.MapField));
#endregion
#region
/// <summary>按类别分组获取字段列表</summary>
/// <param name="entity">实体对象</param>
/// <returns></returns>
public IDictionary<String, IList<DataField>> GroupByCategory(IEntity entity)
{
var dic = new Dictionary<String, IList<DataField>>();
var groupFields = this.GroupBy(e => e.Category + "").ToList();
foreach (var item in groupFields)
{
var key = item.Key.IsNullOrEmpty() ? "默认" : item.Key;
if (HiddenGroups.Contains(key)) continue;
if (GroupVisible != null && !GroupVisible(entity, key)) continue;
if (!dic.TryGetValue(key, out var list))
dic[key] = list = new List<DataField>();
//(list as List<DataField>).AddRange(item);
foreach (var elm in item)
{
list.Add(elm);
}
}
return dic;
}
#endregion
}

View File

@ -0,0 +1,9 @@
namespace NewLife.Cube.ViewModels
{
/// <summary>表单字段</summary>
public class FormField : DataField
{
/// <summary>表单字段的分部视图名称。允许针对字段定义视图</summary>
public String GroupView { get; set; }
}
}

View File

@ -0,0 +1,59 @@
using System;
namespace NewLife.Cube.ViewModels
{
/// <summary>界面元素模型</summary>
public class ItemModel
{
#region
/// <summary>名称</summary>
public String Name { get; set; }
/// <summary>数值</summary>
public Object Value { get; set; }
/// <summary>类型</summary>
public Type Type { get; set; }
/// <summary>字段长度</summary>
public Int32 Length { get;set; }
/// <summary>格式化字符串</summary>
public String Format { get; set; }
/// <summary>元素类型</summary>
public String ItemType { get; set; }
/// <summary>Html特性</summary>
public Object HtmlAttributes { get; set; }
#endregion
#region
/// <summary>实例化</summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="type"></param>
public ItemModel(String name, Object value, Type type)
{
Name = name;
Value = value;
Type = type;
}
/// <summary>实例化</summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="type"></param>
/// <param name="format"></param>
/// <param name="htmlAttributes"></param>
public ItemModel(String name, Object value, Type type, String format, Object htmlAttributes)
{
Name = name;
Value = value;
Type = type;
Format = format;
HtmlAttributes = htmlAttributes;
}
#endregion
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections;
namespace NewLife.Cube.ViewModels
{
/// <summary>下拉列表模型</summary>
public class ListBoxModel
{
#region
/// <summary>名称</summary>
public String Name { get; set; }
/// <summary>数据</summary>
public IEnumerable Value { get; set; }
/// <summary>已选值</summary>
public Object SelectedValues { get; set; }
/// <summary>标签</summary>
public String OptionLabel { get; set; }
/// <summary>自动提交</summary>
public Boolean AutoPostback { get; set; }
/// <summary>Html特性</summary>
public Object HtmlAttributes { get; set; }
#endregion
#region
/// <summary>实例化</summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="selectedValues"></param>
public ListBoxModel(String name, IEnumerable value, Object selectedValues)
{
Name = name;
Value = value;
SelectedValues = selectedValues;
}
/// <summary>实例化</summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="selectedValues"></param>
/// <param name="optionLabel"></param>
/// <param name="autoPostback"></param>
/// <param name="htmlAttributes"></param>
public ListBoxModel(String name, IEnumerable value, Object selectedValues, String optionLabel, Boolean autoPostback, Object htmlAttributes)
{
Name = name;
Value = value;
SelectedValues = selectedValues;
OptionLabel = optionLabel;
AutoPostback = autoPostback;
HtmlAttributes = htmlAttributes;
}
#endregion
}
}

View File

@ -0,0 +1,119 @@
using System.Text.RegularExpressions;
using NewLife.Data;
using XCode.Configuration;
namespace NewLife.Cube.ViewModels;
/// <summary>获取数据委托</summary>
/// <param name="entity"></param>
/// <returns></returns>
public delegate String GetValueDelegate(Object entity);
/// <summary>列表字段</summary>
public class ListField : DataField
{
#region
/// <summary>单元格文字</summary>
public String Text { get; set; }
/// <summary>单元格标题。数据单元格上的提示文字</summary>
public String Title { get; set; }
/// <summary>单元格链接。数据单元格的链接</summary>
public String Url { get; set; }
/// <summary>单元格图标。数据单元格前端显示时的图标或图片</summary>
public String Icon { get; set; }
/// <summary>链接目标。_blank/_self/_parent/_top</summary>
public String Target { get; set; }
/// <summary>头部文字</summary>
public String Header { get; set; }
/// <summary>头部标题。数据移上去后显示的文字</summary>
public String HeaderTitle { get; set; }
///// <summary>头部链接。一般是排序</summary>
//public String HeaderUrl { get; set; }
/// <summary>数据动作。设为action时走ajax请求</summary>
public String DataAction { get; set; }
/// <summary>获取数据委托。可用于自定义列表页单元格数值的显示</summary>
public GetValueDelegate GetValue { get; set; }
#endregion
#region
/// <summary>填充</summary>
/// <param name="field"></param>
public override void Fill(FieldItem field)
{
base.Fill(field);
Header = field.DisplayName;
}
#endregion
#region
private static readonly Regex _reg = new(@"{(\w+)}", RegexOptions.Compiled);
private static String Replace(String input, IExtend data) => _reg.Replace(input, m => data[m.Groups[1].Value + ""] + "");
/// <summary>针对指定实体对象计算DisplayName替换其中变量</summary>
/// <param name="data"></param>
/// <returns></returns>
public virtual String GetDisplayName(IExtend data)
{
if (DisplayName.IsNullOrEmpty()) return null;
return Replace(DisplayName, data);
}
/// <summary>针对指定实体对象计算链接名,替换其中变量</summary>
/// <param name="data"></param>
/// <returns></returns>
public virtual String GetLinkName(IExtend data)
{
// 如果设置了单元格文字则优先使用。Text>Entity[name]>DisplayName
var txt = Text;
if (txt.IsNullOrEmpty())
{
// 在数据列中,实体对象取属性值优先于显示名
if (Field != null && DisplayName == Field.DisplayName) return data[Name] as String;
txt = DisplayName;
}
if (txt.IsNullOrEmpty()) return null;
//return _reg.Replace(txt, m => data[m.Groups[1].Value + ""] + "");
return Replace(txt, data);
}
/// <summary>针对指定实体对象计算url替换其中变量</summary>
/// <param name="data"></param>
/// <returns></returns>
public virtual String GetUrl(IExtend data)
{
var svc = GetService<IUrlExtend>();
if (svc != null) return svc.Resolve(this, data);
if (Url.IsNullOrEmpty()) return null;
//return _reg.Replace(Url, m => data[m.Groups[1].Value + ""] + "");
return Replace(Url, data);
}
/// <summary>针对指定实体对象计算title替换其中变量</summary>
/// <param name="data"></param>
/// <returns></returns>
public virtual String GetTitle(IExtend data)
{
if (Title.IsNullOrEmpty()) return null;
//return _reg.Replace(Title, m => data[m.Groups[1].Value + ""] + "");
return Replace(Title, data);
}
#endregion
}

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
namespace NewLife.Cube.ViewModels
{
/// <summary>
/// 菜单树
/// </summary>
public class MenuTree
{
/// <summary>
///
/// </summary>
/// <value></value>
public Int32 ID { get; set; }
/// <summary>
/// 名称
/// </summary>
/// <value></value>
public String Name { get; set; }
/// <summary>
/// 显示名
/// </summary>
public String DisplayName { get; set; }
/// <summary>
/// 父级id
/// </summary>
public Int32? ParentID { get; set; }
/// <summary>
/// 链接
/// </summary>
/// <value></value>
public String Url { get; set; }
/// <summary>
/// 图标
/// </summary>
/// <value></value>
public String Icon { get; set; }
/// <summary>
/// 是否可见
/// </summary>
/// <value></value>
public Boolean Visible { get; set; }
/// <summary>是否新窗口打开</summary>
public Boolean NewWindow { get; set; }
/// <summary>可选权限子项</summary>
public Dictionary<Int32, String> Permissions { get; set; }
/// <summary>
/// 子菜单
/// </summary>
/// <value></value>
public IList<MenuTree> Children { get => GetChildren?.Invoke(this) ?? null; set { } }
/// <summary>
/// 获取子菜单的方法
/// </summary>
private static Func<MenuTree, IList<MenuTree>> GetChildren;
/// <summary>
/// 获取菜单树
/// </summary>
/// <param name="getChildrenSrc">自定义的获取子菜单需要数据的方法</param>
/// <param name="getMenuList">获取菜单列表的方法</param>
/// <param name="src">获取菜单列表的初始数据来源</param>
/// <returns></returns>
public static IList<MenuTree> GetMenuTree<T>(Func<MenuTree, T> getChildrenSrc,
Func<T, IList<MenuTree>> getMenuList, T src) where T : class
{
GetChildren = m => getMenuList?.Invoke(getChildrenSrc(m));
return getMenuList?.Invoke(src);
}
/// <summary>
/// 已重载。
/// </summary>
/// <returns></returns>
public override String ToString() => Name;
}
}

View File

@ -0,0 +1,374 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
namespace NewLife.Cube.Web;
/// <summary>
/// 浏览器特性分析器
/// </summary>
public class UserAgentParser
{
#region
/// <summary>原始字符串</summary>
public String UserAgent { get; set; }
/// <summary>
/// 兼容性,一般是 Mozilla/5.0
/// </summary>
public String Compatible { get; set; }
/// <summary>平台</summary>
public String Platform { get; set; }
/// <summary>加密特性</summary>
public String Encryption { get; set; }
/// <summary>系统或处理器</summary>
public String OSorCPU { get; set; }
/// <summary>设备</summary>
public String Device { get; set; }
/// <summary>设备编译版本</summary>
public String DeviceBuild { get; set; }
/// <summary>发行版本</summary>
public String Version { get; set; }
/// <summary>用户浏览器</summary>
public String Brower { get; set; }
/// <summary>移动版本</summary>
public String Mobile { get; set; }
/// <summary>网络类型</summary>
public String NetType { get; set; }
#endregion
#region
private static readonly Regex _regex = new(@"([^\s\(\)]+)\s*(\([^\(\)]+\))?");
/// <summary>
/// 分析浏览器UserAgent字符串
/// </summary>
/// <param name="userAgent"></param>
/// <returns></returns>
public Boolean Parse(String userAgent)
{
if (userAgent.IsNullOrEmpty()) return false;
UserAgent = userAgent;
var ms = _regex.Matches(userAgent);
var count = ms.Count;
if (count == 0) return false;
var infos = ms.Select(e => e.Value?.Trim()).ToArray();
var exts = ms[0].Groups[2].Value?.Trim('(', ')').Split(';');
// 首先识别主流浏览器,不同浏览器格式不同
ParseFirefox(infos, exts);
ParseOpera(infos, exts);
ParseHuawei(infos, exts);
ParseTencent(infos, exts);
ParseAliApp(infos, exts);
// 其它浏览器
if (Brower.IsNullOrEmpty()) ParseOtherBrowser(infos);
if (Brower.IsNullOrEmpty()) ParseChrome(infos);
if (Brower.IsNullOrEmpty()) ParseSafari(infos, exts);
// 移动
var inf = infos.FirstOrDefault(e => e.StartsWithIgnoreCase("Mobile/") || e.EqualIgnoreCase("Mobile"));
if (inf != null) Mobile = inf.Trim();
// 网络类型
inf = infos.FirstOrDefault(e => e.StartsWithIgnoreCase("NetType/"));
if (inf != null) NetType = inf.Trim()["NetType/".Length..];
{
// 识别操作系统平台
Compatible = ms[0].Groups[1].Value;
// Mozilla/MozillaVersion (Platform; Encryption; OS-or-CPU; Language; PrereleaseVersi
var ss = exts;
if (ss != null && ss.Length > 0 && Platform.IsNullOrEmpty())
{
if (ss.Length >= 5)
{
Platform = ss[0]?.Trim();
Encryption = ss[1]?.Trim();
OSorCPU = ss[2]?.Trim().TrimStart("CPU ");
//Device = ss[3]?.Trim();
// 有可能是设备和版本
var str = ss[4]?.Trim();
if (!str.IsNullOrEmpty() && str.Contains("Build/"))
Device = str;
else
Version = str;
}
else if (ss.Length >= 4)
{
Platform = ss[0]?.Trim();
if (Platform.EqualIgnoreCase("compatible"))
{
Brower = ss[1]?.Trim();
OSorCPU = ss[2]?.Trim();
}
else
{
Encryption = ss[1]?.Trim();
OSorCPU = ss[2]?.Trim().TrimStart("CPU ");
//// WebKit 特殊
//if (!infos.Any(e => e.Contains("WebKit"))) Device = ss[3]?.Trim();
Device = ss[3]?.Trim();
if (Device == "en" || Device.StartsWithIgnoreCase("en")) Device = null;
}
}
else if (ss.Length >= 3 && ss[0].EqualIgnoreCase("compatible"))
{
Platform = ss[0]?.Trim();
Brower = ss[1]?.Trim();
OSorCPU = ss[2]?.Trim();
}
else if (ss.Length >= 2)
{
Platform = ss[0]?.Trim();
OSorCPU = ss[1]?.Trim().TrimStart("CPU ");
}
else if (ss.Length >= 1)
{
Platform = ss[0]?.Trim();
}
}
// 处理操作系统与平台
if (Platform.StartsWithIgnoreCase("Windows "))
{
OSorCPU = Platform;
Platform = "Windows";
}
else if (Platform.EqualIgnoreCase("Linux") && OSorCPU.StartsWithIgnoreCase("Android ", "HarmonyOS"))
{
Platform = "Android";
}
else if (Platform.EqualIgnoreCase("X11") && Encryption.StartsWithIgnoreCase("Ubuntu"))
{
Platform = Encryption;
Encryption = null;
}
// 处理系统和处理器
if (!OSorCPU.IsNullOrEmpty())
{
var p = OSorCPU.IndexOf("like");
if (p >= 0) OSorCPU = OSorCPU[..p].Trim();
}
// 处理设备
if (!Device.IsNullOrEmpty())
{
var p = Device.IndexOf("Build/");
if (p >= 0)
{
DeviceBuild = Device[(p + "Build/".Length)..].Trim();
Device = Device[..p].Trim();
}
}
}
// 浏览器兜底
if (Brower.IsNullOrEmpty()) Brower = Compatible;
return true;
}
private void ParseChrome(String[] infos)
{
var inf = infos.FirstOrDefault(e => e.StartsWith("Chrome/"));
if (inf == null) return;
Brower = inf;
}
private void ParseFirefox(String[] infos, String[] exts)
{
var inf = infos.FirstOrDefault(e => e.StartsWith("Firefox/"));
if (inf == null) return;
Brower = inf;
if (exts.Length >= 5)
{
Platform = exts[0]?.Trim();
Encryption = exts[1]?.Trim();
OSorCPU = exts[2]?.Trim();
Version = exts[4]?.Trim();
}
else if (exts.Length >= 4)
{
Platform = exts[0]?.Trim();
Encryption = exts[1]?.Trim();
OSorCPU = exts[2]?.Trim();
Version = exts[3]?.Trim();
}
else if (exts.Length >= 3)
{
Platform = exts[0]?.Trim();
//Encryption = exts[1]?.Trim();
Version = exts[2]?.Trim();
}
}
private void ParseOpera(String[] infos, String[] exts)
{
var inf = infos.FirstOrDefault(e => e.StartsWith("Opera/"));
if (inf == null) return;
Brower = inf;
var p = inf.IndexOf(' ');
if (p > 0)
{
Brower = inf[..p];
if (exts.Length >= 3)
{
Platform = exts[0]?.Trim();
Encryption = exts[1]?.Trim();
OSorCPU = exts[2]?.Trim();
}
}
}
private void ParseHuawei(String[] infos, String[] exts)
{
var inf = infos.FirstOrDefault(e => e.StartsWith("HuaweiBrowser/"));
if (inf == null) return;
Brower = inf;
if (exts.Length >= 5)
{
Platform = exts[0]?.Trim();
//Encryption = exts[1]?.Trim();
OSorCPU = exts[2]?.Trim();
Device = exts[3]?.Trim();
Version = exts[4]?.Trim();
}
}
private void ParseTencent(String[] infos, String[] exts)
{
var inf = infos.FirstOrDefault(e => e.StartsWith("wxwork/"));
inf ??= infos.FirstOrDefault(e => e.StartsWith("QQ/"));
inf ??= infos.FirstOrDefault(e => e.StartsWith("MicroMessenger/"));
if (inf == null) return;
var p1 = inf.IndexOf('(');
if (p1 > 0) inf = inf[..p1];
Brower = inf;
if (exts.Length >= 4)
{
Platform = exts[0]?.Trim();
OSorCPU = exts[1]?.Trim();
Device = exts[2]?.Trim();
Version = exts[3]?.Trim();
}
}
private void ParseAliApp(String[] infos, String[] exts)
{
var inf = infos.FirstOrDefault(e => e.StartsWith("AliApp("));
if (inf == null) return;
var p1 = inf.IndexOf('(');
if (p1 < 0) return;
var p2 = inf.IndexOf(')', p1 + 1);
if (p2 < 0) return;
Brower = inf.Substring(p1 + 1, p2 - p1 - 1);
}
private void ParseSafari(String[] infos, String[] exts)
{
var inf = infos.FirstOrDefault(e => e.StartsWith("Safari/"));
if (inf == null) return;
var p1 = inf.IndexOf('(');
if (p1 > 0)
{
var str = inf[p1..];
inf = inf[..p1];
// 识别扩展
var ss = str.Trim('(', ')').Split(';');
if (ss.Length >= 2) Device = ss[1].Trim();
}
Brower = inf.Trim();
// 合并版本
inf = infos.FirstOrDefault(e => e.StartsWithIgnoreCase("Version/"));
if (inf != null)
Brower = Brower.Split('/')[0] + "/" + inf.Split('/')[^1];
}
private void ParseOtherBrowser(String[] infos)
{
var list = infos.Where(e => !e.Contains("(") && !e.StartsWithIgnoreCase("Mozilla/", "AppleWebKit/", "Chrome/", "Safari/", "Gecko/", "Mobile/", "Version/") && !e.EqualIgnoreCase("Mobile")).ToList();
if (list.Count == 0) return;
Brower = list[0];
}
#endregion
#region
private static String[] _robots = new[] { "bot", "crawler", "spider", "okhttp", "python" };
/// <summary>是否蜘蛛机器人</summary>
public Boolean IsRobot
{
get
{
//if (UserAgent.StartsWithIgnoreCase("okhttp")) return true;
var str = Brower;
if (!str.IsNullOrEmpty())
{
var p = str.IndexOf('/');
if (p > 0) str = str[..p];
if (str.EndsWithIgnoreCase(_robots)) return true;
}
str = OSorCPU;
if (!str.IsNullOrEmpty())
{
var p = str.LastIndexOf('/');
if (p > 0) str = str[(p + 1)..];
if (str.EndsWithIgnoreCase(_robots)) return true;
}
str = Device;
if (!str.IsNullOrEmpty())
{
var p = str.LastIndexOf('/');
if (p > 0) str = str[(p + 1)..];
if (str.EndsWithIgnoreCase(_robots)) return true;
}
return false;
}
}
/// <summary>是否移动端</summary>
public Boolean IsMobile => !Mobile.IsNullOrEmpty();
#endregion
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>设置控制器</summary>
[DisplayName("基本设置")]
[Area("Admin")]
[Menu(0, false, Icon = "fa-bomb")]
public class CoreController : ConfigController<NewLife.Setting>
{
}
}

View File

@ -0,0 +1,65 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using NewLife.Cube.Services;
using NewLife.Cube.ViewModels;
namespace NewLife.Cube.Admin.Controllers;
/// <summary>系统设置控制器</summary>
[DisplayName("魔方设置")]
[Area("Admin")]
[Menu(30, true, Icon = "fa-wrench")]
public class CubeController : ConfigController<Setting>
{
private Boolean _has;
private readonly UIService _uIService;
/// <summary>实例化</summary>
/// <param name="uIService"></param>
public CubeController(UIService uIService) => _uIService = uIService;
/// <summary>执行前</summary>
/// <param name="filterContext"></param>
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!_has)
{
var list = GetMembers(typeof(Setting));
var df = list.FirstOrDefault(e => e.Name == "Theme");
if (df != null)
{
df.Description = $"可选主题 {_uIService.Themes.Join("/")}";
df.DataSource = e => _uIService.Themes.ToDictionary(e => e, e => e);
}
df = list.FirstOrDefault(e => e.Name == "Skin");
if (df != null)
{
df.Description = $"可选皮肤 {_uIService.Skins.Join("/")}";
df.DataSource = e => _uIService.Skins.ToDictionary(e => e, e => e);
}
df = list.FirstOrDefault(e => e.Name == "EChartsTheme");
if (df != null)
{
var themes = _uIService.GetEChartsThemes();
df.Description = $"可选主题 {themes.Join("/")}";
themes.Insert(0, "default");
df.DataSource = e => themes.ToDictionary(e => e, e => e);
}
_has = true;
}
base.OnActionExecuting(filterContext);
}
/// <summary>
/// 获取登录设置
/// </summary>
/// <returns></returns>
[AllowAnonymous]
public ActionResult GetLoginConfig() => Ok(data: new LoginConfigModel());
}

View File

@ -0,0 +1,110 @@
using System.ComponentModel;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using NewLife.Reflection;
using XCode;
using XCode.DataAccessLayer;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers;
/// <summary>数据库管理</summary>
[DisplayName("数据库")]
[EntityAuthorize(PermissionFlags.Detail)]
[Area("Admin")]
[Menu(26, true, Icon = "fa-database")]
public class DbController : ControllerBaseX
{
/// <summary>数据库列表</summary>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Detail)]
public ActionResult Index()
{
var list = new List<DbItem>();
var dir = NewLife.Setting.Current.BackupPath.GetBasePath().AsDirectory();
// 读取配置文件
foreach (var item in DAL.ConnStrs.ToArray())
{
var di = new DbItem
{
Name = item.Key,
ConnStr = item.Value
};
var dal = DAL.Create(item.Key);
di.Type = dal.DbType;
var t = Task.Run(() =>
{
try
{
return dal.Db.ServerVersion;
}
catch { return null; }
});
if (t.Wait(300)) di.Version = t.Result;
if (dir.Exists) di.Backups = dir.GetFiles($"{dal.ConnName}_*", SearchOption.TopDirectoryOnly).Length;
list.Add(di);
}
return View("Index", list);
}
/// <summary>备份数据库</summary>
/// <param name="name"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Insert)]
public ActionResult Backup(String name)
{
var sw = Stopwatch.StartNew();
var dal = DAL.Create(name);
//var bak = dal.Db.CreateMetaData().SetSchema(DDLSchema.BackupDatabase, dal.ConnName, null, false);
var bak = dal.Db.CreateMetaData().Invoke("Backup", dal.ConnName, null, false);
sw.Stop();
WriteLog("备份", true, $"备份数据库 {name} 到 {bak},耗时 {sw.Elapsed}");
return Index();
}
/// <summary>备份并压缩数据库</summary>
/// <param name="name"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Insert)]
public ActionResult BackupAndCompress(String name)
{
var sw = Stopwatch.StartNew();
var dal = DAL.Create(name);
//var bak = dal.Db.CreateMetaData().SetSchema(DDLSchema.BackupDatabase, dal.ConnName, null, true);
//var bak = dal.Db.CreateMetaData().Invoke("Backup", dal.ConnName, null, true);
var bak = $"{name}_{DateTime.Now:yyyyMMddHHmmss}.zip";
bak = NewLife.Setting.Current.BackupPath.CombinePath(bak);
//var tables = dal.Tables;
var tables = EntityFactory.GetTables(name, false);
dal.BackupAll(tables, bak);
sw.Stop();
WriteLog("备份", true, $"备份数据库 {name} 并压缩到 {bak},耗时 {sw.Elapsed}");
return Index();
}
/// <summary>下载数据库备份</summary>
/// <param name="name"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Detail)]
public ActionResult Download(String name)
{
var dal = DAL.Create(name);
var xml = DAL.Export(dal.Tables);
WriteLog("下载", true, "下载数据库架构 " + name);
return File(xml.GetBytes(), "application/xml", name + ".xml");
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Web;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>部门</summary>
[DataPermission(null, "ManagerID={#userId}")]
[DisplayName("部门")]
[Area("Admin")]
[Menu(95, true, Icon = "fa-users")]
public class DepartmentController : EntityController<Department>
{
static DepartmentController()
{
LogOnChange = true;
ListFields.RemoveField("ID", "Ex1", "Ex2", "Ex3", "Ex4", "Ex5", "Ex6");
ListFields.RemoveUpdateField();
ListFields.RemoveCreateField();
ListFields.RemoveRemarkField();
}
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<Department> Search(Pager p)
{
var id = p["id"].ToInt(-1);
if (id > 0)
{
var list = new List<Department>();
var entity = Department.FindByID(id);
if (entity != null) list.Add(entity);
return list;
}
var parentId = p["parentId"].ToInt(-1);
var enable = p["enable"]?.ToBoolean();
var visible = p["visible"]?.ToBoolean();
return Department.Search(parentId, enable, visible, p["Q"], p);
}
}
}

View File

@ -0,0 +1,407 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using XCode.Membership;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using NewLife.Cube.Extensions;
namespace NewLife.Cube.Admin.Controllers;
/// <summary>文件管理</summary>
[DisplayName("文件")]
[EntityAuthorize(PermissionFlags.Detail)]
[Area("Admin")]
[Menu(28, false, Icon = "fa-file")]
public class FileController : ControllerBaseX
{
#region
private String Root => "../".GetCurrentPath();
private FileInfo GetFile(String r)
{
if (r.IsNullOrEmpty()) return null;
// 默认根目录
var fi = Root.CombinePath(r).AsFile();
var root = Root.EnsureEnd(Path.DirectorySeparatorChar + "");
if (!fi.FullName.StartsWithIgnoreCase(root))
{
WriteLog("Valid", false, $"文件[{fi.FullName}]非法越界!");
return null;
}
if (!fi.Exists) return null;
return fi;
}
private DirectoryInfo GetDirectory(String r)
{
if (r.IsNullOrEmpty()) return null;
// 默认根目录
var di = Root.CombinePath(r).AsDirectory();
var root = Root.EnsureEnd(Path.DirectorySeparatorChar + "");
if (!di.FullName.StartsWithIgnoreCase(root))
{
WriteLog("Valid", false, $"目录[{di.FullName}]非法越界!");
return null;
}
if (!di.Exists) return null;
return di;
}
private FileItem GetItem(String r)
{
var inf = GetFile(r) as FileSystemInfo ?? GetDirectory(r);
if (inf == null) return null;
var fi = new FileItem
{
Name = inf.Name,
FullName = GetFullName(inf.FullName),
Raw = inf.FullName,
Directory = inf is DirectoryInfo,
LastWrite = inf.LastWriteTime
};
if (inf is FileInfo)
{
var f = inf as FileInfo;
if (f.Length < 1024)
fi.Size = $"{f.Length:n0}";
else if (f.Length < 1024 * 1024)
fi.Size = $"{(Double)f.Length / 1024:n2}K";
else if (f.Length < 1024 * 1024 * 1024)
fi.Size = $"{(Double)f.Length / 1024 / 1024:n2}M";
else if (f.Length < 1024L * 1024 * 1024 * 1024)
fi.Size = $"{(Double)f.Length / 1024 / 1024 / 1024:n2}G";
}
return fi;
}
private String GetFullName(String r) => r.TrimStart(Root).TrimStart(Root.TrimEnd(Path.DirectorySeparatorChar + ""));
#endregion
#region &
/// <summary>文件管理主视图</summary>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Detail)]
public ActionResult Index(String r, String sort, String message = "")
{
var di = GetDirectory(r) ?? Root.AsDirectory();
// 计算当前路径
var fd = di.FullName;
if (fd.StartsWith(Root)) fd = fd[Root.Length..];
ViewBag.Current = fd;
// 遍历所有子目录
var fis = di.GetFileSystemInfos();
var list = new List<FileItem>();
foreach (var item in fis)
{
if (item.Attributes.Has(FileAttributes.Hidden)) continue;
var fi = GetItem(item.FullName);
list.Add(fi);
}
// 排序,目录优先
list = sort switch
{
"size" => list.OrderByDescending(e => e.Size).ThenBy(e => e.Name).ToList(),
"lastwrite" => list.OrderByDescending(e => e.LastWrite).ThenBy(e => e.Name).ToList(),
_ => list.OrderByDescending(e => e.Directory).ThenBy(e => e.Name).ToList(),
};
// 在开头插入上一级目录
var root = Root.TrimEnd(Path.DirectorySeparatorChar);
if (!di.FullName.EqualIgnoreCase(Root, root))
{
if (di.Parent != null)
{
list.Insert(0, new FileItem
{
Name = "../",
Directory = true,
FullName = GetFullName(di.Parent.FullName)
});
}
}
// 剪切板
ViewBag.Clip = GetClip();
// 提示信息
ViewBag.Message = message;
return View("Index", list);
}
/// <summary>删除</summary>
/// <param name="r"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Delete)]
public ActionResult Delete(String r)
{
var p = "";
var fi = GetFile(r);
if (fi != null)
{
p = GetFullName(fi.Directory.FullName);
WriteLog("删除", true, fi.FullName);
fi.Delete();
}
else
{
var di = GetDirectory(r);
if (di == null) throw new Exception("找不到文件或目录!");
p = GetFullName(di.Parent.FullName);
WriteLog("删除", true, di.FullName);
di.Delete(true);
}
return RedirectToAction("Index", new { r = p });
}
#endregion
#region
/// <summary>压缩文件</summary>
/// <param name="r"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Insert)]
public ActionResult Compress(String r)
{
var p = "";
var fi = GetFile(r);
if (fi != null)
{
p = GetFullName(fi.Directory.FullName);
var dst = $"{fi.Name}_{DateTime.Now:yyyyMMddHHmmss}.zip";
dst = fi.Directory.FullName.CombinePath(dst);
WriteLog("压缩", true, $"{fi.FullName} => {dst}");
fi.Compress(dst);
}
else
{
var di = GetDirectory(r);
if (di == null) throw new Exception("找不到文件或目录!");
p = GetFullName(di.Parent.FullName);
var dst = $"{di.Name}_{DateTime.Now:yyyyMMddHHmmss}.zip";
dst = di.Parent.FullName.CombinePath(dst);
WriteLog("压缩", true, $"{di.FullName} => {dst}");
di.Compress(dst);
}
return RedirectToAction("Index", new { r = p });
}
/// <summary>解压缩</summary>
/// <param name="r"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Update)]
public ActionResult Decompress(String r)
{
var fi = GetFile(r);
if (fi == null) throw new Exception("找不到文件!");
var p = GetFullName(fi.Directory.FullName);
WriteLog("解压缩", true, fi.FullName);
fi.Extract(fi.Directory.FullName, true);
return RedirectToAction("Index", new { r = p });
}
#endregion
#region
/// <summary>上传文件</summary>
/// <param name="r"></param>
/// <param name="file"></param>
/// <returns></returns>
[HttpPost]
[EntityAuthorize(PermissionFlags.Insert)]
public async Task<ActionResult> Upload(String r, IFormFile file)
{
if (file != null)
{
var di = GetDirectory(r) ?? Root.AsDirectory();
if (di == null) throw new Exception("找不到目录!");
var dest = di.FullName.CombinePath(file.FileName);
WriteLog("上传", true, dest);
dest.EnsureDirectory(true);
//System.IO.File.WriteAllBytes(dest, file.OpenReadStream().ReadBytes(-1));
using var fs = new FileStream(dest, FileMode.OpenOrCreate, FileAccess.ReadWrite);
await file.CopyToAsync(fs);
}
return RedirectToAction("Index", new { r });
}
/// <summary>上传文件</summary>
/// <param name="r"></param>
/// <param name="file"></param>
/// <returns></returns>
[HttpPost]
[EntityAuthorize(PermissionFlags.Insert)]
public async Task<ActionResult> UploadLayui(String r, IFormFile file)
{
try
{
if (file != null)
{
var di = GetDirectory(r) ?? Root.AsDirectory();
if (di == null) throw new Exception("找不到目录!");
var dest = di.FullName.CombinePath(file.FileName);
WriteLog("上传", true, dest);
dest.EnsureDirectory(true);
//System.IO.File.WriteAllBytes(dest, file.OpenReadStream().ReadBytes(-1));
using var fs = new FileStream(dest, FileMode.OpenOrCreate, FileAccess.ReadWrite);
await file.CopyToAsync(fs);
}
return Json(new { code = 0, message = "上传成功" });
}
catch (Exception ex)
{
WriteLog("上传失败", false, ex + "");
return Json(new { code = 500, message = "上传失败" });
}
}
/// <summary>下载文件</summary>
/// <param name="r"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Detail)]
public ActionResult Download(String r)
{
var fi = GetFile(r);
if (fi == null) throw new Exception("找不到文件!");
WriteLog("下载", true, fi.FullName);
return PhysicalFile(fi.FullName, "application/octet-stream", fi.Name, true);
}
#endregion
#region
private const String CLIPKEY = "File_Clipboard";
private List<FileItem> GetClip()
{
var list = Session[CLIPKEY] as List<FileItem>;
if (list == null) Session[CLIPKEY] = list = new List<FileItem>();
return list;
}
/// <summary>复制文件到剪切板</summary>
/// <param name="r"></param>
/// <param name="f"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Detail)]
public ActionResult Copy(String r, String f)
{
var fi = GetItem(f);
if (fi == null) throw new Exception("找不到文件或目录!");
var list = GetClip();
if (!list.Any(e => e.Raw == fi.Raw)) list.Add(fi);
//return RedirectToAction("Index", new { r });
return Index(r, null);
}
/// <summary>从剪切板移除</summary>
/// <param name="r"></param>
/// <param name="f"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Detail)]
public ActionResult CancelCopy(String r, String f)
{
var fi = GetItem(f);
if (fi == null) throw new Exception("找不到文件或目录!");
var list = GetClip();
list.RemoveAll(e => e.Raw == fi.Raw);
//return RedirectToAction("Index", new { r });
return Index(r, null);
}
/// <summary>粘贴文件到当前目录</summary>
/// <param name="r"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Insert)]
public ActionResult Paste(String r)
{
var di = GetDirectory(r) ?? Root.AsDirectory();
if (di == null) throw new Exception("找不到目录!");
var list = GetClip();
foreach (var item in list)
{
var dst = di.FullName.CombinePath(item.Name);
WriteLog("复制", true, $"{item.Raw} => {dst}");
if (item.Directory)
item.Raw.AsDirectory().CopyTo(dst);
else
System.IO.File.Copy(item.Raw, dst, true);
}
list.Clear();
return Index(r, null);
}
/// <summary>移动文件到当前目录</summary>
/// <param name="r"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Insert)]
public ActionResult Move(String r)
{
var di = GetDirectory(r) ?? Root.AsDirectory();
if (di == null) throw new Exception("找不到目录!");
var list = GetClip();
foreach (var item in list)
{
var dst = di.FullName.CombinePath(item.Name);
WriteLog("移动", true, $"{item.Raw} => {dst}");
if (item.Directory)
Directory.Move(item.Raw, dst);
else
System.IO.File.Move(item.Raw, dst);
}
list.Clear();
return Index(r, null);
}
/// <summary>清空剪切板</summary>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Detail)]
public ActionResult ClearClipboard(String r)
{
var list = GetClip();
list.Clear();
return Index(r, null);
}
#endregion
}

View File

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.ViewModels;
using NewLife.Web;
using XCode;
using XCode.Membership;
using static XCode.Membership.Log;
using XLog = XCode.Membership.Log;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>审计日志控制器</summary>
[DataPermission(null, "CreateUserID={#userId}")]
[DisplayName("审计日志")]
[Description("系统内重要操作均记录日志,便于审计。任何人都不能删除、修改或伪造操作日志。")]
[Area("Admin")]
[Menu(70, true, Icon = "fa-history")]
public class LogController : ReadOnlyEntityController<XLog>
{
static LogController()
{
// 日志列表需要显示详细信息,不需要显示用户编号
ListFields.AddDataField("Remark", null, "Action");
ListFields.RemoveField("CreateUserID");
//FormFields.RemoveField("Remark");
//{
// var df = ListFields.GetField("TraceId") as ListField;
// df.DisplayName = "跟踪";
// df.Url = StarHelper.BuildUrl("{TraceId}");
// df.DataVisible = (e, f) => !(e as XLog).TraceId.IsNullOrEmpty();
//}
{
// 今天的时间不显示日期
var df = ListFields.GetField("CreateTime") as ListField;
df.GetValue = e => (e as XLog).CreateTime.ToFullString("").TrimStart(DateTime.Today.ToString("yyyy-MM-dd "));
}
}
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<XLog> Search(Pager p)
{
var category = p["category"];
var action = p["act"];
var success = p["success"]?.ToBoolean();
var linkid = p["linkid"].ToInt(-1);
var userid = p["userid"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
var key = p["Q"];
// 默认排序
if (p.Sort.IsNullOrEmpty()) p.OrderBy = _.ID.Desc();
// 附近日志
if (key.IsNullOrEmpty() && userid < 0 && category.IsNullOrEmpty() && start.Year < 2000 && end.Year < 2000)
{
var id = p["id"].ToLong();
var act = p["act"];
if (act == "near" && id > 0)
{
var range = p["range"].ToInt();
if (range <= 0) range = 10;
// 雪花Id抽取时间
var snow = XLog.Meta.Factory.Snow;
if (snow.TryParse(id, out var time, out var _, out var _))
{
start = time.AddSeconds(-range);
end = time.AddSeconds(range);
return XLog.FindAll(_.ID.Between(start, end, snow), p);
}
}
}
return XLog.Search(category, action, linkid, success, userid, start, end, key, p);
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>菜单控制器</summary>
[DisplayName("菜单")]
[Description("系统操作菜单以及功能目录树。支持排序,不可见菜单仅用于功能权限限制。每个菜单的权限子项由系统自动生成,请不要人为修改")]
[Area("Admin")]
[Menu(80, true, Icon = "fa-navicon")]
public class MenuController : EntityTreeController<Menu>
{
static MenuController()
{
// 过滤要显示的字段
ListFields.RemoveField("Remark");
}
/// <summary>验证实体对象</summary>
/// <param name="entity"></param>
/// <param name="type"></param>
/// <param name="post"></param>
/// <returns></returns>
protected override Boolean Valid(Menu entity, DataObjectMethodType type, Boolean post)
{
var rs = base.Valid(entity, type, post);
// 清空缓存
if (post) XCode.Membership.Menu.Meta.Session.ClearCache($"{type}-{entity}", true);
return rs;
}
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>OAuth配置</summary>
[Area("Admin")]
[Menu(0, false)]
public class OAuthConfigController : EntityController<OAuthConfig>
{
static OAuthConfigController()
{
LogOnChange = true;
ListFields.RemoveField("Secret", "Logo", "AuthUrl", "AccessUrl", "UserUrl", "Remark");
}
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Cube.Extensions;
using NewLife.Cube.ViewModels;
using NewLife.Web;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>OAuth日志</summary>
[DataPermission(null, "UserId={#userId}")]
[DisplayName("OAuth日志")]
[Area("Admin")]
[Menu(0, false)]
public class OAuthLogController : ReadOnlyEntityController<OAuthLog>
{
static OAuthLogController()
{
ListFields.RemoveField("Id")
.RemoveUpdateField();
ListFields.TraceUrl("TraceId");
//{
// var df = ListFields.GetField("TraceId") as ListField;
// df.DisplayName = "跟踪";
// df.Url = StarHelper.BuildUrl("{TraceId}");
// df.DataVisible = (e, f) => !(e as OAuthLog).TraceId.IsNullOrEmpty();
//}
}
/// <summary>搜索</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<OAuthLog> Search(Pager p)
{
var provider = p["provider"];
var connectId = p["connectId"].ToInt(-1);
var userId = p["userId"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
var key = p["Q"];
if (p.Sort.IsNullOrEmpty()) p.Sort = OAuthLog._.Id.Desc();
return OAuthLog.Search(provider, connectId, userId, start, end, key, p);
}
}
}

View File

@ -0,0 +1,21 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>字典参数</summary>
[DisplayName("字典参数")]
[Area("Admin")]
[Menu(30, false, Icon = "fa-wrench")]
public class ParameterController : EntityController<Parameter>
{
static ParameterController()
{
LogOnChange = true;
ListFields.RemoveField("ID", "Ex1", "Ex2", "Ex3", "Ex4", "Ex5", "Ex6", "UpdateUserID", "UpdateIP");
ListFields.RemoveCreateField();
}
}
}

View File

@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Web;
using XCode.Membership;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Threading.Tasks;
using XCode;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>角色控制器</summary>
[DisplayName("角色")]
[Description("系统基于角色授权,每个角色对不同的功能模块具备添删改查以及自定义权限等多种权限设定。")]
[Area("Admin")]
[Menu(90, true, Icon = "fa-user-plus")]
public class RoleController : EntityController<Role>
{
static RoleController()
{
ListFields.RemoveField("ID", "Ex1", "Ex2", "Ex3", "Ex4", "Ex5", "Ex6", "UpdateUserID", "UpdateIP", "Remark");
ListFields.RemoveCreateField();
{
var df = ListFields.AddListField("Remark", "UpdateUser");
}
}
/// <summary>动作执行前</summary>
/// <param name="filterContext"></param>
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
ViewBag.HeaderTitle = "角色管理";
//ViewBag.HeaderContent = "系统基于角色授权,每个角色对不同的功能模块具备添删改查以及自定义权限等多种权限设定。";
var bs = this.Bootstrap();
bs.MaxColumn = 1;
base.OnActionExecuting(filterContext);
}
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<Role> Search(Pager p)
{
var id = p["id"].ToInt(-1);
if (id > 0)
{
var list = new List<Role>();
var entity = Role.FindByID(id);
if (entity != null) list.Add(entity);
return list;
}
return Role.Search(p["dtStart"].ToDateTime(), p["dtEnd"].ToDateTime(), p["Q"], p);
}
/// <summary>
/// 添加权限授权
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public override async Task<ActionResult> Add(Role entity)
{
// 检测避免乱用Add/id
if (Factory.Unique.IsIdentity && entity[Factory.Unique.Name].ToInt() != 0) throw new Exception("我们约定添加数据时路由id部分默认没有数据以免模型绑定器错误识别");
if (!Valid(entity, DataObjectMethodType.Insert, true))
{
ViewBag.StatusMessage = "验证失败!";
ViewBag.Fields = AddFormFields;
return View("AddForm", entity);
}
var rs = false;
var err = "";
try
{
//SaveFiles(entity);
entity.CreateTime = DateTime.Now;
entity.CreateIP = GetHostAddresses();
entity.Enable = true;
// 保存权限项
var menus = XCode.Membership.Menu.Root.AllChilds;
var dels = new List<Int32>();
// 遍历所有权限资源
foreach (var item in menus)
{
// 是否授权该项
var has = GetBool("p" + item.ID);
if (!has)
dels.Add(item.ID);
else
{
// 遍历所有权限子项
var any = false;
foreach (var pf in item.Permissions)
{
var has2 = GetBool("pf" + item.ID + "_" + pf.Key);
if (has2)
entity.Set(item.ID, (PermissionFlags)pf.Key);
else
entity.Reset(item.ID, (PermissionFlags)pf.Key);
any |= has2;
}
// 如果原来没有权限,这是首次授权,且右边没有勾选任何子项,则授权全部
if (!any & !entity.Has(item.ID)) entity.Set(item.ID);
}
}
// 删除已经被放弃权限的项
foreach (var item in dels)
{
if (entity.Has(item)) entity.Permissions.Remove(item);
}
OnInsert(entity);
var fs =await SaveFiles(entity);
if (fs.Count > 0) OnUpdate(entity);
if (LogOnChange) LogProvider.Provider.WriteLog("Insert", entity);
rs = true;
//var masterCode = Entity<TEntity>.Meta.Factory.AllFields.Find(x => {
// return x.Field.ColumnName.Equals("QrCode");
//});
//if (masterCode != null)
//{
// //更新二维码
// entity.SetValue("QrCode", entity["QrCode"].ToString() + "?data={\"name\":\""+ entity["Name"]+ "\",\"code\":\""+entity["Code"]+"\"}";
// entity.Update();
//}
}
catch (ArgumentException aex)
{
err = aex.Message;
ModelState.AddModelError(aex.ParamName, aex.Message);
}
catch (Exception ex)
{
err = ex.Message;
ModelState.AddModelError("", ex.Message);
}
if (!rs)
{
WriteLog("Add", false, err);
ViewBag.StatusMessage = SysConfig.Develop ? ("添加失败!" + err) : "添加失败!";
// 添加失败ID清零否则会显示保存按钮
entity[Role.Meta.Unique.Name] = 0;
if (IsJsonRequest) return Json(500, ViewBag.StatusMessage);
ViewBag.Fields = AddFormFields;
return View("AddForm", entity);
}
ViewBag.StatusMessage = "添加成功!";
//添加明细
rs = AddDetailed(entity);
if (!rs)
{
WriteLog("Edit", false, err);
ViewBag.StatusMessage = SysConfig.Develop ? ("添加明细失败!" + err) : "添加明细失败!";
// 添加失败ID清零否则会显示保存按钮
entity[Role.Meta.Unique.Name] = 0;
if (IsJsonRequest) return Json(500, ViewBag.StatusMessage);
ViewBag.Fields = AddFormFields;
return View("AddForm", entity);
}
if (IsJsonRequest) return Json(0, ViewBag.StatusMessage);
var url = Session["Cube_Add_Referrer"] as String;
if (!url.IsNullOrEmpty())
return Redirect(url);
else
// 新增完成跳到列表页,更新完成保持本页
return RedirectToAction("Index");
}
/// <summary>保存</summary>
/// <param name="entity"></param>
/// <returns></returns>
public override async Task<ActionResult> Edit(Role entity)
{
// 保存权限项
var menus = XCode.Membership.Menu.Root.AllChilds;
//var pfs = EnumHelper.GetDescriptions<PermissionFlags>().Where(e => e.Key > PermissionFlags.None);
var dels = new List<Int32>();
// 遍历所有权限资源
foreach (var item in menus)
{
// 是否授权该项
var has = GetBool("p" + item.ID);
if (!has)
dels.Add(item.ID);
else
{
// 遍历所有权限子项
var any = false;
foreach (var pf in item.Permissions)
{
var has2 = GetBool("pf" + item.ID + "_" + pf.Key);
if (has2)
entity.Set(item.ID, (PermissionFlags)pf.Key);
else
entity.Reset(item.ID, (PermissionFlags)pf.Key);
any |= has2;
}
// 如果原来没有权限,这是首次授权,且右边没有勾选任何子项,则授权全部
if (!any & !entity.Has(item.ID)) entity.Set(item.ID);
}
}
// 删除已经被放弃权限的项
foreach (var item in dels)
{
if (entity.Has(item)) entity.Permissions.Remove(item);
}
return await base.Edit(entity);
}
/// <summary>
/// 获取客户端IP地址
/// </summary>
/// <returns></returns>
protected virtual String GetHostAddresses()
{
return HttpContext.GetUserHost();
}
/// <summary>添加实体主表对应的从表记录</summary>
/// <param name="entity"></param>
/// <returns></returns>
protected virtual bool AddDetailed(IEntity entity)
{
if (entity == null)
{
return false;
}
// TO DO
return true;
}
/// <summary>验证实体对象</summary>
/// <param name="entity"></param>
/// <param name="type"></param>
/// <param name="post"></param>
/// <returns></returns>
protected override Boolean Valid(Role entity, DataObjectMethodType type, Boolean post)
{
var rs = base.Valid(entity, type, post);
// 清空缓存
if (post) Role.Meta.Session.ClearCache($"{type}-{entity}", true);
return rs;
}
private Boolean GetBool(String name)
{
var v = GetRequest(name);
if (v.IsNullOrEmpty()) return false;
v = v.Split(",")[0];
if (!v.EqualIgnoreCase("true", "false")) throw new XException("非法布尔值Request[{0}]={1}", name, v);
return v.ToBoolean();
}
}
}

View File

@ -0,0 +1,15 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Common;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>系统设置控制器</summary>
[DisplayName("系统设置")]
[Area("Admin")]
[Menu(0, false)]
public class SysController : ConfigController<SysConfig>
{
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Cube.ViewModels;
using NewLife.Web;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>用户链接控制器</summary>
[DataPermission(null, "UserID={#userId}")]
[DisplayName("用户链接")]
[Description("第三方登录信息")]
[Area("Admin")]
[Menu(0, false)]
public class UserConnectController : EntityController<UserConnect>
{
static UserConnectController()
{
ListFields.RemoveField("AccessToken", "RefreshToken", "Avatar", "UpdateUserID");
ListFields.RemoveCreateField();
// 提供者列,增加查询
{
var df = ListFields.GetField("Provider") as ListField;
//df.DisplayName = "{Provider}";
df.Url = "/Admin/UserConnect?provider={Provider}";
}
// 用户列,增加连接
{
var df = ListFields.GetField("UserName") as ListField;
df.Header = "用户";
df.HeaderTitle = "对应的本地用户信息";
//df.DisplayName = "{UserName}";
df.Url = "/Admin/User?id={UserID}";
}
{
var df = ListFields.AddListField("OAuthLog", "Enable");
//df.Header = "OAuth日志";
df.DisplayName = "OAuth日志";
df.Url = "/Admin/OAuthLog?connectId={ID}";
}
//// 插入一列
//{
// var df = ListFields.AddDataField("用户信息", "CreateUserID");
// df.DisplayName = "用户信息";
// df.Url = "User?id={UserID}";
//}
}
/// <summary>构造</summary>
public UserConnectController() => PageSetting.EnableAdd = false;
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<UserConnect> Search(Pager p)
{
var key = p["Q"];
var userid = p["userid"].ToInt();
var provider = p["provider"];
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
return UserConnect.Search(provider, userid, start, end, key, p);
}
}
}

View File

@ -0,0 +1,732 @@
using System.ComponentModel;
using System.Web;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NewLife.Caching;
using NewLife.Common;
using NewLife.Cube.Areas.Admin.Models;
using NewLife.Cube.Entity;
using NewLife.Cube.Services;
using NewLife.Log;
using NewLife.Reflection;
using NewLife.Web;
using XCode;
using XCode.Membership;
using static XCode.Membership.User;
namespace NewLife.Cube.Admin.Controllers;
/// <summary>用户控制器</summary>
[DataPermission(null, "ID={#userId}")]
[DisplayName("用户")]
[Description("系统基于角色授权,每个角色对不同的功能模块具备添删改查以及自定义权限等多种权限设定。")]
[Area("Admin")]
[Menu(100, true, Icon = "fa-user")]
public class UserController : EntityController<User>
{
/// <summary>用于防爆破登录。即使内存缓存,也有一定用处,最糟糕就是每分钟重试次数等于集群节点数的倍数</summary>
private static readonly ICache _cache = Cache.Default ?? new MemoryCache();
private readonly PasswordService _passwordService;
private readonly UserService _userService;
static UserController()
{
ListFields.RemoveField("Avatar", "RoleIds", "Online", "Age", "Birthday", "LastLoginIP", "RegisterIP", "RegisterTime");
ListFields.RemoveField("Phone", "Code", "Question", "Answer");
ListFields.RemoveField("Ex1", "Ex2", "Ex3", "Ex4", "Ex5", "Ex6");
ListFields.RemoveUpdateField();
ListFields.RemoveField("Remark");
{
var df = ListFields.AddListField("Link", "Logins");
//df.Header = "链接";
df.HeaderTitle = "第三方登录的链接信息";
df.DisplayName = "链接";
df.Title = "第三方登录的链接信息";
df.Url = "/Admin/UserConnect?userId={ID}";
}
{
var df = ListFields.AddListField("Token", "Logins");
//df.Header = "令牌";
df.DisplayName = "令牌";
df.Url = "/Admin/UserToken?userId={ID}";
}
{
var df = ListFields.AddListField("Log", "Logins");
//df.Header = "日志";
df.DisplayName = "日志";
df.Url = "/Admin/Log?userId={ID}";
}
{
var df = ListFields.AddListField("OAuthLog", "Logins");
//df.Header = "OAuth日志";
df.DisplayName = "OAuth日志";
df.Url = "/Admin/OAuthLog?userId={ID}";
}
{
var df = AddFormFields.AddDataField("RoleIds", "RoleNames");
df.DataSource = entity => Role.FindAllWithCache().OrderByDescending(e => e.Sort).ToDictionary(e => e.ID, e => e.Name);
AddFormFields.RemoveField("RoleNames");
}
//{
// var df = AddFormFields.GetField("RegisterTime");
// df.DataVisible = (e, f) => f.Name != "RegisterTime";
//}
{
var df = EditFormFields.AddDataField("RoleIds", "RoleNames");
df.DataSource = entity => Role.FindAllWithCache().OrderByDescending(e => e.Sort).ToDictionary(e => e.ID, e => e.Name);
EditFormFields.RemoveField("RoleNames");
}
{
AddFormFields.GroupVisible = (entity, group) => (entity as User).ID == 0 && group != "扩展";
}
}
/// <summary>
/// 实例化用户控制器
/// </summary>
/// <param name="passwordService"></param>
/// <param name="userService"></param>
public UserController(PasswordService passwordService, UserService userService)
{
_passwordService = passwordService;
_userService = userService;
}
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<User> Search(Pager p)
{
var id = p["id"].ToInt(-1);
if (id > 0)
{
var list = new List<User>();
var entity = FindByID(id);
entity.Password = null;
if (entity != null) list.Add(entity);
return list;
}
//var roleId = p["roleId"].ToInt(-1);
var roleIds = p["roleIds"].SplitAsInt();
//var departmentId = p["departmentId"].ToInt(-1);
var departmentIds = p["departmentId"].SplitAsInt();
var areaIds = p["areaId"].SplitAsInt("/");
var enable = p["enable"]?.ToBoolean();
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
var key = p["q"];
//p.RetrieveState = true;
//return XCode.Membership.User.Search(roleId, departmentId, enable, start, end, key, p);
//var exp = new WhereExpression();
//if (roleId >= 0) exp &= _.RoleID == roleId | _.RoleIds.Contains("," + roleId + ",");
//if (roleIds != null && roleIds.Length > 0) exp &= _.RoleID.In(roleIds) | _.RoleIds.Contains("," + roleIds.Join(",") + ",");
//if (departmentId >= 0) exp &= _.DepartmentID == departmentId;
//if (departmentIds != null && departmentIds.Length > 0) exp &= _.DepartmentID.In(departmentIds);
//if (enable != null) exp &= _.Enable == enable.Value;
//exp &= _.LastLogin.Between(start, end);
//if (!key.IsNullOrEmpty()) exp &= _.Code.StartsWith(key) | _.Name.StartsWith(key) | _.DisplayName.StartsWith(key) | _.Mobile.StartsWith(key) | _.Mail.StartsWith(key);
//var list2 = XCode.Membership.User.FindAll(exp, p);
if (areaIds.Length > 0)
{
var rs = areaIds.ToList();
var r = Area.FindByID(areaIds[areaIds.Length - 1]);
if (r != null)
{
// 城市,要下一级
if (r.Level == 2)
{
rs.AddRange(r.Childs.Select(e => e.ID));
}
// 省份,要下面两级
else if (r.Level == 1)
{
rs.AddRange(r.Childs.Select(e => e.ID));
foreach (var item in r.Childs)
{
rs.AddRange(item.Childs.Select(e => e.ID));
}
}
}
areaIds = rs.ToArray();
}
//if (roleId > 0) roleIds.Add(roleId);
//if (departmentId > 0) departmentIds.Add(departmentId);
var list2 = XCode.Membership.User.Search(roleIds, departmentIds, areaIds, enable, start, end, key, p);
foreach (var user in list2)
{
user.Password = null;
}
return list2;
}
/// <summary>验证实体对象</summary>
/// <param name="entity"></param>
/// <param name="type"></param>
/// <param name="post"></param>
/// <returns></returns>
protected override Boolean Valid(User entity, DataObjectMethodType type, Boolean post)
{
if (!post && type == DataObjectMethodType.Update)
{
// 清空密码,不向浏览器输出
//entity.Password = null;
entity["Password"] = null;
}
if (post && type == DataObjectMethodType.Update)
{
var ds = (entity as IEntity).Dirtys;
if (ds["Password"])
{
if (entity.Password.IsNullOrEmpty())
ds["Password"] = false;
else
entity.Password = ManageProvider.Provider.PasswordProvider.Hash(entity.Password);
}
if (!entity.RoleIds.IsNullOrEmpty()) entity.RoleIds = entity.RoleIds == "-1" ? null : entity.RoleIds.Replace(",-1,", ",");
}
return base.Valid(entity, type, post);
}
#region
/// <summary>登录</summary>
/// <returns></returns>
[AllowAnonymous]
public ActionResult Login()
{
var returnUrl = GetRequest("r");
if (returnUrl.IsNullOrEmpty()) returnUrl = GetRequest("ReturnUrl");
// 如果已登录,直接跳转
if (ManageProvider.User != null)
{
if (Url.IsLocalUrl(returnUrl))
return Redirect(returnUrl);
else
return RedirectToAction("Index", "Index", new { page = returnUrl });
}
// 是否已完成第三方登录
var logId = Session["Cube_OAuthId"].ToLong();
// 如果禁用本地登录,且只有一个第三方登录,直接跳转,构成单点登录
var ms = OAuthConfig.GetValids(GrantTypes.AuthorizationCode);
if (ms != null && !Setting.Current.AllowLogin)
{
if (ms.Count == 0) throw new Exception("禁用了本地密码登录,且没有配置第三方登录");
if (logId > 0) throw new Exception("已完成第三方登录但无法绑定本地用户且没有开启自动注册建议开启OAuth应用的自动注册");
// 只有一个,跳转
if (ms.Count == 1)
{
var url = $"~/Sso/Login?name={ms[0].Name}";
if (!returnUrl.IsNullOrEmpty()) url += "&r=" + HttpUtility.UrlEncode(returnUrl);
return Redirect(url);
}
}
// 部分提供支持应用内免登录,直接跳转
if (ms != null && ms.Count > 0 && logId == 0 && GetRequest("autologin") != "0")
{
var agent = Request.Headers["User-Agent"] + "";
if (!agent.IsNullOrEmpty())
{
foreach (var item in ms)
{
var client = OAuthClient.Create(item.Name);
if (client != null && client.Support(agent))
{
var url = $"~/Sso/Login?name={item.Name}";
if (!returnUrl.IsNullOrEmpty()) url += "&r=" + HttpUtility.UrlEncode(returnUrl);
return Redirect(url);
}
}
}
}
//ViewBag.IsShowTip = XCode.Membership.User.Meta.Count == 1;
//ViewBag.ReturnUrl = returnUrl;
var model = GetViewModel(returnUrl);
model.OAuthItems = ms.Where(e => e.Visible).ToList();
return View(model);
}
private LoginViewModel GetViewModel(String returnUrl)
{
var set = Setting.Current;
var sys = SysConfig.Current;
var model = new LoginViewModel
{
DisplayName = sys.DisplayName,
AllowLogin = set.AllowLogin,
AllowRegister = set.AllowRegister,
//AutoRegister = set.AutoRegister,
LoginTip = set.LoginTip,
ResourceUrl = set.ResourceUrl,
ReturnUrl = returnUrl,
//OAuthItems = ms,
};
// 默认登录提示,没有新用户之前
if (model.LoginTip.IsNullOrEmpty() && XCode.Membership.User.Meta.Count <= 1)
model.LoginTip = "首个注册登录用户成为管理员默认用户admin/admin推荐第三方登录";
if (model.ResourceUrl.IsNullOrEmpty()) model.ResourceUrl = "/Content";
model.ResourceUrl = model.ResourceUrl.TrimEnd('/');
// 是否使用Sso登录
var appId = GetRequest("ssoAppId").ToInt();
var app = App.FindById(appId);
if (app != null)
{
model.DisplayName = app + "";
model.Logo = app.Logo;
}
return model;
}
/// <summary>密码登录</summary>
/// <returns></returns>
[HttpPost()]
[AllowAnonymous]
public ActionResult Login(LoginModel loginModel)
{
var username = loginModel.Username;
var password = loginModel.Password;
var remember = loginModel.Remember;
// 连续错误校验
var key = $"Login:{username}";
var errors = _cache.Get<Int32>(key);
var ipKey = $"Login:{UserHost}";
var ipErrors = _cache.Get<Int32>(ipKey);
var set = Setting.Current;
var returnUrl = GetRequest("r");
if (returnUrl.IsNullOrEmpty()) returnUrl = GetRequest("ReturnUrl");
try
{
if (username.IsNullOrEmpty()) throw new ArgumentNullException(nameof(username), "用户名不能为空!");
if (password.IsNullOrEmpty()) throw new ArgumentNullException(nameof(password), "密码不能为空!");
if (errors >= set.MaxLoginError && set.MaxLoginError > 0) throw new InvalidOperationException($"[{username}]登录错误过多,请在{set.LoginForbiddenTime}秒后再试!");
if (ipErrors >= set.MaxLoginError && set.MaxLoginError > 0) throw new InvalidOperationException($"IP地址[{UserHost}]登录错误过多,请在{set.LoginForbiddenTime}秒后再试!");
var provider = ManageProvider.Provider;
if (ModelState.IsValid && provider.Login(username, password, remember) != null)
{
// 登录成功,清空错误数
if (errors > 0) _cache.Remove(key);
if (ipErrors > 0) _cache.Remove(ipKey);
if (IsJsonRequest)
{
var token = HttpContext.Items["jwtToken"];
return Json(0, "ok", new { /*provider.Current.ID,*/ Token = token });
}
//FormsAuthentication.SetAuthCookie(username, remember ?? false);
// 记录在线统计
var stat = UserStat.GetOrAdd(DateTime.Today);
if (stat != null)
{
stat.Logins++;
stat.SaveAsync(5_000);
}
if (Url.IsLocalUrl(returnUrl)) return Redirect(returnUrl);
// 不要嵌入自己
if (returnUrl.EndsWithIgnoreCase("/Admin", "/Admin/User/Login")) returnUrl = null;
// 登录后自动绑定
var logId = Session["Cube_OAuthId"].ToLong();
if (logId > 0)
{
Session["Cube_OAuthId"] = null;
var log = NewLife.Cube.Controllers.SsoController.Provider.BindAfterLogin(logId);
if (log != null && log.Success && !log.RedirectUri.IsNullOrEmpty()) return Redirect(log.RedirectUri);
}
return RedirectToAction("Index", "Index", new { page = returnUrl });
}
// 如果我们进行到这一步时某个地方出错,则重新显示表单
ModelState.AddModelError("username", "提供的用户名或密码不正确。");
}
catch (Exception ex)
{
// 登录失败比较重要,记录一下
var action = ex is InvalidOperationException ? "风控" : "登录";
LogProvider.Provider.WriteLog(typeof(User), action, false, ex.Message, 0, username, UserHost);
XTrace.WriteLine("[{0}]登录失败!{1}", username, ex.Message);
XTrace.WriteException(ex);
// 累加错误数,首次出错时设置过期时间
_cache.Increment(key, 1);
_cache.Increment(ipKey, 1);
var time = 300;
if (set.LoginForbiddenTime > 0) time = set.LoginForbiddenTime;
if (errors <= 0) _cache.SetExpire(key, TimeSpan.FromSeconds(time));
if (ipErrors <= 0) _cache.SetExpire(ipKey, TimeSpan.FromSeconds(time));
if (IsJsonRequest)
{
return Json(500, ex.Message);
}
ModelState.AddModelError("", ex.Message);
}
////云飞扬2019-02-15修改密码错误后会走到这需要给ViewBag.IsShowTip重赋值否则抛异常
//ViewBag.IsShowTip = XCode.Membership.User.Meta.Count == 1;
var model = GetViewModel(returnUrl);
model.OAuthItems = OAuthConfig.GetVisibles();
return View(model);
}
/// <summary>注销</summary>
/// <returns></returns>
[AllowAnonymous]
public ActionResult Logout()
{
var returnUrl = GetRequest("r");
if (returnUrl.IsNullOrEmpty()) returnUrl = GetRequest("ReturnUrl");
var set = Setting.Current;
if (set.LogoutAll)
{
// 如果是单点登录,则走单点登录注销
var name = Session["Cube_Sso"] as String;
if (!name.IsNullOrEmpty())
{
UserService.ClearOnline(ManageProvider.User as User);
return Redirect($"~/Sso/Logout?name={name}&r={HttpUtility.UrlEncode(returnUrl)}");
}
//if (!name.IsNullOrEmpty()) return RedirectToAction("Logout", "Sso", new
//{
// area = "",
// name,
// r = returnUrl
//});
}
ManageProvider.Provider.Logout();
if (IsJsonRequest) return Ok();
if (!returnUrl.IsNullOrEmpty()) return Redirect(returnUrl);
return RedirectToAction(nameof(Login));
}
#endregion
/// <summary>获取用户资料</summary>
/// <param name="id"></param>
/// <returns></returns>
//[AllowAnonymous]
[EntityAuthorize]
public ActionResult Info(Int32 id)
{
//if (id == null || id.Value <= 0) throw new Exception("无效用户编号!");
var user = ManageProvider.User as XCode.Membership.User;
if (user == null) return RedirectToAction("Login");
if (id > 0 && id != user.ID) throw new Exception("禁止查看非当前登录用户资料");
user = XCode.Membership.User.FindByKeyForEdit(user.ID);
if (user == null) throw new Exception("无效用户编号!");
//user.Password = null;
user["Password"] = null;
if (IsJsonRequest)
{
var userInfo = new UserInfo();
userInfo.Copy(user);
userInfo.SetPermission(user.Roles);
userInfo.SetRoleNames(user.Roles);
return Json(0, "ok", userInfo);
}
// 用于显示的列
if (ViewBag.Fields == null) ViewBag.Fields = EditFormFields;
ViewBag.Factory = XCode.Membership.User.Meta.Factory;
// 必须指定视图名因为其它action会调用
return View("Info", user);
}
/// <summary>更新用户资料</summary>
/// <param name="user"></param>
/// <returns></returns>
[HttpPost]
//[AllowAnonymous]
[EntityAuthorize]
public async Task<ActionResult> Info(User user)
{
var cur = ManageProvider.User;
if (cur == null) return RedirectToAction("Login");
if (user.ID != cur.ID) throw new Exception("禁止修改非当前登录用户资料");
var entity = user as IEntity;
if (entity.Dirtys["Name"]) throw new Exception("禁止修改用户名!");
if (entity.Dirtys["RoleID"]) throw new Exception("禁止修改角色!");
if (entity.Dirtys["Enable"]) throw new Exception("禁止修改禁用!");
var file = HttpContext.Request.Form.Files["avatar"];
if (file != null)
{
var set = Setting.Current;
var fileName = user.ID + Path.GetExtension(file.FileName);
var att = await SaveFile(user, file, set.AvatarPath, fileName);
if (att != null) user.Avatar = att.FilePath;
}
user.Update();
return Info(user.ID);
}
/// <summary>保存文件</summary>
/// <param name="entity">实体对象</param>
/// <param name="file">文件</param>
/// <param name="uploadPath">上传目录默认使用UploadPath配置</param>
/// <param name="fileName">文件名,如若指定则忽略前面的目录</param>
/// <returns></returns>
protected override Task<Attachment> SaveFile(User entity, IFormFile file, String uploadPath, String fileName)
{
// 修改保存目录和文件名
var set = Setting.Current;
if (file.Name.EqualIgnoreCase("avatar")) fileName = entity.ID + Path.GetExtension(file.FileName);
return base.SaveFile(entity, file, set.AvatarPath, fileName);
}
/// <summary>修改密码</summary>
/// <returns></returns>
//[AllowAnonymous]
[EntityAuthorize]
public ActionResult ChangePassword()
{
var user = ManageProvider.User as XCode.Membership.User;
if (user == null) return RedirectToAction("Login");
var name = Session["Cube_Sso"] as String;
var model = new ChangePasswordModel
{
Name = user.Name,
SsoName = name,
};
return View(model);
}
/// <summary>修改密码</summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
//[AllowAnonymous]
[EntityAuthorize]
public ActionResult ChangePassword(ChangePasswordModel model)
{
if (model.NewPassword.IsNullOrWhiteSpace()) throw new ArgumentException($"新密码不能为 Null 或空白", nameof(model.NewPassword));
if (model.NewPassword2.IsNullOrWhiteSpace()) throw new ArgumentException($"确认密码不能为 Null 或空白", nameof(model.NewPassword2));
if (model.NewPassword != model.NewPassword2) throw new ArgumentException($"两次输入密码不一致", nameof(model.NewPassword));
if (!_passwordService.Valid(model.NewPassword)) throw new ArgumentException($"密码太弱要求8位起且包含数字大小写字母和符号", nameof(model.NewPassword));
// SSO 登录不需要知道原密码就可以修改,原则上更相信外方,同时也避免了直接第三方登录没有设置密码的尴尬
var ssoName = Session["Cube_Sso"] as String;
var requireOldPass = ssoName.IsNullOrEmpty();
if (requireOldPass)
{
if (model.OldPassword.IsNullOrWhiteSpace()) throw new ArgumentException($"原密码不能为 Null 或空白", nameof(model.OldPassword));
if (model.NewPassword == model.OldPassword) throw new ArgumentException($"修改密码不能与原密码一致", nameof(model.NewPassword));
}
var current = ManageProvider.User;
if (current == null) return RedirectToAction("Login");
var user = ManageProvider.Provider.ChangePassword(current.Name, model.NewPassword, requireOldPass ? model.OldPassword : null);
//(user as User).Update();
ViewBag.StatusMessage = "修改成功!";
if (IsJsonRequest) return Ok(ViewBag.StatusMessage);
return ChangePassword();
}
/// <summary>用户绑定</summary>
/// <returns></returns>
//[AllowAnonymous]
[EntityAuthorize]
public ActionResult Binds()
{
var user = ManageProvider.User as XCode.Membership.User;
if (user == null) return RedirectToAction("Login");
user = XCode.Membership.User.FindByKeyForEdit(user.ID);
if (user == null) throw new Exception("无效用户编号!");
// 第三方绑定
var ucs = UserConnect.FindAllByUserID(user.ID);
var ms = OAuthConfig.GetValids(GrantTypes.AuthorizationCode);
var model = new BindsModel
{
Name = user.Name,
Connects = ucs,
OAuthItems = ms,
};
if (IsJsonRequest) return Ok(data: model);
return View(model);
}
/// <summary>注册</summary>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public ActionResult Register(RegisterModel registerModel)
{
var email = registerModel.Email;
var username = registerModel.Username;
var password = registerModel.Password;
var password2 = registerModel.Password2;
var set = Setting.Current;
if (!set.AllowRegister) throw new Exception("禁止注册!");
try
{
//if (String.IsNullOrEmpty(email)) throw new ArgumentNullException("email", "邮箱地址不能为空!");
if (String.IsNullOrEmpty(username)) throw new ArgumentNullException("username", "用户名不能为空!");
if (String.IsNullOrEmpty(password)) throw new ArgumentNullException("password", "密码不能为空!");
if (String.IsNullOrEmpty(password2)) throw new ArgumentNullException("password2", "重复密码不能为空!");
if (password != password2) throw new ArgumentOutOfRangeException("password2", "两次密码必须一致!");
if (!_passwordService.Valid(password)) throw new ArgumentException($"密码太弱要求8位起且包含数字大小写字母和符号", nameof(password));
// 去重判断
var user = FindByName(username);
if (user != null) throw new ArgumentException(nameof(username), $"用户[{username}]已存在!");
user = FindByMail(email);
if (user != null) throw new ArgumentException(nameof(email), $"邮箱[{email}]已存在!");
var r = Role.GetOrAdd(set.DefaultRole);
//user = new User()
//{
// Name = username,
// Password = password,
// Mail = email,
// RoleID = r.ID,
// Enable = true
//};
//user.Register();
var user2 = ManageProvider.Provider.Register(username, password, r.ID, true);
// 注册成功
}
catch (ArgumentException aex)
{
ModelState.AddModelError(aex.ParamName, aex.Message);
}
var model = GetViewModel(null);
model.OAuthItems = OAuthConfig.GetVisibles();
return View("Login", model);
}
/// <summary>清空密码</summary>
/// <param name="id"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Update)]
public ActionResult ClearPassword(Int32 id)
{
if (!ManageProvider.User.Role.IsSystem) throw new Exception("清除密码操作需要管理员权限,非法操作!");
// 前面表单可能已经清空密码
var user = FindByID(id);
//user.Password = "nopass";
user.Password = null;
user.SaveWithoutValid();
if (IsJsonRequest) return Ok();
return RedirectToAction("Edit", new { id });
}
///// <summary>批量启用</summary>
///// <param name="keys"></param>
///// <returns></returns>
//[EntityAuthorize(PermissionFlags.Update)]
//public ActionResult EnableSelect(String keys) => EnableOrDisableSelect();
///// <summary>批量禁用</summary>
///// <param name="keys"></param>
///// <returns></returns>
//[EntityAuthorize(PermissionFlags.Update)]
//public ActionResult DisableSelect(String keys) => EnableOrDisableSelect(false);
//private ActionResult EnableOrDisableSelect(Boolean isEnable = true)
//{
// var count = 0;
// var ids = GetRequest("keys").SplitAsInt();
// if (ids.Length > 0)
// {
// foreach (var id in ids)
// {
// var user = FindByID(id);
// if (user != null && user.Enable != isEnable)
// {
// user.Enable = isEnable;
// user.Update();
// Interlocked.Increment(ref count);
// }
// }
// }
// return JsonRefresh($"共{(isEnable ? "启用" : "禁用")}[{count}]个用户");
//}
}

View File

@ -0,0 +1,71 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Cube.Extensions;
using NewLife.Cube.ViewModels;
using NewLife.Web;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers;
/// <summary>用户在线控制器</summary>
[DataPermission(null, "UserID={#userId}")]
[Area("Admin")]
[Menu(0, false)]
public class UserOnlineController : EntityController<UserOnline>
{
/// <summary>
/// 实例化
/// </summary>
public UserOnlineController()
{
PageSetting.EnableAdd = false;
ListFields.RemoveField("ID", "UserID", "SessionID", "Status", "LastError", "CreateIP", "CreateTime");
ListFields.TraceUrl("TraceId");
{
var df = ListFields.GetField("Name") as ListField;
//df.DisplayName = "跟踪";
df.Url = "/Admin/User?id={UserID}";
df.DataVisible = e => (e as UserOnline).UserID > 0;
}
}
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<UserOnline> Search(Pager p)
{
var userid = p["UserID"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
// 强制当前用户
if (userid < 0)
{
var user = ManageProvider.User;
if (!user.Roles.Any(e => e.IsSystem)) userid = user.ID;
}
return UserOnline.Search(userid, null, start, end, p["Q"], p);
}
/// <summary>验证数据</summary>
/// <param name="entity"></param>
/// <param name="type"></param>
/// <param name="post"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
protected override Boolean Valid(UserOnline entity, DataObjectMethodType type, Boolean post)
{
if (!post) return base.Valid(entity, type, post);
return type switch
{
DataObjectMethodType.Update or DataObjectMethodType.Insert => throw new Exception("不允许添加/修改记录"),
_ => base.Valid(entity, type, post),
};
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Charts;
using NewLife.Cube.Entity;
using NewLife.Web;
using XCode.Membership;
using static NewLife.Cube.Entity.UserStat;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>访问统计控制器</summary>
[Area("Admin")]
[Menu(0, false)]
public class UserStatController : ReadOnlyEntityController<UserStat>
{
static UserStatController()
{
ListFields.RemoveField("ID", "CreateTime", "UpdateTime", "Remark");
}
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<UserStat> Search(Pager p)
{
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
p.RetrieveState = true;
var list = SearchByDate(start, end, p["Q"], p);
if (list.Count > 0)
{
var list2 = list.OrderBy(e => e.Date).ToList();
var chart = new ECharts
{
Height = 400,
};
chart.SetX(list2, _.Date);
//chart.SetY("数值");
chart.YAxis = new[] {
new { name = "数值", type = "value" },
new { name = "总数", type = "value" }
};
chart.AddDataZoom();
var line = chart.AddLine(list2, _.Total, null, true);
line["yAxisIndex"] = 1;
chart.Add(list2, _.Logins);
chart.Add(list2, _.OAuths);
chart.Add(list2, _.MaxOnline);
chart.Add(list2, _.Actives);
chart.Add(list2, _.ActivesT7);
chart.Add(list2, _.ActivesT30);
chart.Add(list2, _.News);
chart.Add(list2, _.NewsT7);
chart.Add(list2, _.NewsT30);
//chart.Add(list2, _.OnlineTime);
chart.SetTooltip();
//chart["dataZoom"] = new[] {
// new {
// show = true,
// realtime = true,
// start = 0,
// end = 100,
// xAxiaIndex = new[] { 0, 1 }
// }
//};
//var chart2 = new ECharts();
//chart2.AddPie(list, _.Total, e => new NameValue(e.Date.ToString("yyyy-MM-dd"), e.Total));
//chart2.AddPie(list, _.MaxOnline, e => new NameValue(e.Date.ToString("yyyy-MM-dd"), e.Total));
ViewBag.Charts = new[] { chart };
//ViewBag.Charts2 = new[] { chart2 };
}
return list;
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Web;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>用户令牌控制器</summary>
[DataPermission(null, "UserID={#userId}")]
[DisplayName("用户令牌")]
[Description("授权指定用户访问接口数据,支持有效期")]
[Area("Admin")]
[Menu(0, false)]
public class UserTokenController : EntityController<UserToken>
{
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<UserToken> Search(Pager p)
{
var token = p["Q"];
var userid = p["UserID"].ToInt(-1);
var enable = p["enable"]?.ToBoolean();
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
// 强制当前用户
if (userid < 0)
{
var user = ManageProvider.User;
if (!user.Roles.Any(e => e.IsSystem)) userid = user.ID;
}
return UserToken.Search(token, userid, enable, start, end, p);
}
/// <summary>验证权限</summary>
/// <param name="entity">实体对象</param>
/// <param name="type">操作类型</param>
/// <param name="post">是否提交数据阶段</param>
/// <returns></returns>
protected override Boolean ValidPermission(UserToken entity, DataObjectMethodType type, Boolean post)
{
var user = ManageProvider.Provider?.Current;
// 系统角色拥有特权
if (user is IUser user2 && user2.Roles.Any(e => e.IsSystem)) return true;
// 特殊处理添加操作
if (type == DataObjectMethodType.Insert && entity.UserID <= 0)
{
entity.UserID = user.ID;
}
return entity.UserID == user.ID;
}
}
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using XCode.Membership;
namespace NewLife.Cube.Admin.Controllers
{
/// <summary>设置控制器</summary>
[DisplayName("数据中间件")]
[Area("Admin")]
[Menu(0, false)]
public class XCodeController : ConfigController<XCode.Setting>
{
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using NewLife.Cube.Entity;
namespace NewLife.Cube.Areas.Admin.Models
{
/// <summary>第三方绑定模型</summary>
public class BindsModel : ICubeModel
{
/// <summary>用户名</summary>
public String Name { get; set; }
/// <summary>用户链接集合</summary>
public IList<UserConnect> Connects { get; set; }
/// <summary>可选的第三方</summary>
public IList<OAuthConfig> OAuthItems { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
namespace NewLife.Cube.Areas.Admin.Models
{
/// <summary>修改密码模型</summary>
public class ChangePasswordModel : ICubeModel
{
/// <summary>用户名</summary>
public String Name { get; set; }
/// <summary>Sso登录渠道</summary>
public String SsoName { get; set; }
/// <summary>旧密码</summary>
public String OldPassword { get; set; }
/// <summary>新密码</summary>
public String NewPassword { get; set; }
/// <summary>确认密码</summary>
public String NewPassword2 { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using NewLife.Cube.Areas.Admin.Models;
using XCode.DataAccessLayer;
namespace NewLife.Cube.Admin
{
/// <summary>数据项</summary>
public class DbItem : ICubeModel
{
/// <summary>连接名</summary>
public String Name { get; set; }
/// <summary>数据库类型</summary>
public DatabaseType Type { get; set; }
/// <summary>连接字符串</summary>
public String ConnStr { get; set; }
/// <summary>数据驱动版本</summary>
public String Version { get; set; }
/// <summary>是否动态</summary>
public Boolean Dynamic { get; set; }
/// <summary>备份数</summary>
public Int32 Backups { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using NewLife.Cube.Areas.Admin.Models;
using XCode.DataAccessLayer;
namespace NewLife.Cube.Admin
{
/// <summary>文件项</summary>
public class FileItem : ICubeModel
{
/// <summary>连接名</summary>
public String Name { get; set; }
/// <summary>全路径</summary>
public String FullName { get; set; }
/// <summary>原始路径</summary>
public String Raw { get; set; }
/// <summary>是否目录</summary>
public Boolean Directory { get; set; }
/// <summary>大小字符串</summary>
public String Size { get; set; }
/// <summary>最后写入时间</summary>
public DateTime LastWrite { get; set; }
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using NewLife.Cube.Entity;
namespace NewLife.Cube.Areas.Admin.Models
{
/// <summary>登录视图模型</summary>
public class LoginViewModel
{
/// <summary>名称</summary>
public String Name { get; set; }
/// <summary>显示名</summary>
public String DisplayName { get; set; }
/// <summary>Logo</summary>
public String Logo { get; set; }
/// <summary>允许登录</summary>
public Boolean AllowLogin { get; set; }
/// <summary>允许注册</summary>
public Boolean AllowRegister { get; set; }
///// <summary>自动注册</summary>
//public Boolean AutoRegister { get; set; }
/// <summary>登录提示</summary>
public String LoginTip { get; set; }
/// <summary>资源地址。指向CDN如 https://sso.newlifex.com/content/,留空表示使用本地</summary>
public String ResourceUrl { get; set; }
/// <summary>返回地址</summary>
public String ReturnUrl { get; set; }
/// <summary>OAuth系统集合</summary>
public IList<OAuthConfig> OAuthItems { get; set; }
}
}

View File

@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using NewLife.Collections;
using NewLife.Cube.Entity;
using NewLife.Web;
using XCode.Membership;
namespace NewLife.Cube.Areas.Admin.Models
{
/// <summary>
/// 继承此接口可通过json方式传值
/// </summary>
public interface ICubeModel
{
}
/// <summary>
/// 登录模型
/// </summary>
public class LoginModel : ICubeModel
{
/// <summary>
/// 登录用户名
/// </summary>
public String Username { get; set; }
/// <summary>
/// 密码
/// </summary>
public String Password { get; set; }
/// <summary>
/// 记住登录状态
/// </summary>
public Boolean Remember { get; set; }
}
/// <summary>注册模型</summary>
public class RegisterModel : ICubeModel
{
/// <summary>
/// 电子邮箱
/// </summary>
public String Email { get; set; }
/// <summary>
/// 用户名
/// </summary>
public String Username { get; set; }
/// <summary>
/// 密码
/// </summary>
public String Password { get; set; }
/// <summary>
/// 确认密码
/// </summary>
public String Password2 { get; set; }
}
/// <summary>
/// 用户信息
/// </summary>
public class UserInfo
{
/// <summary>
/// 编号
/// </summary>
public Int32 ID { get; set; }
/// <summary>名称。登录用户名</summary>
public String Name { get; set; }
/// <summary>密码</summary>
public String Password { get; set; }
/// <summary>昵称</summary>
public String DisplayName { get; set; }
/// <summary>性别。未知、男、女</summary>
public XCode.Membership.SexKinds Sex { get; set; }
/// <summary>邮件</summary>
public String Mail { get; set; }
/// <summary>手机</summary>
public String Mobile { get; set; }
/// <summary>代码。身份证、员工编号等</summary>
public String Code { get; set; }
/// <summary>头像</summary>
public String Avatar { get; set; }
/// <summary>角色。主要角色</summary>
public Int32 RoleID { get; set; }
/// <summary>角色组。次要角色集合</summary>
public String RoleIds { get; set; }
/// <summary>
/// 主要角色名
/// </summary>
public String RoleName { get; set; }
/// <summary>
/// 角色集合名,逗号隔开
/// </summary>
public String RoleNames { get; set; }
/// <summary>部门。组织机构</summary>
public Int32 DepartmentID { get; set; }
/// <summary>在线</summary>
public Boolean Online { get; set; }
/// <summary>启用</summary>
public Boolean Enable { get; set; }
/// <summary>登录次数</summary>
public Int32 Logins { get; set; }
/// <summary>最后登录</summary>
public DateTime LastLogin { get; set; }
/// <summary>最后登录IP</summary>
public String LastLoginIP { get; set; }
/// <summary>注册时间</summary>
public DateTime RegisterTime { get; set; }
/// <summary>注册IP</summary>
public String RegisterIP { get; set; }
/// <summary>扩展1</summary>
public Int32 Ex1 { get; set; }
/// <summary>扩展2</summary>
public Int32 Ex2 { get; set; }
/// <summary>扩展3</summary>
public Double Ex3 { get; set; }
/// <summary>扩展4</summary>
public String Ex4 { get; set; }
/// <summary>扩展5</summary>
public String Ex5 { get; set; }
/// <summary>扩展6</summary>
public String Ex6 { get; set; }
/// <summary>更新者</summary>
public String UpdateUser { get; set; }
/// <summary>更新用户</summary>
public Int32 UpdateUserID { get; set; }
/// <summary>更新地址</summary>
public String UpdateIP { get; set; }
/// <summary>更新时间</summary>
public DateTime UpdateTime { get; set; }
/// <summary>备注</summary>
public String Remark { get; set; }
/// <summary>
/// 包括角色组的权限集合
/// </summary>
public String Permission { get; set; }
/// <summary>
/// 设置用户权限集合
/// </summary>
/// <param name="roles"></param>
public void SetPermission(IRole[] roles)
{
var ps = new Dictionary<Int32, Int32>();
foreach (var role in roles)
{
foreach (var rolePermission in role.Permissions)
{
if (!ps.ContainsKey(rolePermission.Key))
{
ps[rolePermission.Key] = rolePermission.Value.ToInt();
continue;
}
var permission = ps[rolePermission.Key];
var addPermission = rolePermission.Value.ToInt();
// 总权限=旧权限+新权限-重复权限
// 比如旧权限1+2+8=11新权限1+2+16=19重复权限11&19=3总权限=11+19-3=27
ps[rolePermission.Key] = (permission + addPermission) - (permission & addPermission);
}
}
var sb = Pool.StringBuilder.Get();
// 根据资源按照从小到大排序一下
foreach (var item in ps.OrderBy(e => e.Key))
{
if (sb.Length > 0) sb.Append(',');
sb.AppendFormat("{0}#{1}", item.Key, item.Value);
}
Permission = sb.Put(true);
}
/// <summary>
/// 设置所有角色名
/// </summary>
/// <param name="roles"></param>
public void SetRoleNames(IRole[] roles)
{
if (roles == null) return;
if (!RoleNames.IsNullOrWhiteSpace()) return;
RoleNames = roles.Select(s => s.Name).Join();
}
}
}

View File

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Cube.Controllers
{
/// <summary>应用系统</summary>
[DisplayName("应用系统")]
[Area("Cube")]
[Menu(38, true, Icon = "fa-star")]
public class AppController : EntityController<App>
{
static AppController()
{
LogOnChange = true;
ListFields.RemoveField("ID", "Secret", "HomePage", "Logo", "White", "Black", "Urls", "Remark");
ListFields.RemoveCreateField();
ListFields.RemoveUpdateField();
{
var df = ListFields.AddListField("AppLog", "Enable");
//df.Header = "日志";
df.DisplayName = "日志";
df.Url = "/Cube/AppLog?appId={Id}";
}
{
var df = AddFormFields.GetField("RoleIds");
df.DataSource = e => Role.FindAllWithCache().OrderByDescending(e => e.Sort).ToDictionary(e => e.ID, e => e.Name);
}
{
var df = EditFormFields.GetField("RoleIds");
df.DataSource = e => Role.FindAllWithCache().OrderByDescending(e => e.Sort).ToDictionary(e => e.ID, e => e.Name);
}
{
var df = ListFields.AddListField("Log", "UpdateUserId");
//df.Header = "修改日志";
df.DisplayName = "修改日志";
df.Url = "/Admin/Log?category=应用系统&linkId={ID}";
}
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Cube.Extensions;
using NewLife.Cube.ViewModels;
using NewLife.Web;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Cube.Controllers
{
/// <summary>应用日志</summary>
[DisplayName("应用日志")]
[Area("Cube")]
[Menu(0, false)]
public class AppLogController : ReadOnlyEntityController<AppLog>
{
static AppLogController()
{
ListFields.RemoveField("ID");
ListFields.TraceUrl("TraceId");
//{
// var df = ListFields.GetField("TraceId") as ListField;
// df.DisplayName = "跟踪";
// df.Url = StarHelper.BuildUrl("{TraceId}");
// df.DataVisible = (e, f) => !(e as AppLog).TraceId.IsNullOrEmpty();
//}
}
/// <summary>搜索</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<AppLog> Search(Pager p)
{
var appId = p["appId"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
var key = p["Q"];
if (p.Sort.IsNullOrEmpty()) p.Sort = AppLog._.Id.Desc();
return AppLog.Search(appId, start, end, key, p);
}
}
}

View File

@ -0,0 +1,146 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Charts;
using NewLife.Cube.ViewModels;
using NewLife.Web;
using XCode;
using XCode.Membership;
using static XCode.Membership.Area;
namespace NewLife.Cube.Cube.Controllers
{
/// <summary>地区</summary>
[DisplayName("地区")]
[Area("Cube")]
[Menu(50, true, Icon = "fa-area-chart")]
public class AreaController : EntityController<Area>
{
static AreaController()
{
LogOnChange = true;
ListFields.RemoveCreateField();
ListFields.RemoveRemarkField();
{
var df = ListFields.GetField("ParentID") as ListField;
df.DisplayName = "{ParentPath}";
df.Url = "/Cube/Area?Id={ParentID}";
}
{
var df = ListFields.AddDataField("sub", "Level") as ListField;
df.DisplayName = "下级";
df.Url = "/Cube/Area?parentId={ID}";
}
//AddFormFields.AddField("ID");
}
private static Boolean _inited;
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<Area> Search(Pager p)
{
if (!_inited)
{
_inited = true;
// 异步初始化数据
//if (Area.Meta.Count == 0) ThreadPoolX.QueueUserWorkItem(() => Area.FetchAndSave());
// 必须同步初始化,否则无法取得当前登录用户信息
//if (Area.Meta.Count == 0) Area.FetchAndSave();
if (Area.Meta.Count == 0) Import("http://x.newlifex.com/Area.csv.gz", true);
}
var id = p["id"].ToInt(-1);
if (id < 0) id = p["q"].ToInt(-1);
if (id > 0)
{
var ss = new List<Area>();
var entity = FindByID(id);
if (entity != null) ss.Add(entity);
return ss;
}
Boolean? enable = null;
if (!p["enable"].IsNullOrEmpty()) enable = p["enable"].ToBoolean();
var idstart = p["idStart"].ToInt(-1);
var idend = p["idEnd"].ToInt(-1);
var parentid = p["parentid"].ToInt(-1);
if (parentid < 0)
{
var areaId = p["AreaID"];
parentid = ("-1/" + areaId).SplitAsInt("/").LastOrDefault();
}
var level = p["Level"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
// 地区默认升序
if (p.Sort.IsNullOrEmpty()) p.OrderBy = _.ID.Asc();
var list = Area.Search(parentid, level, idstart, idend, enable, p["q"], start, end, p);
if (list.Count > 0)
{
var exp = new WhereExpression();
exp &= _.ID <= 999999;
var list2 = Area.FindAll(exp.GroupBy(_.Kind), null, _.ID.Count() & _.Kind, 0, 0);
list2 = list2.OrderByDescending(e => e.ID).ToList();
if (list2.Count >= 0)
{
var chart = new ECharts
{
Height = 400,
};
chart.SetX(list2, _.Kind, e => e.Kind ?? "未知");
chart.SetY(null, "value");
chart.SetTooltip();
var bar = chart.AddBar(list2, _.Kind, e => e.ID);
ViewBag.Charts = new[] { chart };
}
if (list2.Count >= 0)
{
var chart = new ECharts
{
Height = 400,
};
//chart.SetX(list2, _.Kind);
//chart.SetY(null, "value");
chart.Legend = new { show = "false", top = "5%", left = "center" };
var pie = chart.AddPie(list2, _.Kind, e => new NameValue(e.Kind ?? "未知", e.ID));
pie["radius"] = new[] { "40%", "70%" };
pie["avoidLabelOverlap"] = false;
pie["itemStyle"] = new { borderRadius = 10, borderColor = "#fff", borderWidth = 2 };
pie["label"] = new { show = false, position = "center" };
pie["emphasis"] = new { label = new { show = true, fontSize = 40, fontWeight = "bold" } };
pie["labelLine"] = new { show = false };
chart.SetTooltip("item", null, null);
ViewBag.Charts2 = new[] { chart };
}
}
return list;
}
/// <summary>
/// 中国地图
/// </summary>
/// <returns></returns>
public ActionResult Map()
{
PageSetting.EnableNavbar = false;
return View("Map");
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Cube.ViewModels;
using NewLife.Web;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Cube.Controllers
{
/// <summary>附件管理</summary>
[Area("Cube")]
[Menu(38, true, Icon = "fa-file-text")]
public class AttachmentController : EntityController<Attachment>
{
static AttachmentController()
{
ListFields.RemoveField("ID", "Hash", "Url", "Source", "UpdateUserID", "UpdateIP", "Remark");
ListFields.RemoveCreateField();
{
var df = ListFields.GetField("Category") as ListField;
df.Url = "/Cube/Area?category={Category}";
}
{
var df = ListFields.GetField("Key") as ListField;
df.Url = "/Cube/Area?category={Category}&key={Key}";
}
{
var df = ListFields.GetField("Extension") as ListField;
df.Url = "/Cube/Area?ext={Extension}";
}
{
var df = ListFields.AddListField("Info", null, "Title");
df.DisplayName = "信息页";
df.Url = "{Url}";
df.DataVisible = e => !(e as Attachment).Url.IsNullOrEmpty();
}
{
var df = ListFields.AddListField("down", null, "Title");
df.DisplayName = "下载";
df.Url = "/cube/file/{Id}{Extension}";
df.Target = "blank";
}
}
/// <summary>搜索</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<Attachment> Search(Pager p)
{
var category = p["category"];
var key = p["key"];
var ext = p["ext"];
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
if (p.Sort.IsNullOrEmpty()) p.Sort = AppLog._.Id.Desc();
return Attachment.Search(category, key, ext, start, end, p["Q"], p);
}
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Cube.Services;
using NewLife.Threading;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Cube.Controllers
{
/// <summary>定时任务</summary>
[Area("Cube")]
[Menu(35, true, Icon = "fa-clock-o")]
public class CronJobController : EntityController<CronJob>
{
static CronJobController()
{
LogOnChange = true;
ListFields.RemoveField("Method", "Remark");
ListFields.RemoveCreateField();
{
var df = ListFields.AddListField("Log", null, "Enable");
//df.Header = "日志";
df.DisplayName = "日志";
df.Url = "/Admin/Log?category=定时作业&linkId={Id}";
}
{
var df = ListFields.AddListField("JobLog", null, "Enable");
//df.Header = "作业日志";
df.DisplayName = "作业日志";
df.Url = "/Admin/Log?category=JobService&linkId={Id}";
}
{
var df = ListFields.AddListField("Execute", null, "NextTime");
//df.Header = "马上执行";
df.DisplayName = "马上执行";
df.Url = "/Cube/CronJob/ExecuteNow?id={Id}";
df.DataAction = "action";
}
}
/// <summary>修改数据时,唤醒作业服务跟进</summary>
/// <param name="entity"></param>
/// <param name="type"></param>
/// <param name="post"></param>
/// <returns></returns>
protected override Boolean Valid(CronJob entity, DataObjectMethodType type, Boolean post)
{
if (post)
{
var cron = new Cron();
if (!cron.Parse(entity.Cron)) throw new ArgumentException("Cron表达式有误", nameof(entity.Cron));
// 重算下一次的时间
if (entity is IEntity e && !e.Dirtys[nameof(entity.Name)]) entity.NextTime = cron.GetNext(DateTime.Now);
JobService.Wake();
}
return base.Valid(entity, type, post);
}
/// <summary>马上执行</summary>
/// <param name="id"></param>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Update)]
public ActionResult ExecuteNow(String id)
{
var entity = CronJob.FindById(id.ToInt());
if (entity != null && entity.Enable)
{
entity.NextTime = DateTime.Now;
entity.Update();
JobService.Wake(entity.Id, -1);
}
return JsonRefresh($"已安排执行!");
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Web;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Cube.Controllers
{
/// <summary>模型列</summary>
[Area("Cube")]
[Menu(56, true, Icon = "fa-table")]
public class ModelColumnController : EntityController<ModelColumn>
{
//static ModelColumnController()
//{
// ListFields.RemoveField("Id", "TableId", "IsDataObjectField", "Description", "ShowInList", "ShowInAddForm", "ShowInEditForm", "ShowInDetailForm",
// "ShowInSearch", "Sort", "Width", "CellText", "CellTitle", "CellUrl", "HeaderText", "HeaderTitle", "HeaderUrl", "DataAction", "DataSource");
// ListFields.RemoveCreateField();
// ListFields.RemoveUpdateField();
//}
static ModelColumnController() => ListFields.RemoveField("TableId");
/// <summary>
/// 高级搜索
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<ModelColumn> Search(Pager p)
{
var tableId = p["tableId"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
var key = p["Q"];
if (p.Sort.IsNullOrEmpty()) p.Sort = ModelColumn._.Sort.Asc();
return ModelColumn.Search(tableId, null, start, end, key, p);
}
}
}

View File

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Threading;
using NewLife.Web;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Cube.Controllers
{
/// <summary>模型表</summary>
[Area("Cube")]
[Menu(59, true, Icon = "fa-table")]
public class ModelTableController : EntityController<ModelTable>
{
static ModelTableController()
{
ListFields.RemoveField("ID", "Controller", "TableName", "ConnName");
ListFields.RemoveCreateField();
ListFields.RemoveUpdateField();
{
var df = ListFields.AddListField("Columns", "Enable");
//df.Header = "列集合";
df.DisplayName = "列集合";
df.Url = "/Cube/ModelColumn?tableId={Id}";
}
ModelTableSetting = table =>
{
if (table == null) return null;
var columns = table.GetColumns();
// 不在列表页显示
var fields = columns.FindAll(fa =>
fa.ShowInList &&
(fa.Name.EqualIgnoreCase(ModelTable._.Controller)
|| fa.Name.EqualIgnoreCase(ModelTable._.TableName)
|| fa.Name.EqualIgnoreCase(ModelTable._.ConnName)));
foreach (var field in fields)
{
field.ShowInList = false;
}
// 调整列宽
columns.Find(f => f.Name.EqualIgnoreCase(ModelTable._.Name)).Width = "115";
columns.Find(f => f.Name.EqualIgnoreCase(ModelTable._.DisplayName)).Width = "115";
columns.Find(f => f.Name.EqualIgnoreCase(ModelTable._.Url)).Width = "200";
columns.Save();
// 添加列
var column = ModelColumn.FindByTableIdAndName(table.Id, "Columns") ?? new ModelColumn
{
TableId = table.Id,
Name = "Columns",
DisplayName = "列集合",
//CellText = "列集合",
//CellTitle = "列集合",
CellUrl = "/Admin/ModelColumn?tableId={id}",
ShowInList = true,
Enable = true,
Sort = 5,
Width = "80",
};
column.Save();
return table;
};
}
private static Boolean _inited;
private static void Init()
{
if (_inited) return;
_inited = true;
// 扫描模型表
//ModelTable.ScanModel(areaName, menus);
ThreadPool.QueueUserWorkItem(s =>
{
var mf = ManageProvider.Menu;
if (mf == null) return;
try
{
foreach (var areaType in AreaBase.GetAreas())
{
var areaName = areaType.Name.TrimEnd("Area");
var menus = mf.FindByFullName(areaName);
var root = mf.FindByFullName(areaType.Namespace + ".Controllers");
root ??= mf.Root.FindByPath(areaName);
if (root != null) ModelTable.ScanModel(areaName, root.Childs);
}
}
catch { }
});
}
/// <summary>
/// 高级搜索
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<ModelTable> Search(Pager p)
{
Init();
var category = p["category"];
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
var key = p["Q"];
return ModelTable.Search(category, null, start, end, key, p);
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using NewLife.Cube.Entity;
using NewLife.Cube.ViewModels;
using XCode.Membership;
namespace NewLife.Cube.Cube.Controllers
{
/// <summary>委托代理</summary>
[Area("Cube")]
[Menu(0, true, Icon = "fa-user-secret")]
public class PrincipalAgentController : EntityController<PrincipalAgent>
{
static PrincipalAgentController()
{
ListFields.RemoveField("ID");
ListFields.RemoveCreateField();
LogOnChange = true;
{
var ff = AddFormFields.GetField("PrincipalName") as FormField;
ff.GroupView = "_Form_PrincipalName";
}
{
var ff = EditFormFields.GetField("PrincipalName") as FormField;
ff.GroupView = "_Form_PrincipalName";
}
{
var ff = AddFormFields.GetField("AgentName") as FormField;
ff.GroupView = "_Form_AgentName";
}
{
var ff = EditFormFields.GetField("AgentName") as FormField;
ff.GroupView = "_Form_AgentName";
}
}
/// <summary>
/// 添加页面初始化数据
/// </summary>
/// <param name="entity"></param>
/// <param name="type"></param>
/// <param name="post"></param>
/// <returns></returns>
protected override Boolean Valid(PrincipalAgent entity, DataObjectMethodType type, Boolean post)
{
if (!post && type == DataObjectMethodType.Insert)
{
entity.Enable = true;
entity.Times = 1;
entity.Expire = DateTime.Now.AddMinutes(20);
}
return base.Valid(entity, type, post);
}
}
}

View File

@ -62,38 +62,6 @@
</ItemGroup>
<ItemGroup>
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\AppController.cs" Link="Areas\Cube\Controllers\AppController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\AppLogController.cs" Link="Areas\Cube\Controllers\AppLogController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\AreaController.cs" Link="Areas\Cube\Controllers\AreaController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\AttachmentController.cs" Link="Areas\Cube\Controllers\AttachmentController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\CoreController.cs" Link="Areas\Admin\Controllers\CoreController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\CronJobController.cs" Link="Areas\Cube\Controllers\CronJobController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\CubeController.cs" Link="Areas\Admin\Controllers\CubeController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\DbController.cs" Link="Areas\Admin\Controllers\DbController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\DepartmentController.cs" Link="Areas\Admin\Controllers\DepartmentController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\FileController.cs" Link="Areas\Admin\Controllers\FileController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\LogController.cs" Link="Areas\Admin\Controllers\LogController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\MenuController.cs" Link="Areas\Admin\Controllers\MenuController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\ModelColumnController.cs" Link="Areas\Cube\Controllers\ModelColumnController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\ModelTableController.cs" Link="Areas\Cube\Controllers\ModelTableController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\OAuthConfigController.cs" Link="Areas\Admin\Controllers\OAuthConfigController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\OAuthLogController.cs" Link="Areas\Admin\Controllers\OAuthLogController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\ParameterController.cs" Link="Areas\Admin\Controllers\ParameterController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\PrincipalAgentController.cs" Link="Areas\Cube\Controllers\PrincipalAgentController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\RoleController.cs" Link="Areas\Admin\Controllers\RoleController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\SysController.cs" Link="Areas\Admin\Controllers\SysController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\UserConnectController.cs" Link="Areas\Admin\Controllers\UserConnectController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\UserController.cs" Link="Areas\Admin\Controllers\UserController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\UserOnlineController.cs" Link="Areas\Admin\Controllers\UserOnlineController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\UserStatController.cs" Link="Areas\Admin\Controllers\UserStatController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\UserTokenController.cs" Link="Areas\Admin\Controllers\UserTokenController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Controllers\XCodeController.cs" Link="Areas\Admin\Controllers\XCodeController.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Models\ChangePasswordModel.cs" Link="Areas\Admin\Models\ChangePasswordModel.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Models\BindsModel.cs" Link="Areas\Admin\Models\BindsModel.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Models\DbItem.cs" Link="Areas\Admin\Models\DbItem.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Models\FileItem.cs" Link="Areas\Admin\Models\FileItem.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Models\LoginViewModel.cs" Link="Areas\Admin\Models\LoginViewModel.cs" />
<Compile Include="..\NewLife.Cube\Areas\Admin\Models\UserModel.cs" Link="Areas\Admin\Models\UserModel.cs" />
<Compile Include="..\NewLife.Cube\Common\ConfigController.cs" Link="Common\ConfigController.cs" />
<Compile Include="..\NewLife.Cube\Common\ControllerBaseX.cs" Link="Common\ControllerBaseX.cs" />
<Compile Include="..\NewLife.Cube\Common\DataPermissionAttribute.cs" Link="Common\DataPermissionAttribute.cs" />
@ -146,9 +114,6 @@
<Compile Include="..\NewLife.Cube\Extensions\PagerHelper.cs" Link="Extensions\PagerHelper.cs" />
<Compile Include="..\NewLife.Cube\Extensions\WebHelper.cs" Link="Extensions\WebHelper.cs" />
<Compile Include="..\NewLife.Cube\Setting.cs" Link="Setting.cs" />
<Compile Include="..\NewLife.Cube\ViewModels\FieldModel.cs" Link="ViewModels\FieldModel.cs" />
<Compile Include="..\NewLife.Cube\ViewModels\LoginConfigModel.cs" Link="ViewModels\LoginConfigModel.cs" />
<Compile Include="..\NewLife.Cube\ViewModels\SelectUserModel.cs" Link="ViewModels\SelectUserModel.cs" />
<Compile Include="..\NewLife.Cube\Web\AttachmentProvider.cs" Link="Web\AttachmentProvider.cs" />
<Compile Include="..\NewLife.Cube\Web\Models\ApprovalInfo.cs" Link="Web\Models\ApprovalInfo.cs" />
<Compile Include="..\NewLife.Cube\Web\Models\CheckInData.cs" Link="Web\Models\CheckInData.cs" />

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NewLife.Cube.Common;
namespace NewLife.Cube.ViewModels
{
/// <summary>
/// 字段模型
/// </summary>
public class FieldModel
{
private String _name;
private String _columnName;
/// <summary>
/// 默认CamelCase小驼峰
/// </summary>
public FormatType FormatType = FormatType.CamelCase;
/// <summary>
/// 默认CamelCase小驼峰
/// </summary>
public FieldModel() { }
/// <summary>
///
/// </summary>
/// <param name="formatType">0-小驼峰1-小写2-保持默认</param>
public FieldModel(FormatType formatType) => FormatType = formatType;
/// <summary>备注</summary>
public String Description { get; set; }
/// <summary>说明</summary>
public String DisplayName { get; set; }
/// <summary>属性名</summary>
public String Name
{
get => _name.FormatName(FormatType);
internal set => _name = value;
}
/// <summary>是否允许空</summary>
public Boolean IsNullable { get; internal set; }
/// <summary>长度</summary>
public Int32 Length { get; internal set; }
/// <summary>是否数据绑定列</summary>
public Boolean IsDataObjectField { get; set; }
/// <summary>是否动态字段</summary>
public Boolean IsDynamic { get; set; }
/// <summary>用于数据绑定的字段名</summary>
/// <remarks>
/// 默认使用BindColumn特性中指定的字段名如果没有指定则使用属性名。
/// 字段名可能两边带有方括号等标识符
/// </remarks>
public String ColumnName
{
get => _columnName.FormatName(FormatType);
set => _columnName = value;
}
/// <summary>是否只读</summary>
/// <remarks>set { _ReadOnly = value; } 放出只读属性的设置,比如在编辑页面的时候,有的字段不能修改 如修改用户时 不能修改用户名</remarks>
public Boolean ReadOnly { get; set; }
/// <summary>
/// 字段类型
/// </summary>
public String TypeStr { get; set; } = nameof(String);
#region
/// <summary>
/// 是否定制字段
/// </summary>
public Boolean IsCustom { get; set; }
/// <summary>前缀名称。放在某字段之前</summary>
public String BeforeName { get; set; }
/// <summary>后缀名称。放在某字段之后</summary>
public String AfterName { get; set; }
/// <summary>链接</summary>
public String Url { get; set; }
/// <summary>标题。数据单元格上的提示文字</summary>
public String Title { get; set; }
/// <summary>头部文字</summary>
public String Header { get; set; }
/// <summary>头部链接。一般是排序</summary>
public String HeaderUrl { get; set; }
/// <summary>头部标题。数据移上去后显示的文字</summary>
public String HeaderTitle { get; set; }
/// <summary>数据动作。设为action时走ajax请求</summary>
public String DataAction { get; set; }
#endregion
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NewLife.Common;
using NewLife.Cube.Entity;
using NewLife.Reflection;
namespace NewLife.Cube.ViewModels
{
/// <summary>
/// 登录配置模型
/// </summary>
public class LoginConfigModel
{
private readonly Setting _set = Setting.Current;
private readonly SysConfig _cubeSet = SysConfig.Current;
/// <summary>
/// 显示名
/// </summary>
public String DisplayName => _cubeSet.DisplayName;
/// <summary>
/// Logo图标
/// </summary>
public String Logo => String.Empty;
/// <summary>
/// 允许登录
/// </summary>
public Boolean AllowLogin => _set.AllowLogin;
/// <summary>
/// 允许注册
/// </summary>
public Boolean AllowRegister => _set.AllowRegister;
//public Boolean AutoRegister => _set.AutoRegister;
/// <summary>
/// 提供者
/// </summary>
public List<OAuthConfigModel> Providers =>
OAuthConfig.FindAllWithCache().Where(w=>w.Enable).Select(s =>
{
var m = new OAuthConfigModel();
m.Copy(s);
return m;
}).ToList();
}
/// <summary>
/// OAuth配置模型
/// </summary>
public class OAuthConfigModel
{
/// <summary>
/// 应用名
/// </summary>
public String Name { get; set; }
/// <summary>
/// 图标
/// </summary>
public String Logo { get; set; }
/// <summary>
/// 显示名
/// </summary>
public String NickName { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using System;
namespace NewLife.Cube.ViewModels
{
/// <summary>选择用户控件所使用的模型</summary>
public class SelectUserModel
{
/// <summary>控件</summary>
public String Id { get; set; }
/// <summary>角色</summary>
public Int32 RoleId { get; set; }
/// <summary>部门</summary>
public Int32 DepartmentId { get; set; }
/// <summary>用户Id</summary>
public Int32 UserId { get; set; }
}
}