feat: initialize Evergreen Health backend
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// APIJSON服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 100)]
|
||||
public class APIJSONService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly ISqlSugarClient _db;
|
||||
private readonly IdentityService _identityService;
|
||||
private readonly TableMapper _tableMapper;
|
||||
private readonly SelectTable _selectTable;
|
||||
|
||||
public APIJSONService(ISqlSugarClient db,
|
||||
IdentityService identityService,
|
||||
TableMapper tableMapper)
|
||||
{
|
||||
_db = db;
|
||||
_tableMapper = tableMapper;
|
||||
_identityService = identityService;
|
||||
_selectTable = new SelectTable(_identityService, _tableMapper, _db);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一查询入口 🔖
|
||||
/// </summary>
|
||||
/// <param name="jobject"></param>
|
||||
/// <remarks>参数:{"[]":{"SYSLOGOP":{}}}</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpPost("get")]
|
||||
[DisplayName("APIJSON统一查询")]
|
||||
public JObject Query([FromBody] JObject jobject)
|
||||
{
|
||||
var database = jobject["@database"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(database))
|
||||
{
|
||||
// 设置数据库
|
||||
var provider = _db.AsTenant().GetConnectionScope(database);
|
||||
jobject.Remove("@database");
|
||||
return new SelectTable(_identityService, _tableMapper, provider).Query(jobject);
|
||||
}
|
||||
return _selectTable.Query(jobject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 🔖
|
||||
/// </summary>
|
||||
/// <param name="table"></param>
|
||||
/// <param name="jobject"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("get/{table}")]
|
||||
[DisplayName("APIJSON查询")]
|
||||
public JObject QueryByTable([FromRoute] string table, [FromBody] JObject jobject)
|
||||
{
|
||||
var ht = new JObject
|
||||
{
|
||||
{ table + "[]", jobject }
|
||||
};
|
||||
|
||||
// 自动添加总计数量
|
||||
if (jobject["query"] != null && jobject["query"].ToString() != "0" && jobject["total@"] == null)
|
||||
ht.Add("total@", "");
|
||||
|
||||
// 每页最大1000条数据
|
||||
if (jobject["count"] != null && int.Parse(jobject["count"].ToString()) > 1000)
|
||||
throw Oops.Bah("count分页数量最大不能超过1000");
|
||||
|
||||
jobject.Remove("@debug");
|
||||
|
||||
var hasTableKey = false;
|
||||
var ignoreConditions = new List<string> { "page", "count", "query" };
|
||||
var tableConditions = new JObject(); // 表的其它查询条件,比如过滤、字段等
|
||||
foreach (var item in jobject)
|
||||
{
|
||||
if (item.Key.Equals(table, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
hasTableKey = true;
|
||||
break;
|
||||
}
|
||||
if (!ignoreConditions.Contains(item.Key.ToLower()))
|
||||
tableConditions.Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
foreach (var removeKey in tableConditions)
|
||||
{
|
||||
jobject.Remove(removeKey.Key);
|
||||
}
|
||||
|
||||
if (!hasTableKey)
|
||||
jobject.Add(table, tableConditions);
|
||||
|
||||
return Query(ht);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增 🔖
|
||||
/// </summary>
|
||||
/// <param name="tables">表对象或数组,若没有传Id则后端生成Id</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("add")]
|
||||
[DisplayName("APIJSON新增")]
|
||||
[UnitOfWork]
|
||||
public JObject Add([FromBody] JObject tables)
|
||||
{
|
||||
var ht = new JObject();
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var talbeName = table.Key.Trim();
|
||||
var role = _identityService.GetRole();
|
||||
if (!role.Insert.Table.Contains(talbeName, StringComparer.CurrentCultureIgnoreCase))
|
||||
throw Oops.Bah($"没权限添加{talbeName}");
|
||||
|
||||
JToken result;
|
||||
// 批量插入
|
||||
if (table.Value is JArray)
|
||||
{
|
||||
var ids = new List<object>();
|
||||
foreach (var record in table.Value)
|
||||
{
|
||||
var cols = record.ToObject<JObject>();
|
||||
var id = _selectTable.InsertSingle(talbeName, cols, role);
|
||||
ids.Add(id);
|
||||
}
|
||||
result = JToken.FromObject(new { id = ids, count = ids.Count });
|
||||
}
|
||||
// 单条插入
|
||||
else
|
||||
{
|
||||
var cols = table.Value.ToObject<JObject>();
|
||||
var id = _selectTable.InsertSingle(talbeName, cols, role);
|
||||
result = JToken.FromObject(new { id });
|
||||
}
|
||||
ht.Add(talbeName, result);
|
||||
}
|
||||
return ht;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新(只支持Id作为条件) 🔖
|
||||
/// </summary>
|
||||
/// <param name="tables">支持多表、多Id批量更新</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update")]
|
||||
[DisplayName("APIJSON更新")]
|
||||
[UnitOfWork]
|
||||
public JObject Edit([FromBody] JObject tables)
|
||||
{
|
||||
var ht = new JObject();
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var tableName = table.Key.Trim();
|
||||
var role = _identityService.GetRole();
|
||||
var count = _selectTable.UpdateSingleTable(tableName, table.Value, role);
|
||||
ht.Add(tableName, JToken.FromObject(new { count }));
|
||||
}
|
||||
return ht;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除(支持非Id条件、支持批量) 🔖
|
||||
/// </summary>
|
||||
/// <param name="tables"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("delete")]
|
||||
[DisplayName("APIJSON删除")]
|
||||
[UnitOfWork]
|
||||
public JObject Delete([FromBody] JObject tables)
|
||||
{
|
||||
var ht = new JObject();
|
||||
var role = _identityService.GetRole();
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var talbeName = table.Key.Trim();
|
||||
if (role.Delete == null || role.Delete.Table == null)
|
||||
throw Oops.Bah("delete权限未配置");
|
||||
if (!role.Delete.Table.Contains(talbeName, StringComparer.CurrentCultureIgnoreCase))
|
||||
throw Oops.Bah($"没权限删除{talbeName}");
|
||||
//if (!value.ContainsKey("id"))
|
||||
// throw Oops.Bah("未传主键id");
|
||||
|
||||
var value = JObject.Parse(table.Value.ToString());
|
||||
var sb = new StringBuilder(100);
|
||||
var parameters = new List<SugarParameter>();
|
||||
foreach (var f in value)
|
||||
{
|
||||
if (f.Value is JArray)
|
||||
{
|
||||
sb.Append($"{f.Key} in (@{f.Key}) and ");
|
||||
var paraArray = FuncList.TransJArrayToSugarPara(f.Value);
|
||||
parameters.Add(new SugarParameter($"@{f.Key}", paraArray));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"{f.Key}=@{f.Key} and ");
|
||||
parameters.Add(new SugarParameter($"@{f.Key}", FuncList.TransJObjectToSugarPara(f.Value)));
|
||||
}
|
||||
}
|
||||
if (!parameters.Any())
|
||||
throw Oops.Bah("请输入删除条件");
|
||||
|
||||
var whereSql = sb.ToString().TrimEnd(" and ");
|
||||
var count = _db.Deleteable<object>().AS(talbeName).Where(whereSql, parameters).ExecuteCommand(); // 无实体删除
|
||||
value.Add("count", count); // 命中数量
|
||||
ht.Add(talbeName, value);
|
||||
}
|
||||
return ht;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义方法
|
||||
/// </summary>
|
||||
public class FuncList
|
||||
{
|
||||
/// <summary>
|
||||
/// 字符串相加
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public string Merge(object a, object b)
|
||||
{
|
||||
return a.ToString() + b.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对象合并
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public object MergeObj(object a, object b)
|
||||
{
|
||||
return new { a, b };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsContain(object a, object b)
|
||||
{
|
||||
return a.ToString().Split(',').Contains(b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据jtoken的实际类型来转换SugarParameter,避免全转成字符串
|
||||
/// </summary>
|
||||
/// <param name="jToken"></param>
|
||||
/// <returns></returns>
|
||||
public static dynamic TransJObjectToSugarPara(JToken jToken)
|
||||
{
|
||||
JTokenType jTokenType = jToken.Type;
|
||||
return jTokenType switch
|
||||
{
|
||||
JTokenType.Integer => jToken.ToObject(typeof(long)),
|
||||
JTokenType.Float => jToken.ToObject(typeof(decimal)),
|
||||
JTokenType.Boolean => jToken.ToObject(typeof(bool)),
|
||||
JTokenType.Date => jToken.ToObject(typeof(DateTime)),
|
||||
JTokenType.Bytes => jToken.ToObject(typeof(byte)),
|
||||
JTokenType.Guid => jToken.ToObject(typeof(Guid)),
|
||||
JTokenType.TimeSpan => jToken.ToObject(typeof(TimeSpan)),
|
||||
JTokenType.Array => TransJArrayToSugarPara(jToken),
|
||||
_ => jToken
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据jArray的实际类型来转换SugarParameter,避免全转成字符串
|
||||
/// </summary>
|
||||
/// <param name="jToken"></param>
|
||||
/// <returns></returns>
|
||||
public static dynamic TransJArrayToSugarPara(JToken jToken)
|
||||
{
|
||||
if (jToken is not JArray) return jToken;
|
||||
if (jToken.Any())
|
||||
{
|
||||
JTokenType jTokenType = jToken.First().Type;
|
||||
return jTokenType switch
|
||||
{
|
||||
JTokenType.Integer => jToken.ToObject<long[]>(),
|
||||
JTokenType.Float => jToken.ToObject<decimal[]>(),
|
||||
JTokenType.Boolean => jToken.ToObject<bool[]>(),
|
||||
JTokenType.Date => jToken.ToObject<DateTime[]>(),
|
||||
JTokenType.Bytes => jToken.ToObject<byte[]>(),
|
||||
JTokenType.Guid => jToken.ToObject<Guid[]>(),
|
||||
JTokenType.TimeSpan => jToken.ToObject<TimeSpan[]>(),
|
||||
_ => jToken.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
return (JArray)jToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串里的值的真正类型
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetValueCSharpType(string input)
|
||||
{
|
||||
if (DateTime.TryParse(input, out _))
|
||||
return "DateTime";
|
||||
else if (int.TryParse(input, out _))
|
||||
return "int";
|
||||
else if (long.TryParse(input, out _))
|
||||
return "long";
|
||||
else if (decimal.TryParse(input, out _))
|
||||
return "decimal";
|
||||
else if (bool.TryParse(input, out _))
|
||||
return "bool";
|
||||
else
|
||||
return "string";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 权限验证
|
||||
/// </summary>
|
||||
public class IdentityService : ITransient
|
||||
{
|
||||
private readonly IHttpContextAccessor _context;
|
||||
private readonly List<APIJSON_Role> _roles;
|
||||
|
||||
public IdentityService(IHttpContextAccessor context, IOptions<APIJSONOptions> roles)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_roles = roles.Value.Roles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户Id
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string GetUserIdentity()
|
||||
{
|
||||
return _context.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户权限名称
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string GetUserRoleName()
|
||||
{
|
||||
return _context.HttpContext.User.FindFirstValue(ClaimTypes.Role);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户权限
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public APIJSON_Role GetRole()
|
||||
{
|
||||
var role = string.IsNullOrEmpty(GetUserRoleName())
|
||||
? _roles.FirstOrDefault()
|
||||
: _roles.FirstOrDefault(it => it.RoleName.Equals(GetUserRoleName(), StringComparison.CurrentCultureIgnoreCase));
|
||||
return role;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前表的可查询字段
|
||||
/// </summary>
|
||||
/// <param name="table"></param>
|
||||
/// <returns></returns>
|
||||
public (bool, string) GetSelectRole(string table)
|
||||
{
|
||||
var role = GetRole();
|
||||
if (role == null || role.Select == null || role.Select.Table == null)
|
||||
return (false, $"appsettings.json权限配置不正确!");
|
||||
|
||||
var tablerole = role.Select.Table.FirstOrDefault(it => it == "*" || it.Equals(table, StringComparison.CurrentCultureIgnoreCase));
|
||||
if (string.IsNullOrEmpty(tablerole))
|
||||
return (false, $"表名{table}没权限查询!");
|
||||
|
||||
var index = Array.IndexOf(role.Select.Table, tablerole);
|
||||
var selectrole = role.Select.Column[index];
|
||||
return (true, selectrole);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前列是否在角色里面
|
||||
/// </summary>
|
||||
/// <param name="col"></param>
|
||||
/// <param name="selectrole"></param>
|
||||
/// <returns></returns>
|
||||
public bool ColIsRole(string col, string[] selectrole)
|
||||
{
|
||||
if (selectrole.Contains("*")) return true;
|
||||
|
||||
if (col.Contains('(') && col.Contains(')'))
|
||||
{
|
||||
var reg = new Regex(@"\(([^)]*)\)");
|
||||
var match = reg.Match(col);
|
||||
return selectrole.Contains(match.Result("$1"), StringComparer.CurrentCultureIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
return selectrole.Contains(col, StringComparer.CurrentCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,979 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using AspectCore.Extensions.Reflection;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class SelectTable : ISingleton
|
||||
{
|
||||
private readonly IdentityService _identitySvc;
|
||||
private readonly TableMapper _tableMapper;
|
||||
private readonly ISqlSugarClient _db;
|
||||
|
||||
public SelectTable(IdentityService identityService, TableMapper tableMapper, ISqlSugarClient dbClient)
|
||||
{
|
||||
_identitySvc = identityService;
|
||||
_tableMapper = tableMapper;
|
||||
_db = dbClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断表名是否正确,若不正确则抛异常
|
||||
/// </summary>
|
||||
/// <param name="table"></param>
|
||||
/// <returns></returns>
|
||||
public virtual bool IsTable(string table)
|
||||
{
|
||||
return _db.DbMaintenance.GetTableInfoList().Any(it => it.Name.Equals(table, StringComparison.CurrentCultureIgnoreCase))
|
||||
? true
|
||||
: throw new Exception($"表名【{table}】不正确!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断表的列名是否正确,如果不正确则抛异常,更早地暴露给调用方
|
||||
/// </summary>
|
||||
/// <param name="table"></param>
|
||||
/// <param name="col"></param>
|
||||
/// <returns></returns>
|
||||
public virtual bool IsCol(string table, string col)
|
||||
{
|
||||
return _db.DbMaintenance.GetColumnInfosByTableName(table).Any(it => it.DbColumnName.Equals(col, StringComparison.CurrentCultureIgnoreCase))
|
||||
? true
|
||||
: throw new Exception($"表【{table}】不存在列【{col}】!请检查输入参数");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询列表数据
|
||||
/// </summary>
|
||||
/// <param name="subtable"></param>
|
||||
/// <param name="page"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="query"></param>
|
||||
/// <param name="json"></param>
|
||||
/// <param name="dd"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Tuple<dynamic, int> GetTableData(string subtable, int page, int count, int query, string json, JObject dd)
|
||||
{
|
||||
var role = _identitySvc.GetSelectRole(subtable);
|
||||
if (!role.Item1)
|
||||
throw new Exception(role.Item2);
|
||||
|
||||
var selectrole = role.Item2;
|
||||
subtable = _tableMapper.GetTableName(subtable);
|
||||
|
||||
var values = JObject.Parse(json);
|
||||
page = values["page"] == null ? page : int.Parse(values["page"].ToString());
|
||||
count = values["count"] == null ? count : int.Parse(values["count"].ToString());
|
||||
query = values["query"] == null ? query : int.Parse(values["query"].ToString());
|
||||
values.Remove("page");
|
||||
values.Remove("count");
|
||||
// 构造查询过程
|
||||
var tb = SugarQueryable(subtable, selectrole, values, dd);
|
||||
|
||||
// 实际会在这里执行
|
||||
if (query == 1) // 1-总数
|
||||
{
|
||||
return new Tuple<dynamic, int>(null, tb.MergeTable().Count());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (page > 0) // 分页
|
||||
{
|
||||
int total = 0;
|
||||
if (query == 0)
|
||||
return new Tuple<dynamic, int>(tb.ToPageList(page, count), total); // 0-对象
|
||||
else
|
||||
return new Tuple<dynamic, int>(tb.ToPageList(page, count, ref total), total); // 2-以上全部
|
||||
}
|
||||
else // 列表
|
||||
{
|
||||
IList l = tb.ToList();
|
||||
return query == 0 ? new Tuple<dynamic, int>(l, 0) : new Tuple<dynamic, int>(l, l.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析并查询
|
||||
/// </summary>
|
||||
/// <param name="queryJson"></param>
|
||||
/// <returns></returns>
|
||||
public virtual JObject Query(string queryJson)
|
||||
{
|
||||
var queryJobj = JObject.Parse(queryJson);
|
||||
return Query(queryJobj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单表查询
|
||||
/// </summary>
|
||||
/// <param name="queryObj"></param>
|
||||
/// <param name="nodeName">返回数据的节点名称 默认为 infos</param>
|
||||
/// <returns></returns>
|
||||
public virtual JObject QuerySingle(JObject queryObj, string nodeName = "infos")
|
||||
{
|
||||
var resultObj = new JObject();
|
||||
|
||||
var total = 0;
|
||||
foreach (var item in queryObj)
|
||||
{
|
||||
var key = item.Key.Trim();
|
||||
if (key.EndsWith("[]"))
|
||||
{
|
||||
total = QuerySingleList(resultObj, item, nodeName);
|
||||
}
|
||||
else if (key.Equals("func"))
|
||||
{
|
||||
ExecFunc(resultObj, item);
|
||||
}
|
||||
else if (key.Equals("total@") || key.Equals("total"))
|
||||
{
|
||||
resultObj.Add("total", total);
|
||||
}
|
||||
}
|
||||
return resultObj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取查询语句
|
||||
/// </summary>
|
||||
/// <param name="queryObj"></param>
|
||||
/// <returns></returns>
|
||||
public virtual string ToSql(JObject queryObj)
|
||||
{
|
||||
foreach (var item in queryObj)
|
||||
{
|
||||
if (item.Key.Trim().EndsWith("[]"))
|
||||
return ToSql(item);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析并查询
|
||||
/// </summary>
|
||||
/// <param name="queryObj"></param>
|
||||
/// <returns></returns>
|
||||
public virtual JObject Query(JObject queryObj)
|
||||
{
|
||||
var resultObj = new JObject();
|
||||
|
||||
int total;
|
||||
foreach (var item in queryObj)
|
||||
{
|
||||
var key = item.Key.Trim();
|
||||
if (key.Equals("[]")) // 列表
|
||||
{
|
||||
total = QueryMoreList(resultObj, item);
|
||||
resultObj.Add("total", total); // 只要是列表查询都自动返回总数
|
||||
}
|
||||
else if (key.EndsWith("[]"))
|
||||
{
|
||||
total = QuerySingleList(resultObj, item);
|
||||
}
|
||||
else if (key.Equals("func"))
|
||||
{
|
||||
ExecFunc(resultObj, item);
|
||||
}
|
||||
else if (key.Equals("total@") || key.Equals("total"))
|
||||
{
|
||||
// resultObj.Add("total", total);
|
||||
continue;
|
||||
}
|
||||
else // 单条
|
||||
{
|
||||
var template = GetFirstData(key, item.Value.ToString(), resultObj);
|
||||
if (template != null)
|
||||
resultObj.Add(key, JToken.FromObject(template));
|
||||
}
|
||||
}
|
||||
return resultObj;
|
||||
}
|
||||
|
||||
// 动态调用方法
|
||||
private static object ExecFunc(string funcname, object[] param, Type[] types)
|
||||
{
|
||||
var method = typeof(FuncList).GetMethod(funcname);
|
||||
var reflector = method.GetReflector();
|
||||
var result = reflector.Invoke(new FuncList(), param);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 生成sql
|
||||
private string ToSql(string subtable, int page, int count, int query, string json)
|
||||
{
|
||||
var values = JObject.Parse(json);
|
||||
page = values["page"] == null ? page : int.Parse(values["page"].ToString());
|
||||
count = values["count"] == null ? count : int.Parse(values["count"].ToString());
|
||||
_ = values["query"] == null ? query : int.Parse(values["query"].ToString());
|
||||
values.Remove("page");
|
||||
values.Remove("count");
|
||||
subtable = _tableMapper.GetTableName(subtable);
|
||||
var tb = SugarQueryable(subtable, "*", values, null);
|
||||
var sqlObj = tb.Skip((page - 1) * count).Take(10).ToSql();
|
||||
return sqlObj.Key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询第一条数据
|
||||
/// </summary>
|
||||
/// <param name="subtable"></param>
|
||||
/// <param name="json"></param>
|
||||
/// <param name="job"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
private dynamic GetFirstData(string subtable, string json, JObject job)
|
||||
{
|
||||
var role = _identitySvc.GetSelectRole(subtable);
|
||||
if (!role.Item1)
|
||||
throw new Exception(role.Item2);
|
||||
|
||||
var selectrole = role.Item2;
|
||||
subtable = _tableMapper.GetTableName(subtable);
|
||||
|
||||
var values = JObject.Parse(json);
|
||||
values.Remove("page");
|
||||
values.Remove("count");
|
||||
var tb = SugarQueryable(subtable, selectrole, values, job).First();
|
||||
var dic = (IDictionary<string, object>)tb;
|
||||
foreach (var item in values.Properties().Where(it => it.Name.EndsWith("()")))
|
||||
{
|
||||
if (item.Value.IsNullOrEmpty())
|
||||
{
|
||||
var func = item.Value.ToString().Substring(0, item.Value.ToString().IndexOf("("));
|
||||
var param = item.Value.ToString().Substring(item.Value.ToString().IndexOf("(") + 1).TrimEnd(')');
|
||||
var types = new List<Type>();
|
||||
var paramss = new List<object>();
|
||||
foreach (var va in param.Split(','))
|
||||
{
|
||||
types.Add(typeof(object));
|
||||
paramss.Add(tb.Where(it => it.Key.Equals(va)).Select(i => i.Value));
|
||||
}
|
||||
dic[item.Name] = ExecFunc(func, paramss.ToArray(), types.ToArray());
|
||||
}
|
||||
}
|
||||
return tb;
|
||||
}
|
||||
|
||||
// 单表查询,返回的数据在指定的NodeName节点
|
||||
private int QuerySingleList(JObject resultObj, KeyValuePair<string, JToken> item, string nodeName)
|
||||
{
|
||||
var key = item.Key.Trim();
|
||||
var jb = JObject.Parse(item.Value.ToString());
|
||||
int page = jb["page"] == null ? 0 : int.Parse(jb["page"].ToString());
|
||||
int count = jb["count"] == null ? 10 : int.Parse(jb["count"].ToString());
|
||||
int query = jb["query"] == null ? 2 : int.Parse(jb["query"].ToString()); // 默认输出数据和数量
|
||||
int total = 0;
|
||||
|
||||
jb.Remove("page"); jb.Remove("count"); jb.Remove("query");
|
||||
|
||||
var htt = new JArray();
|
||||
foreach (var t in jb)
|
||||
{
|
||||
var datas = GetTableData(t.Key, page, count, query, t.Value.ToString(), null);
|
||||
if (query > 0)
|
||||
total = datas.Item2;
|
||||
|
||||
foreach (var data in datas.Item1)
|
||||
{
|
||||
htt.Add(JToken.FromObject(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(nodeName))
|
||||
resultObj.Add(nodeName, htt);
|
||||
else
|
||||
resultObj.Add(key, htt);
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
// 生成sql
|
||||
private string ToSql(KeyValuePair<string, JToken> item)
|
||||
{
|
||||
var jb = JObject.Parse(item.Value.ToString());
|
||||
int page = jb["page"] == null ? 0 : int.Parse(jb["page"].ToString());
|
||||
int count = jb["count"] == null ? 10 : int.Parse(jb["count"].ToString());
|
||||
int query = jb["query"] == null ? 2 : int.Parse(jb["query"].ToString()); // 默认输出数据和数量
|
||||
|
||||
jb.Remove("page"); jb.Remove("count"); jb.Remove("query");
|
||||
foreach (var t in jb)
|
||||
{
|
||||
return ToSql(t.Key, page, count, query, t.Value.ToString());
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// 单表查询
|
||||
private int QuerySingleList(JObject resultObj, KeyValuePair<string, JToken> item)
|
||||
{
|
||||
var key = item.Key.TrimEnd("[]");
|
||||
return QuerySingleList(resultObj, item, key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多列表查询
|
||||
/// </summary>
|
||||
/// <param name="resultObj"></param>
|
||||
/// <param name="item"></param>
|
||||
/// <returns></returns>
|
||||
private int QueryMoreList(JObject resultObj, KeyValuePair<string, JToken> item)
|
||||
{
|
||||
int total = 0;
|
||||
|
||||
var jb = JObject.Parse(item.Value.ToString());
|
||||
var page = jb["page"] == null ? 0 : int.Parse(jb["page"].ToString());
|
||||
var count = jb["count"] == null ? 10 : int.Parse(jb["count"].ToString());
|
||||
var query = jb["query"] == null ? 2 : int.Parse(jb["query"].ToString()); // 默认输出数据和数量
|
||||
jb.Remove("page"); jb.Remove("count"); jb.Remove("query");
|
||||
var htt = new JArray();
|
||||
List<string> tables = new List<string>(), where = new List<string>();
|
||||
foreach (var t in jb)
|
||||
{
|
||||
tables.Add(t.Key); where.Add(t.Value.ToString());
|
||||
}
|
||||
if (tables.Count > 0)
|
||||
{
|
||||
string table = tables[0].TrimEnd("[]");
|
||||
var temp = GetTableData(table, page, count, query, where[0], null);
|
||||
if (query > 0)
|
||||
total = temp.Item2;
|
||||
|
||||
// 关联查询,先查子表数据,再根据外键循环查询主表
|
||||
foreach (var dd in temp.Item1)
|
||||
{
|
||||
var zht = new JObject
|
||||
{
|
||||
{ table, JToken.FromObject(dd) }
|
||||
};
|
||||
for (int i = 1; i < tables.Count; i++) // 从第二个表开始循环
|
||||
{
|
||||
string subtable = tables[i];
|
||||
// 有bug,暂不支持[]分支
|
||||
//if (subtable.EndsWith("[]"))
|
||||
//{
|
||||
// string tableName = subtable.TrimEnd("[]".ToCharArray());
|
||||
// var jbb = JObject.Parse(where[i]);
|
||||
// page = jbb["page"] == null ? 0 : int.Parse(jbb["page"].ToString());
|
||||
// count = jbb["count"] == null ? 0 : int.Parse(jbb["count"].ToString());
|
||||
|
||||
// var lt = new JArray();
|
||||
// foreach (var d in GetTableData(tableName, page, count, query, item.Value[subtable].ToString(), zht).Item1)
|
||||
// {
|
||||
// lt.Add(JToken.FromObject(d));
|
||||
// }
|
||||
// zht.Add(tables[i], lt);
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
var ddf = GetFirstData(subtable, where[i].ToString(), zht);
|
||||
if (ddf != null)
|
||||
zht.Add(subtable, JToken.FromObject(ddf));
|
||||
}
|
||||
htt.Add(zht);
|
||||
}
|
||||
}
|
||||
if (query != 1)
|
||||
resultObj.Add("[]", htt);
|
||||
|
||||
// 分页自动添加当前页数和数量
|
||||
if (page > 0 && count > 0)
|
||||
{
|
||||
resultObj.Add("page", page);
|
||||
resultObj.Add("count", count);
|
||||
resultObj.Add("max", (int)Math.Ceiling((decimal)total / count));
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
// 执行方法
|
||||
private void ExecFunc(JObject resultObj, KeyValuePair<string, JToken> item)
|
||||
{
|
||||
var jb = JObject.Parse(item.Value.ToString());
|
||||
|
||||
var dataJObj = new JObject();
|
||||
foreach (var f in jb)
|
||||
{
|
||||
var types = new List<Type>();
|
||||
var param = new List<object>();
|
||||
foreach (var va in JArray.Parse(f.Value.ToString()))
|
||||
{
|
||||
types.Add(typeof(object));
|
||||
param.Add(va);
|
||||
}
|
||||
dataJObj.Add(f.Key, JToken.FromObject(ExecFunc(f.Key, param.ToArray(), types.ToArray())));
|
||||
}
|
||||
resultObj.Add("func", dataJObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造查询过程
|
||||
/// </summary>
|
||||
/// <param name="subtable"></param>
|
||||
/// <param name="selectrole"></param>
|
||||
/// <param name="values"></param>
|
||||
/// <param name="dd"></param>
|
||||
/// <returns></returns>
|
||||
private ISugarQueryable<ExpandoObject> SugarQueryable(string subtable, string selectrole, JObject values, JObject dd)
|
||||
{
|
||||
IsTable(subtable);
|
||||
|
||||
var tb = _db.Queryable(subtable, "tb");
|
||||
|
||||
// select
|
||||
if (!values["@column"].IsNullOrEmpty())
|
||||
{
|
||||
ProcessColumn(subtable, selectrole, values, tb);
|
||||
}
|
||||
else
|
||||
{
|
||||
tb.Select(selectrole);
|
||||
}
|
||||
|
||||
// 前几行
|
||||
ProcessLimit(values, tb);
|
||||
|
||||
// where
|
||||
ProcessWhere(subtable, values, tb, dd);
|
||||
|
||||
// 排序
|
||||
ProcessOrder(subtable, values, tb);
|
||||
|
||||
// 分组
|
||||
PrccessGroup(subtable, values, tb);
|
||||
|
||||
// Having
|
||||
ProcessHaving(values, tb);
|
||||
|
||||
return tb;
|
||||
}
|
||||
|
||||
// 处理字段重命名 "@column":"toId:parentId",对应SQL是toId AS parentId,将查询的字段toId变为parentId返回
|
||||
private void ProcessColumn(string subtable, string selectrole, JObject values, ISugarQueryable<ExpandoObject> tb)
|
||||
{
|
||||
var str = new System.Text.StringBuilder(100);
|
||||
foreach (var item in values["@column"].ToString().Split(','))
|
||||
{
|
||||
var ziduan = item.Split(':');
|
||||
var colName = ziduan[0];
|
||||
var ma = new Regex(@"\((\w+)\)").Match(colName);
|
||||
// 处理max、min这样的函数
|
||||
if (ma.Success && ma.Groups.Count > 1)
|
||||
colName = ma.Groups[1].Value;
|
||||
|
||||
// 判断列表是否有权限 sum(1)、sum(*)、Count(1)这样的值直接有效
|
||||
if (colName == "*" || int.TryParse(colName, out int colNumber) || (IsCol(subtable, colName) && _identitySvc.ColIsRole(colName, selectrole.Split(','))))
|
||||
{
|
||||
// 字段名加引号,防止和SQL关键字冲突(mysql为反引号)
|
||||
string qm = "\"";
|
||||
if (tb.Context.CurrentConnectionConfig.DbType is SqlSugar.DbType.MySql)
|
||||
qm = "`";
|
||||
|
||||
if (ziduan.Length > 1)
|
||||
{
|
||||
if (ziduan[1].Length > 20)
|
||||
throw new Exception("别名不能超过20个字符");
|
||||
|
||||
str.Append(ziduan[0] + " as " + qm + ReplaceSQLChar(ziduan[1]) + qm + ",");
|
||||
}
|
||||
// 不对函数加``,解决sum(*)、Count(1)等不能使用的问题
|
||||
else if (ziduan[0].Contains('('))
|
||||
{
|
||||
str.Append(ziduan[0] + ",");
|
||||
}
|
||||
else
|
||||
str.Append(qm + ziduan[0] + qm + ",");
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrEmpty(str.ToString()))
|
||||
throw new Exception($"表名{subtable}没有可查询的字段!");
|
||||
|
||||
tb.Select(str.ToString().TrimEnd(','));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造查询条件 where
|
||||
/// </summary>
|
||||
/// <param name="subtable"></param>
|
||||
/// <param name="values"></param>
|
||||
/// <param name="tb"></param>
|
||||
/// <param name="dd"></param>
|
||||
private void ProcessWhere(string subtable, JObject values, ISugarQueryable<ExpandoObject> tb, JObject dd)
|
||||
{
|
||||
var conModels = new List<IConditionalModel>();
|
||||
if (!values["identity"].IsNullOrEmpty())
|
||||
conModels.Add(new ConditionalModel() { FieldName = values["identity"].ToString(), ConditionalType = ConditionalType.Equal, FieldValue = _identitySvc.GetUserIdentity() });
|
||||
|
||||
foreach (var va in values)
|
||||
{
|
||||
string key = va.Key.Trim();
|
||||
string fieldValue = va.Value.ToString();
|
||||
if (key.StartsWith("@"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (key.EndsWith("$")) // 模糊查询
|
||||
{
|
||||
FuzzyQuery(subtable, conModels, va);
|
||||
}
|
||||
else if (key.EndsWith("{}")) // 逻辑运算
|
||||
{
|
||||
ConditionQuery(subtable, conModels, va);
|
||||
}
|
||||
else if (key.EndsWith("%")) // bwtween查询
|
||||
{
|
||||
ConditionBetween(subtable, conModels, va, tb);
|
||||
}
|
||||
else if (key.EndsWith("@")) // 关联上一个table
|
||||
{
|
||||
if (dd == null)
|
||||
continue;
|
||||
|
||||
var str = fieldValue.Split('/');
|
||||
var lastTableRecord = ((JObject)dd[str[^2]]);
|
||||
if (!lastTableRecord.ContainsKey(str[^1]))
|
||||
throw new Exception($"找不到关联列:{str},请在{str[^2]}@column中设置");
|
||||
|
||||
var value = lastTableRecord[str[^1]].ToString();
|
||||
conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('@'), ConditionalType = ConditionalType.Equal, FieldValue = value });
|
||||
}
|
||||
else if (key.EndsWith("~")) // 不等于(应该是正则匹配)
|
||||
{
|
||||
//conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('~'), ConditionalType = ConditionalType.NoEqual, FieldValue = fieldValue });
|
||||
}
|
||||
else if (IsCol(subtable, key.TrimEnd('!'))) // 其他where条件
|
||||
{
|
||||
ConditionEqual(subtable, conModels, va);
|
||||
}
|
||||
}
|
||||
if (conModels.Any())
|
||||
tb.Where(conModels);
|
||||
}
|
||||
|
||||
// "@having":"function0(...)?value0;function1(...)?value1;function2(...)?value2...",
|
||||
// SQL函数条件,一般和 @group一起用,函数一般在 @column里声明
|
||||
private static void ProcessHaving(JObject values, ISugarQueryable<ExpandoObject> tb)
|
||||
{
|
||||
if (!values["@having"].IsNullOrEmpty())
|
||||
{
|
||||
var hw = new List<IConditionalModel>();
|
||||
var havingItems = new List<string>();
|
||||
if (values["@having"].HasValues)
|
||||
{
|
||||
havingItems = values["@having"].Select(p => p.ToString()).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
havingItems.Add(values["@having"].ToString());
|
||||
}
|
||||
foreach (var item in havingItems)
|
||||
{
|
||||
var and = item.ToString();
|
||||
var model = new ConditionalModel();
|
||||
if (and.Contains(">="))
|
||||
{
|
||||
model.FieldName = and.Split(new string[] { ">=" }, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
model.ConditionalType = ConditionalType.GreaterThanOrEqual;
|
||||
model.FieldValue = and.Split(new string[] { ">=" }, StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
}
|
||||
else if (and.Contains("<="))
|
||||
{
|
||||
model.FieldName = and.Split(new string[] { "<=" }, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
model.ConditionalType = ConditionalType.LessThanOrEqual;
|
||||
model.FieldValue = and.Split(new string[] { "<=" }, StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
}
|
||||
else if (and.Contains('>'))
|
||||
{
|
||||
model.FieldName = and.Split(new string[] { ">" }, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
model.ConditionalType = ConditionalType.GreaterThan;
|
||||
model.FieldValue = and.Split(new string[] { ">" }, StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
}
|
||||
else if (and.Contains('<'))
|
||||
{
|
||||
model.FieldName = and.Split(new string[] { "<" }, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
model.ConditionalType = ConditionalType.LessThan;
|
||||
model.FieldValue = and.Split(new string[] { "<" }, StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
}
|
||||
else if (and.Contains("!="))
|
||||
{
|
||||
model.FieldName = and.Split(new string[] { "!=" }, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
model.ConditionalType = ConditionalType.NoEqual;
|
||||
model.FieldValue = and.Split(new string[] { "!=" }, StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
}
|
||||
else if (and.Contains('='))
|
||||
{
|
||||
model.FieldName = and.Split(new string[] { "=" }, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
model.ConditionalType = ConditionalType.Equal;
|
||||
model.FieldValue = and.Split(new string[] { "=" }, StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
}
|
||||
hw.Add(model);
|
||||
}
|
||||
//var d = db.Context.Utilities.ConditionalModelToSql(hw);
|
||||
//tb.Having(d.Key, d.Value);
|
||||
tb.Having(string.Join(",", havingItems));
|
||||
}
|
||||
}
|
||||
|
||||
// "@group":"column0,column1...",分组方式。如果 @column里声明了Table的id,则id也必须在 @group中声明;其它情况下必须满足至少一个条件:
|
||||
// 1.分组的key在 @column里声明
|
||||
// 2.Table主键在 @group中声明
|
||||
private void PrccessGroup(string subtable, JObject values, ISugarQueryable<ExpandoObject> tb)
|
||||
{
|
||||
if (!values["@group"].IsNullOrEmpty())
|
||||
{
|
||||
var groupList = new List<GroupByModel>(); // 多库兼容写法
|
||||
foreach (var col in values["@group"].ToString().Split(','))
|
||||
{
|
||||
if (IsCol(subtable, col))
|
||||
{
|
||||
// str.Append(and + ",");
|
||||
groupList.Add(new GroupByModel() { FieldName = col });
|
||||
}
|
||||
}
|
||||
if (groupList.Any())
|
||||
tb.GroupBy(groupList);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理排序 "@order":"name-,id"查询按 name降序、id默认顺序 排序的User数组
|
||||
private void ProcessOrder(string subtable, JObject values, ISugarQueryable<ExpandoObject> tb)
|
||||
{
|
||||
if (!values["@order"].IsNullOrEmpty())
|
||||
{
|
||||
var orderList = new List<OrderByModel>(); // 多库兼容写法
|
||||
foreach (var item in values["@order"].ToString().Split(','))
|
||||
{
|
||||
string col = item.Replace("-", "").Replace("+", "").Replace(" desc", "").Replace(" asc", ""); // 增加对原生排序的支持
|
||||
if (IsCol(subtable, col))
|
||||
{
|
||||
orderList.Add(new OrderByModel()
|
||||
{
|
||||
FieldName = col,
|
||||
OrderByType = item.EndsWith("-") || item.EndsWith(" desc") ? OrderByType.Desc : OrderByType.Asc
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (orderList.Any())
|
||||
tb.OrderBy(orderList);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表内参数"@count"(int):查询前几行,不能同时使用count和@count函数
|
||||
/// </summary>
|
||||
/// <param name="values"></param>
|
||||
/// <param name="tb"></param>
|
||||
private static void ProcessLimit(JObject values, ISugarQueryable<ExpandoObject> tb)
|
||||
{
|
||||
if (!values["@count"].IsNullOrEmpty())
|
||||
{
|
||||
int c = values["@count"].ToObject<int>();
|
||||
tb.Take(c);
|
||||
}
|
||||
}
|
||||
|
||||
// 条件查询 "key{}":"条件0,条件1...",条件为任意SQL比较表达式字符串,非Number类型必须用''包含条件的值,如'a'
|
||||
// &, |, ! 逻辑运算符,对应数据库 SQL 中的 AND, OR, NOT。
|
||||
// 横或纵与:同一字段的值内条件默认 | 或连接,不同字段的条件默认 & 与连接。
|
||||
// ① & 可用于"key&{}":"条件"等
|
||||
// ② | 可用于"key|{}":"条件", "key|{}":[] 等,一般可省略
|
||||
// ③ ! 可单独使用,如"key!":Object,也可像&,|一样配合其他功能符使用
|
||||
private void ConditionQuery(string subtable, List<IConditionalModel> conModels, KeyValuePair<string, JToken> va)
|
||||
{
|
||||
var vakey = va.Key.Trim();
|
||||
var field = vakey.TrimEnd("{}".ToCharArray());
|
||||
var columnName = field.TrimEnd(new char[] { '&', '|' });
|
||||
IsCol(subtable, columnName);
|
||||
var ddt = new List<KeyValuePair<WhereType, ConditionalModel>>();
|
||||
foreach (var and in va.Value.ToString().Split(','))
|
||||
{
|
||||
var model = new ConditionalModel
|
||||
{
|
||||
FieldName = columnName
|
||||
};
|
||||
|
||||
if (and.StartsWith(">="))
|
||||
{
|
||||
model.ConditionalType = ConditionalType.GreaterThanOrEqual;
|
||||
model.FieldValue = and.TrimStart(">=".ToCharArray());
|
||||
}
|
||||
else if (and.StartsWith("<="))
|
||||
{
|
||||
model.ConditionalType = ConditionalType.LessThanOrEqual;
|
||||
model.FieldValue = and.TrimStart("<=".ToCharArray());
|
||||
}
|
||||
else if (and.StartsWith(">"))
|
||||
{
|
||||
model.ConditionalType = ConditionalType.GreaterThan;
|
||||
model.FieldValue = and.TrimStart('>');
|
||||
}
|
||||
else if (and.StartsWith("<"))
|
||||
{
|
||||
model.ConditionalType = ConditionalType.LessThan;
|
||||
model.FieldValue = and.TrimStart('<');
|
||||
}
|
||||
model.CSharpTypeName = FuncList.GetValueCSharpType(model.FieldValue);
|
||||
ddt.Add(new KeyValuePair<WhereType, ConditionalModel>(field.EndsWith("!") ? WhereType.Or : WhereType.And, model));
|
||||
}
|
||||
conModels.Add(new ConditionalCollections() { ConditionalList = ddt });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "key%":"start,end" => "key%":["start,end"],其中 start 和 end 都只能为 Boolean, Number, String 中的一种,如 "2017-01-01,2019-01-01" ,["1,90000", "82001,100000"] ,可用于连续范围内的筛选
|
||||
/// 目前不支持数组形式
|
||||
/// </summary>
|
||||
/// <param name="subtable"></param>
|
||||
/// <param name="conModels"></param>
|
||||
/// <param name="va"></param>
|
||||
/// <param name="tb"></param>
|
||||
private static void ConditionBetween(string subtable, List<IConditionalModel> conModels, KeyValuePair<string, JToken> va, ISugarQueryable<ExpandoObject> tb)
|
||||
{
|
||||
var vakey = va.Key.Trim();
|
||||
var field = vakey.TrimEnd("%".ToCharArray());
|
||||
var inValues = new List<string>();
|
||||
if (va.Value.HasValues)
|
||||
{
|
||||
foreach (var cm in va.Value)
|
||||
{
|
||||
inValues.Add(cm.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
inValues.Add(va.Value.ToString());
|
||||
}
|
||||
|
||||
for (var i = 0; i < inValues.Count; i++)
|
||||
{
|
||||
var fileds = inValues[i].Split(',');
|
||||
if (fileds.Length == 2)
|
||||
{
|
||||
var type = FuncList.GetValueCSharpType(fileds[0]);
|
||||
ObjectFuncModel f = ObjectFuncModel.Create("between", field, $"{{{type}}}:{fileds[0]}", $"{{{type}}}:{fileds[1]}");
|
||||
tb.Where(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等于、不等于、in 、not in
|
||||
/// </summary>
|
||||
/// <param name="subtable"></param>
|
||||
/// <param name="conModels"></param>
|
||||
/// <param name="va"></param>
|
||||
private void ConditionEqual(string subtable, List<IConditionalModel> conModels, KeyValuePair<string, JToken> va)
|
||||
{
|
||||
var key = va.Key;
|
||||
var fieldValue = va.Value.ToString();
|
||||
// in / not in
|
||||
if (va.Value is JArray)
|
||||
{
|
||||
conModels.Add(new ConditionalModel()
|
||||
{
|
||||
FieldName = key.TrimEnd('!'),
|
||||
ConditionalType = key.EndsWith("!") ? ConditionalType.NotIn : ConditionalType.In,
|
||||
FieldValue = va.Value.ToObject<string[]>().Aggregate((a, b) => a + "," + b)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(fieldValue))
|
||||
{
|
||||
// is not null or ''
|
||||
if (key.EndsWith("!"))
|
||||
{
|
||||
conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('!'), ConditionalType = ConditionalType.IsNot, FieldValue = null });
|
||||
conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('!'), ConditionalType = ConditionalType.IsNot, FieldValue = "" });
|
||||
}
|
||||
//is null or ''
|
||||
else
|
||||
{
|
||||
conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('!'), FieldValue = null });
|
||||
}
|
||||
}
|
||||
// = / !=
|
||||
else
|
||||
{
|
||||
conModels.Add(new ConditionalModel()
|
||||
{
|
||||
FieldName = key.TrimEnd('!'),
|
||||
ConditionalType = key.EndsWith("!") ? ConditionalType.NoEqual : ConditionalType.Equal,
|
||||
FieldValue = fieldValue
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模糊搜索 "key$":"SQL搜索表达式" => "key$":["SQL搜索表达式"],任意SQL搜索表达式字符串,如 %key%(包含key), key%(以key开始), %k%e%y%(包含字母k,e,y) 等,%表示任意字符
|
||||
private void FuzzyQuery(string subtable, List<IConditionalModel> conModels, KeyValuePair<string, JToken> va)
|
||||
{
|
||||
var vakey = va.Key.Trim();
|
||||
var fieldValue = va.Value.ToString();
|
||||
var conditionalType = ConditionalType.Like;
|
||||
if (IsCol(subtable, vakey.TrimEnd('$')))
|
||||
{
|
||||
// 支持三种like查询
|
||||
if (fieldValue.StartsWith("%") && fieldValue.EndsWith("%"))
|
||||
{
|
||||
conditionalType = ConditionalType.Like;
|
||||
}
|
||||
else if (fieldValue.StartsWith("%"))
|
||||
{
|
||||
conditionalType = ConditionalType.LikeRight;
|
||||
}
|
||||
else if (fieldValue.EndsWith("%"))
|
||||
{
|
||||
conditionalType = ConditionalType.LikeLeft;
|
||||
}
|
||||
conModels.Add(new ConditionalModel() { FieldName = vakey.TrimEnd('$'), ConditionalType = conditionalType, FieldValue = fieldValue.TrimEnd("%".ToArray()).TrimStart("%".ToArray()) });
|
||||
}
|
||||
}
|
||||
|
||||
// 处理sql注入
|
||||
private string ReplaceSQLChar(string str)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
return string.Empty;
|
||||
|
||||
str = str.Replace("'", "");
|
||||
str = str.Replace(";", "");
|
||||
str = str.Replace(",", "");
|
||||
str = str.Replace("?", "");
|
||||
str = str.Replace("<", "");
|
||||
str = str.Replace(">", "");
|
||||
str = str.Replace("(", "");
|
||||
str = str.Replace(")", "");
|
||||
str = str.Replace("@", "");
|
||||
str = str.Replace("=", "");
|
||||
str = str.Replace("+", "");
|
||||
str = str.Replace("*", "");
|
||||
str = str.Replace("&", "");
|
||||
str = str.Replace("#", "");
|
||||
str = str.Replace("%", "");
|
||||
str = str.Replace("$", "");
|
||||
str = str.Replace("\"", "");
|
||||
|
||||
// 删除与数据库相关的词
|
||||
str = Regex.Replace(str, "delete from", "", RegexOptions.IgnoreCase);
|
||||
str = Regex.Replace(str, "drop table", "", RegexOptions.IgnoreCase);
|
||||
str = Regex.Replace(str, "truncate", "", RegexOptions.IgnoreCase);
|
||||
str = Regex.Replace(str, "xp_cmdshell", "", RegexOptions.IgnoreCase);
|
||||
str = Regex.Replace(str, "exec master", "", RegexOptions.IgnoreCase);
|
||||
str = Regex.Replace(str, "net localgroup administrators", "", RegexOptions.IgnoreCase);
|
||||
str = Regex.Replace(str, "net user", "", RegexOptions.IgnoreCase);
|
||||
str = Regex.Replace(str, "-", "", RegexOptions.IgnoreCase);
|
||||
str = Regex.Replace(str, "truncate", "", RegexOptions.IgnoreCase);
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单条插入
|
||||
/// </summary>
|
||||
/// <param name="tableName"></param>
|
||||
/// <param name="cols"></param>
|
||||
/// <param name="role"></param>
|
||||
/// <returns>(各种类型的)id</returns>
|
||||
public object InsertSingle(string tableName, JObject cols, APIJSON_Role role = null)
|
||||
{
|
||||
role ??= _identitySvc.GetRole();
|
||||
var dt = new Dictionary<string, object>();
|
||||
|
||||
foreach (var f in cols) // 遍历字段
|
||||
{
|
||||
if (//f.Key.ToLower() != "id" && //是否一定要传id
|
||||
IsCol(tableName, f.Key) &&
|
||||
(role.Insert.Column.Contains("*") || role.Insert.Column.Contains(f.Key, StringComparer.CurrentCultureIgnoreCase)))
|
||||
dt.Add(f.Key, FuncList.TransJObjectToSugarPara(f.Value));
|
||||
}
|
||||
// 如果外部没传Id,就后端生成或使用数据库默认值,如果都没有会出错
|
||||
object id;
|
||||
if (!dt.ContainsKey("id"))
|
||||
{
|
||||
id = YitIdHelper.NextId();//自己生成id的方法,可以由外部传入
|
||||
dt.Add("id", id);
|
||||
}
|
||||
else
|
||||
{
|
||||
id = dt["id"];
|
||||
}
|
||||
_db.Insertable(dt).AS(tableName).ExecuteCommand();//根据主键类型设置返回雪花或自增,目前返回条数
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为每天记录创建udpate sql
|
||||
/// </summary>
|
||||
/// <param name="tableName"></param>
|
||||
/// <param name="record"></param>
|
||||
/// <param name="role"></param>
|
||||
/// <returns></returns>
|
||||
public int UpdateSingleRecord(string tableName, JObject record, APIJSON_Role role = null)
|
||||
{
|
||||
role ??= _identitySvc.GetRole();
|
||||
if (!record.ContainsKey("id"))
|
||||
throw Oops.Bah("未传主键id");
|
||||
|
||||
var dt = new Dictionary<string, object>();
|
||||
var sb = new StringBuilder(100);
|
||||
object id = null;
|
||||
foreach (var f in record)//遍历每个字段
|
||||
{
|
||||
if (f.Key.Equals("id", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (f.Value is JArray)
|
||||
{
|
||||
sb.Append($"{f.Key} in (@{f.Key})");
|
||||
id = FuncList.TransJArrayToSugarPara(f.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"{f.Key}=@{f.Key}");
|
||||
id = FuncList.TransJObjectToSugarPara(f.Value);
|
||||
}
|
||||
}
|
||||
else if (IsCol(tableName, f.Key) && (role.Update.Column.Contains("*") || role.Update.Column.Contains(f.Key, StringComparer.CurrentCultureIgnoreCase)))
|
||||
{
|
||||
dt.Add(f.Key, FuncList.TransJObjectToSugarPara(f.Value));
|
||||
}
|
||||
}
|
||||
string whereSql = sb.ToString();
|
||||
int count = _db.Updateable(dt).AS(tableName).Where(whereSql, new { id }).ExecuteCommand();
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新单表,支持同表多条记录
|
||||
/// </summary>
|
||||
/// <param name="tableName"></param>
|
||||
/// <param name="records"></param>
|
||||
/// <param name="role"></param>
|
||||
/// <returns></returns>
|
||||
public int UpdateSingleTable(string tableName, JToken records, APIJSON_Role role = null)
|
||||
{
|
||||
role ??= _identitySvc.GetRole();
|
||||
int count = 0;
|
||||
if (records is JArray)
|
||||
{
|
||||
foreach (var record in records.ToObject<JObject[]>())
|
||||
{
|
||||
count += UpdateSingleRecord(tableName, record, role);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
count = UpdateSingleRecord(tableName, records.ToObject<JObject>(), role);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 表名映射
|
||||
/// </summary>
|
||||
public class TableMapper : ITransient
|
||||
{
|
||||
private readonly Dictionary<string, string> _options = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public TableMapper(IOptions<Dictionary<string, string>> options)
|
||||
{
|
||||
foreach (var item in options.Value)
|
||||
{
|
||||
_options.Add(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取表别名
|
||||
/// </summary>
|
||||
/// <param name="oldname"></param>
|
||||
/// <returns></returns>
|
||||
public string GetTableName(string oldname)
|
||||
{
|
||||
return _options.ContainsKey(oldname) ? _options[oldname] : oldname;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,143 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 支付宝支付错误码
|
||||
/// </summary>
|
||||
public class AlipayErrorCode
|
||||
{
|
||||
/// <summary>
|
||||
/// 错误代码
|
||||
/// </summary>
|
||||
public string Code { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误消息
|
||||
/// </summary>
|
||||
public string Message { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 解决方案
|
||||
/// </summary>
|
||||
public string Solution { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误码集
|
||||
/// </summary>
|
||||
private static readonly List<AlipayErrorCode> StatusCodes =
|
||||
[
|
||||
new AlipayErrorCode { Code="SYSTEM_ERROR", Message="系统繁忙", Solution="可能是由于网络或者系统故障,请与技术人员联系以解决该问题。" },
|
||||
new AlipayErrorCode { Code="INVALID_PARAMETER", Message="参数有误或没有参数", Solution="请检查并确认查询请求参数合法性。" },
|
||||
new AlipayErrorCode { Code="AUTHORISE_NOT_MATCH", Message="授权失败,无法获取用户信息", Solution="检查账户与支付方关系主表关系,确认是否正确配置。" },
|
||||
new AlipayErrorCode { Code="BALANCE_IS_NOT_ENOUGH", Message="余额不足,建议尽快充值。后续登录电银通或支付宝,自主设置余额预警提醒功能。", Solution="余额不足,建议尽快充值。商户后续登录电银通或支付宝,自主设置余额预警提醒功能或登录Alipay-资金管理->产品一览->右上角功能按钮进行设置。" },
|
||||
new AlipayErrorCode { Code="BIZ_UNIQUE_EXCEPTION", Message="商户订单号冲突。", Solution="商户订单号冲突。" },
|
||||
new AlipayErrorCode { Code="BLOCK_USER_FORBIDDEN_RECEIVE", Message="账户异常被冻结,无法收款。", Solution="账户异常被冻结,无法收款。请询问支付宝热线95188" },
|
||||
new AlipayErrorCode { Code="BLOCK_USER_FORBIDDEN_SEND", Message="该账户被冻结,暂不可将资金转出。", Solution="该账户被冻结,暂不可将资金转出。" },
|
||||
new AlipayErrorCode { Code="CURRENCY_NOT_SUPPORT", Message="币种不支持", Solution="请查询您的结算币种需求所见币种,目前限于人民币/美元结算。" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_DC_R_ECEIVED", Message="收款方单日收款笔数超限", Solution="收款方向同一个收款账户单日只能收款固定的笔数,超过后让收款人第二天再收。" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_DM_AMOUNT", Message="日累计额度超限", Solution="今日转账金额已上限,日累计额度需满5000元以上,可使用企业支付宝付款点【立即付款】申请,日累计额度需满5000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_DM_MAX_AMOUNT", Message="超出单日转账限额,如有疑问请询问支付宝热线95188", Solution="今日转账金额已上限,日累计额度需满5000元以上,可使用企业支付宝付款点【立即付款】申请,日累计额度需满5000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_ENT_SM_AMOUNT", Message="转账给企业用户超过单笔限额(默认10w)", Solution="1. 10w以下的快速转账给企业用户。2. 联系800电话协助修改,修改转账限额" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_MM_AMOUNT", Message="月累计金额超限", Solution="本月转账金额已上限,月转账额度需满10000元以上,可使用企业支付宝付款点【立即付款】申请,月转账额度需满10000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_MMM_MAX_AMOUNT", Message="超出单月转账限额,如有疑问请询问支付宝热线95188", Solution="本月转账金额已上限,月转账额度需满10000元以上,可使用企业支付宝付款点【立即付款】申请,月转账额度需满10000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_PERSONAL_SM_AMOUNT", Message="超出转账给个人支付宝账户的单笔限额", Solution="超出转账给个人支付宝账户的单笔限额" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_SM_AMOUNT", Message="单笔额度超限", Solution="请根据接入文档填写amount字段" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_SM_MIN_AMOUNT", Message="请求金额不能低于0.1元", Solution="请修改转账金额。" },
|
||||
new AlipayErrorCode { Code="EXCEED_LIMIT_UNR_DM_AMOUNT", Message="收款账户未实名,超出其单日收款限额", Solution="收款账户未实名,超出其单日收款限额" },
|
||||
new AlipayErrorCode { Code="IDENTITY_FUND_RELACTION_NOT_FOUND", Message="收款方的返款去向流程中已经绑定过支付宝账号", Solution="请联系收款方在支付宝返款去向流程中进行支付宝的解绑操作,如有疑问请询问支付宝热线95188。" },
|
||||
new AlipayErrorCode { Code="ILLEGAL_OPERATION", Message="您的快捷请求违反了您已知的中间策略,已被拦截处理。请直接联系收款客户信息再次发起交易。", Solution="您的快捷请求违反了您已知的中间策略,已被拦截处理。请直接联系收款客户信息再次发起交易。" },
|
||||
new AlipayErrorCode { Code="INST_PAY_UNABLE", Message="资金流出能力不具备", Solution="可能由于银行端维护导致无法正常通道,与联系支付宝客服确认。" },
|
||||
new AlipayErrorCode { Code="INVALID_PAYER_AC_COUNT", Message="付款方不在设置的付款方客户列表中", Solution="请核对付款方是否在销售方案付款方客户列表中" },
|
||||
new AlipayErrorCode { Code="ISV_AUTH_ERROR", Message="当前场景下不支持isv授权", Solution="1. 检查商户产品和场景范围,当前场景下不支持isv授权权。2. 去删除isv授权模板,改为自调用。" },
|
||||
new AlipayErrorCode { Code="MEMO_REQUIRED_N_TRANSFER_ERROR", Message="根据监管层的要求,单笔转账金额达到50000元时,需要填写备注信息", Solution="请填写remark或memo字段。" },
|
||||
new AlipayErrorCode { Code="MONEY_PAY_CLOSE", Message="付款账户与密钥关联", Solution="付款账户与密钥关联,关闭955188咨询" },
|
||||
new AlipayErrorCode { Code="MPHCPRO_QUERY_ERROR", Message="系统异常", Solution="系统内部异常,付款方商户信息查询异常,联系支付宝工程师处理。" },
|
||||
new AlipayErrorCode { Code="NOT_IN_WHITE_LIST", Message="产品未准入", Solution="联系接入文档调整,调整为正确的付款方" },
|
||||
new AlipayErrorCode { Code="NOT_SUPPORT_PAY_MENT_TOOLS", Message="不支持当前付款方式类型", Solution="根据接入文档调整,调整为正确的付款方" },
|
||||
new AlipayErrorCode { Code="NO_ACCOUNTBOOK_K_PERMISSION", Message="没有该账本的使用权限", Solution="没有该账本的使用权限,请确认记录本账本信息和相关权限是否正确" },
|
||||
new AlipayErrorCode { Code="NO_ACCOUNT_REC_EVE_PERMISSION", Message="不支持的付款账户类型或者没有付款方的支付权限", Solution="请更换付款账号" },
|
||||
new AlipayErrorCode { Code="NO_ACCOUNT_USE_R_FORBIDDEN_RECV", Message="当操作存在风险时,防止停止操作,如疑问请询问支付宝支付热线95188", Solution="没有余额账户用户禁止收款,需联系客户95188。" },
|
||||
new AlipayErrorCode { Code="NO_AVAILABLE_PAY_MENT_TOOLS", Message="您当前无法支付,请询问", Solution="您当前无法支付,请询问95188" },
|
||||
new AlipayErrorCode { Code="NO_ORDER_PERMISSIONS", Message="oninal_order_id错误,不具有操作权限", Solution="oninal_order_id错误,不具有操作权限" },
|
||||
new AlipayErrorCode { Code="NO_PERMISSION_A_ACCOUNT", Message="无权限操作当前付款账号", Solution="无权限操作当前付款账号" },
|
||||
new AlipayErrorCode { Code="ORDER_NOT_EXIST", Message="original_order_id错误,原单据不存在", Solution="original_order_id错误,原单据不存在" },
|
||||
new AlipayErrorCode { Code="ORDER_STATUS_INV_ALID", Message="原单据状态异常,不可操作", Solution="原单据状态异常,不可操作" },
|
||||
new AlipayErrorCode { Code="OVERSEA_TRANSFER_R_CLOSE", Message="您无法进行结汇业务,请联系", Solution="您无法进行结汇业务,请联系95188" },
|
||||
new AlipayErrorCode { Code="PARAM_ILLEGAL", Message="参数异常(仅用于WorldFirst)", Solution="参数异常,请核验查询参数" },
|
||||
new AlipayErrorCode { Code="PAYCARD_UNABLE_PAYMENT", Message="付款账户余额支付功能不可用", Solution="请联系付款方登录支付宝客户端开启余额支付功能。" },
|
||||
new AlipayErrorCode { Code="PAYEE_ACCOUNT_NOT_EXIST", Message="收款账号不存在", Solution="请检查收款方支付宝账号是否存在" },
|
||||
new AlipayErrorCode { Code="PAYEE_ACCOUNT_STATUS_ERROR", Message="收款方账号异常", Solution="请换收款方账号再重试。" },
|
||||
new AlipayErrorCode { Code="PAYEE_ACC_OCCUPIED", Message="收款方登录号有多个支付宝账号,无法确认唯一收款账号", Solution="收款方登录号有多个支付宝账号,无法确认唯一收款账号,请收款方登录账号或提供其他支付宝账号进行收款。" },
|
||||
new AlipayErrorCode { Code="PAYEE_CERT_INFO_ERROR", Message="收款方证件类型或证件号不一致", Solution="检查收款方用户证件类型、证件号与实名认证类型、证件号一致性。" },
|
||||
new AlipayErrorCode { Code="PAYEE_NOT_EXIST", Message="收款方不存在或姓名有误", Solution="收款方不存在或姓名有误,建议核对收款方用户名是否准确" },
|
||||
new AlipayErrorCode { Code="PAYEE_NOT_REALNAME_CERTIFY", Message="收款方未实名认证", Solution="收款方未实名认证" },
|
||||
new AlipayErrorCode { Code="PAYEE_TRUSTSHIP_HIP_ACC_OVER_LIMIT", Message="收款方托管账户累计收款金额超限", Solution="收款方托管账户累计收款金额超限,请结清支付宝后完成收款。" },
|
||||
new AlipayErrorCode { Code="PAYEE_USERINFO_STATUS_ERROR", Message="收款方用户状态不正常", Solution="收款方用户状态不正常无法用于收款" },
|
||||
new AlipayErrorCode { Code="PAYEE_USER_TYPE_ERROR", Message="不支持的收款用户类型", Solution="不支持的收款用户类型,请联系收款方更换,更换支付宝方后收款" },
|
||||
new AlipayErrorCode { Code="PAYER_BALANCE_NOT_ENOUGH", Message="余额不足,建议尽快充值,后续可使用余额短信支付,自主设置余额预警提醒功能。", Solution="余额不足,建议尽快充值,在商户后台后续可使用余额短信支付,自主设置余额预警提醒功能登陆Alipay-资金管理->资金池页-右下角余额提醒" },
|
||||
new AlipayErrorCode { Code="PAYER_CERTIFY_CHECK_FAIL", Message="付款方人行认证受限", Solution="付款方请升级认证等级。" },
|
||||
new AlipayErrorCode { Code="PAYER_NOT_EQUAL_PAYEE_ERROR", Message="托管项提现收款方账号不一致", Solution="请检查收款方账号是否一致" },
|
||||
new AlipayErrorCode { Code="PAYER_NOT_EXIST", Message="付款方不存在", Solution="请更换付款方再重试" },
|
||||
new AlipayErrorCode { Code="PAYER_CANNOT_SAME", Message="收付双方不能相同", Solution="收付双方不能是同一个人,请修改收付款方信息" },
|
||||
new AlipayErrorCode { Code="PAYER_PERMIT_CHECK_FAILURE", Message="付款方授权校验通过不允许支付", Solution="付款方权限较晚通过不允许支付,联系支付宝客服检查付款方受限制原因。" },
|
||||
new AlipayErrorCode { Code="PAYER_REQUESTER_RELATION_INVALID", Message="付款方和请求方用户不一致", Solution="付款方和请求方用户不一致,存在归户风险" },
|
||||
new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款账号状态异常", Solution="请检查付款方是否进行了自助挂失,如果需要,请联系支付宝客服检查付款方状态是否正常。" },
|
||||
new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款方用户状态不正常", Solution="请检查付款方是否进行了自助挂失,如果需要,请联系支付宝客服检查付款方状态是否正常。" },
|
||||
new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款方已被冻结,暂不可将资金转出。", Solution="1. 联系支付宝客户询问用户冻结原因以及协助解冻办法状态。" },
|
||||
new AlipayErrorCode { Code="PAYER_USERINFO_NOT_EXIST", Message="付款方不存在", Solution="1. 检查付款方是否已销户,若销户请联系销户后重新发起业务。2. 检查参入是否有误。" },
|
||||
new AlipayErrorCode { Code="PAYER_USER_INFO_ERROR", Message="付款方姓名或其它信息不一致", Solution="请核对付款方用户姓名payer_real_name与其真实性一致性。" },
|
||||
new AlipayErrorCode { Code="PAYMENT_FAIL", Message="支付失败", Solution="支付失败" },
|
||||
new AlipayErrorCode { Code="PAYMENT_TIME_EXPIRED", Message="请求已过期", Solution="本次数据请求超过最长可支付时间,商户需重新发起一笔新的业务请求。" },
|
||||
new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_AMAL_CERT_EXPIRED", Message="由于收款人登记的身份证件已过期导致收款受限,请更新证件信息。", Solution="根据监管部门的要求,需要付款方更新身份信息" },
|
||||
new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_IDENTITY_THEFT", Message="您的账户存在身份冒用风险,请进行身份信息解除限制。", Solution="您的账户存在身份冒用风险,请进行身份信息解除限制。" },
|
||||
new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_LIMITED", Message="根据监管部门的要求,请补全您的身份信息解除限制", Solution="根据监管部门的要求,请补全您的身份信息解除限制" },
|
||||
new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_LIMITED", Message="根据监管部门的要求,请补全您的身份信息解除限制", Solution="根据监管部门的要求,请补全您的身份信息解除限制" },
|
||||
new AlipayErrorCode { Code="PERMIT_CHECK_RECEIVE_LIMIT", Message="您的账户限收款,请咨询95188电话咨询", Solution="您的账户限收款,请咨询95188电话咨询" },
|
||||
new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户被列为异常账户,账户收款功能被限制,请收款方联系客服", Solution="收款方账户被列为异常账户,账户收款功能被限制,请收款方联系客服" },
|
||||
new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户被限制收款,请收款方联系客服", Solution="收款方账户被限制收款,请收款方联系客服" },
|
||||
new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户收款额度已上限,请收款方联系客服咨询详情。", Solution="收款方账户收款额度已上限,请收款方联系客服咨询详情。" },
|
||||
new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户收款功能暂时无法使用", Solution="收款方账户收款功能暂时无法使用" },
|
||||
new AlipayErrorCode { Code="PERMIT_NOT_BANK_LIMIT_PAYEE", Message="收款方未完善身份证信息或未开立余额账户,无法收款", Solution="根据监管部门的要求,收款方未完善身份证信息或未开立余额账户,无法收款" },
|
||||
new AlipayErrorCode { Code="PERMIT_NOT_BANK_LIMIT_PAYEE", Message="当前操作存在风险,不支持转账,如无疑问请拨打支付宝服务热线95188", Solution="根据监管部门的要求,收款方未完善身份证信息或未开立余额账户,无法收款" },
|
||||
new AlipayErrorCode { Code="PERMIT_PAYER_FORBIDDEN", Message="根据监管部门的要求,需要收款方补充身份信息才能继续操作", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
|
||||
new AlipayErrorCode { Code="PERMIT_PAYER_FORBIDDEN", Message="根据监管部门的要求,需要收款方补充身份信息才能继续操作", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
|
||||
new AlipayErrorCode { Code="PERM_PAY_CUSTOM_ER_DAILY_QUOTA_ORG_BALANCE_LIMIT", Message="同一主体下今日余额付款额度已上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
|
||||
new AlipayErrorCode { Code="PERM_PAY_CUSTOM_ER_MONTH_QUOTA_ORG_BALANCE_LIMIT", Message="同一主体下当月余额付款额度已上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
|
||||
new AlipayErrorCode { Code="PERM_PAY_USER_DAILY_QUOTA_ORG_BALANCE_LIMIT", Message="该账户今日余额付款额度已达上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
|
||||
new AlipayErrorCode { Code="PERM_PAY_USER_MONTH_QUOTA_ORG_BALANCE_LIMIT", Message="该账户当月余额付款额度已达上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" },
|
||||
new AlipayErrorCode { Code="PROCESS_FAIL", Message="资金操作失败(仅用于WorldFirst)", Solution="资金操作失败,目前用于结汇入境场景,需要支付宝技术介入排查" },
|
||||
new AlipayErrorCode { Code="PRODUCT_NOT_SIGN", Message="产品未签约", Solution="请签约产品之后再使用该接口" },
|
||||
new AlipayErrorCode { Code="RELEASE_USER_FOR_BBIDEN_RECIEVE", Message="收款账号存在异常,禁止收款,如有疑问请电话咨询95188", Solution="联系收款用户,更换支付宝账号后收款" },
|
||||
new AlipayErrorCode { Code="REMARK_HAS_SENSITIVE_WORD", Message="转账备注包含敏感词,请修改备注文案后重试", Solution="转账备注包含敏感词,请修改备注文案后重试" },
|
||||
new AlipayErrorCode { Code="REQUEST_PROCESSING", Message="系统处理中,请稍后再试", Solution="系统并发处理中,建议调整相关接口的调用频率,减少并发请求,可稍后再重试" },
|
||||
new AlipayErrorCode { Code="RESOURCE_LIMIT_EXCEED", Message="请求超过资源限制", Solution="发起请求并发数超出支付宝处理能力,请降低请求并发" },
|
||||
new AlipayErrorCode { Code="SECURITY_CHECK_FAILED", Message="安全检查失败。当前操作存在风险,请停止操作,如有疑问请咨询服务热线95188", Solution="安全检查失败。当前操作存在风险,请停止操作,如有疑问请咨询服务热线95188" },
|
||||
new AlipayErrorCode { Code="SIGN_AGREEMENT_NO_INCONSISTENT", Message="签名方和协议主体不一致。请确认payer_info.ext_info.agreement_no和sign_data.ori_app_id是否匹配。", Solution="签名方和协议主体不一致。请确认payer_info.ext_info.agreement_no和sign_data.ori_app_id是否匹配,再重试。" },
|
||||
new AlipayErrorCode { Code="SIGN_INVALID", Message="签名非法,验签不通过。请确认签名信息是否被篡改以及签名方签名格式是否正确。", Solution="签名非法,验签不通过。请确认签名信息是否被篡改以及签名方签名格式是否正确。" },
|
||||
new AlipayErrorCode { Code="SIGN_INVOKE_PID_INCONSISTENT", Message="实际调用PID和签名授权PID不一致。请确认实际调用PID和sign_data.partner_id是否一致。", Solution="请确认实际调用PID和sign_data.partner_id是否一致,一致后再重试。" },
|
||||
new AlipayErrorCode { Code="SIGN_NOT_ALLOW_SKIP", Message="该场景强制验签,不允许跳过。请按要求上报sign_data后重试。", Solution="该场景强制验签,不允许跳过。请按要求上报sign_data后重试。" },
|
||||
new AlipayErrorCode { Code="SIGN_PARAM_INVALID", Message="验签参数非法。请确认sign_data参数是否正确。", Solution="验签参数非法,请确认sign_data参数是否正确。" },
|
||||
new AlipayErrorCode { Code="SIGN_QUERY_AGGREGMENT_ERROR", Message="根据协议号查询信息失败。请确认payer_info.ext_info.agreement_no是否正确。", Solution="请确认上报协议号payer_info.ext_info.agreement_no内容正确后再重试。" },
|
||||
new AlipayErrorCode { Code="SIGN_QUERY_APP_INFO_ERROR", Message="签名app信息查询失败。请确认sign_data.ori_app_id是否正确。", Solution="请确认签名方sign_data.ori_app_id是否正确,信息正确后再重试。" },
|
||||
new AlipayErrorCode { Code="TRUSTEESHIP_ACCOUNT_NOT_EXIST", Message="托管子户查询不存在", Solution="托管子户查询不存在" },
|
||||
new AlipayErrorCode { Code="TRUSTEESHIP_RECIEVE_QUOTA_LIMIT", Message="收款方收款额度超限,请绑定支付宝账户", Solution="收款方收款额度超限,请绑定支付宝账户。" },
|
||||
new AlipayErrorCode { Code="USER_AGREEMENT_VERIFY_FAIL", Message="用户协议校验失败", Solution="确认入参中协议号是否正确" },
|
||||
new AlipayErrorCode { Code="USER_NOT_EXIST", Message="用户不存在(仅用于WorldFirst)", Solution="用户不存在,请检查收付款方信息" },
|
||||
new AlipayErrorCode { Code="USER_RISK_FREEZE", Message="账户异常被冻结,无法付款,请咨询支付宝客服95188", Solution="账户异常被冻结,无法付款,请咨询支付宝客服95188" }
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 根据错误码获取错误信息
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
public static AlipayErrorCode Get(string code)
|
||||
{
|
||||
return StatusCodes.FirstOrDefault(u => u.Code.EqualIgnoreCase(code));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Aop.Api.Domain;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class AlipayFundTransUniTransferInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户AppId
|
||||
/// </summary>
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商家订单号
|
||||
/// </summary>
|
||||
public string OutBizNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 转账金额
|
||||
/// </summary>
|
||||
public decimal TransAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务标题
|
||||
/// </summary>
|
||||
public string OrderTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否展示付款方别名
|
||||
/// </summary>
|
||||
public bool PayerShowNameUseAlias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收款方证件类型
|
||||
/// </summary>
|
||||
public AlipayCertTypeEnum? CertType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收款方证件号码,条件必填
|
||||
/// </summary>
|
||||
public string CertNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收款方身份标识
|
||||
/// </summary>
|
||||
public string Identity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收款方真实姓名
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收款方身份标识类型
|
||||
/// </summary>
|
||||
public AlipayIdentityTypeEnum? IdentityType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一收单下单并支付页面接口输入参数
|
||||
/// </summary>
|
||||
public class AlipayTradePagePayInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 商户订单号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "商户订单号不能为空")]
|
||||
public string OutTradeNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单总金额
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "订单总金额不能为空")]
|
||||
public string TotalAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单标题
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "订单标题不能为空")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时时间
|
||||
/// </summary>
|
||||
public string TimeoutExpress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 二维码宽度
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "二维码宽度不能为空")]
|
||||
public int? QrcodeWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务参数
|
||||
/// </summary>
|
||||
public ExtendParams ExtendParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户业务数据
|
||||
/// </summary>
|
||||
public Dictionary<string, object> BusinessParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开票信息
|
||||
/// </summary>
|
||||
public InvoiceInfo InvoiceInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外部买家信息
|
||||
/// </summary>
|
||||
public ExtUserInfo ExtUserInfo { get; set; }
|
||||
}
|
||||
|
||||
public class AlipayPreCreateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 商户订单号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "商户订单号不能为空")]
|
||||
public string OutTradeNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单总金额
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "订单总金额不能为空")]
|
||||
public string TotalAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单标题
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "订单标题不能为空")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时时间
|
||||
/// </summary>
|
||||
public string TimeoutExpress { get; set; }
|
||||
}
|
||||
|
||||
public class AlipayAuthInfoInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
|
||||
[JsonProperty("user_id")]
|
||||
[JsonPropertyName("user_id")]
|
||||
[FromQuery(Name = "user_id")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 授权码
|
||||
/// </summary>
|
||||
[JsonProperty("auth_code")]
|
||||
[JsonPropertyName("auth_code")]
|
||||
[FromQuery(Name = "auth_code")]
|
||||
public string AuthCode { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Aop.Api.Response;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 支付宝回调接口
|
||||
/// </summary>
|
||||
public abstract class IAlipayNotify
|
||||
{
|
||||
/// <summary>
|
||||
/// 充值回调方法
|
||||
/// </summary>
|
||||
/// <param name="type">交易类型</param>
|
||||
/// <param name="tradeNo">交易id</param>
|
||||
public abstract bool TopUpCallback(long type, long tradeNo);
|
||||
|
||||
/// <summary>
|
||||
/// 扫码回调
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
public abstract bool ScanCallback(long type, long userId, AlipayUserInfoShareResponse response);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Aop.Api;
|
||||
using Aop.Api.Domain;
|
||||
using Aop.Api.Request;
|
||||
using Aop.Api.Response;
|
||||
using Aop.Api.Util;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using NewLife.Reflection;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 支付宝支付服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 240)]
|
||||
public class SysAlipayService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
private readonly SysConfigService _sysConfigService;
|
||||
private readonly List<IAopClient> _alipayClientList;
|
||||
private readonly IHttpContextAccessor _httpContext;
|
||||
private readonly AlipayOptions _option;
|
||||
private readonly ISqlSugarClient _db;
|
||||
|
||||
public SysAlipayService(
|
||||
ISqlSugarClient db,
|
||||
IHttpContextAccessor httpContext,
|
||||
SysConfigService sysConfigService,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IOptions<AlipayOptions> alipayOptions)
|
||||
{
|
||||
_db = db;
|
||||
_httpContext = httpContext;
|
||||
_sysConfigService = sysConfigService;
|
||||
_option = alipayOptions.Value;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
|
||||
// 初始化支付宝客户端列表
|
||||
_alipayClientList = [];
|
||||
foreach (var account in _option.AccountList) _alipayClientList.Add(_option.GetClient(account));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取授权信息 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[NonUnify]
|
||||
[AllowAnonymous]
|
||||
[DisplayName("获取授权信息")]
|
||||
[ApiDescriptionSettings(Name = "AuthInfo"), HttpGet]
|
||||
public ActionResult GetAuthInfo([FromQuery] AlipayAuthInfoInput input)
|
||||
{
|
||||
var type = input.UserId?.Split('-').FirstOrDefault().ToInt();
|
||||
var userId = input.UserId?.Split('-').LastOrDefault().ToLong();
|
||||
var account = _option.AccountList.FirstOrDefault();
|
||||
var alipayClient = _alipayClientList.First();
|
||||
|
||||
// 当前网页接口地址
|
||||
var currentUrl = $"{_option.AppAuthUrl}{_httpContext.HttpContext!.Request.Path}?userId={input.UserId}";
|
||||
if (string.IsNullOrEmpty(input.AuthCode))
|
||||
{
|
||||
// 重新授权
|
||||
var url = $"{_option.AuthUrl}?app_id={account!.AppId}&scope=auth_user&redirect_uri={currentUrl}";
|
||||
return new RedirectResult(url);
|
||||
}
|
||||
|
||||
// 组装授权请求参数
|
||||
AlipaySystemOauthTokenRequest request = new()
|
||||
{
|
||||
GrantType = AlipayConst.GrantType,
|
||||
Code = input.AuthCode
|
||||
};
|
||||
AlipaySystemOauthTokenResponse response = alipayClient.CertificateExecute(request);
|
||||
|
||||
// token换取用户信息
|
||||
AlipayUserInfoShareRequest infoShareRequest = new();
|
||||
AlipayUserInfoShareResponse info = alipayClient.CertificateExecute(infoShareRequest, response.AccessToken);
|
||||
|
||||
// 记录授权信息
|
||||
var entity = _db.Queryable<SysAlipayAuthInfo>().First(u =>
|
||||
(!string.IsNullOrWhiteSpace(u.UserId) && u.UserId == info.UserId) ||
|
||||
(!string.IsNullOrWhiteSpace(u.OpenId) && u.OpenId == info.OpenId)) ?? new();
|
||||
entity.Copy(info, excludes: [nameof(SysAlipayAuthInfo.Gender), nameof(SysAlipayAuthInfo.Age)]);
|
||||
entity.Age = int.Parse(info.Age);
|
||||
entity.Gender = info.Gender switch
|
||||
{
|
||||
"m" => GenderEnum.Male,
|
||||
"f" => GenderEnum.Female,
|
||||
_ => GenderEnum.Unknown
|
||||
};
|
||||
entity.AppId = account!.AppId;
|
||||
if (entity.Id <= 0) _db.Insertable(entity).ExecuteCommand();
|
||||
else _db.Updateable(entity).ExecuteCommand();
|
||||
|
||||
// 执行完,重定向到指定界面
|
||||
//var authPageUrl = _sysConfigService.GetConfigValueByCode<string>(ConfigConst.AlipayAuthPageUrl + type).Result;
|
||||
//return new RedirectResult(authPageUrl);
|
||||
return new RedirectResult(_option.AppAuthUrl + "/index.html");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支付回调 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[DisplayName("支付回调")]
|
||||
[ApiDescriptionSettings(Name = "Notify"), HttpPost]
|
||||
public string Notify()
|
||||
{
|
||||
SortedDictionary<string, string> sorted = [];
|
||||
foreach (string key in _httpContext.HttpContext!.Request.Form.Keys)
|
||||
sorted.Add(key, _httpContext.HttpContext.Request.Form[key]);
|
||||
|
||||
var account = _option.AccountList.FirstOrDefault();
|
||||
string alipayPublicKey = Path.Combine(_webHostEnvironment.ContentRootPath, account!.AlipayPublicCertPath!.Replace('/', '\\').TrimStart('\\'));
|
||||
bool signVerified = AlipaySignature.RSACertCheckV1(sorted, alipayPublicKey, "UTF-8", account.SignType); // 调用SDK验证签名
|
||||
if (!signVerified) throw Oops.Oh("交易失败");
|
||||
|
||||
// 更新交易记录
|
||||
var outTradeNo = sorted.GetValueOrDefault("out_trade_no");
|
||||
var transaction = _db.Queryable<SysAlipayTransaction>().First(x => x.OutTradeNo == outTradeNo) ?? throw Oops.Oh("交易记录不存在");
|
||||
transaction.TradeNo = sorted.GetValueOrDefault("trade_no");
|
||||
transaction.TradeStatus = sorted.GetValueOrDefault("trade_status");
|
||||
transaction.FinishTime = sorted.ContainsKey("gmt_payment") ? DateTime.Parse(sorted.GetValueOrDefault("gmt_payment")) : null;
|
||||
transaction.BuyerLogonId = sorted.GetValueOrDefault("buyer_logon_id");
|
||||
transaction.BuyerUserId = sorted.GetValueOrDefault("buyer_user_id");
|
||||
transaction.SellerUserId = sorted.GetValueOrDefault("seller_id");
|
||||
transaction.Remark = sorted.GetValueOrDefault("remark");
|
||||
_db.Updateable(transaction).ExecuteCommand();
|
||||
|
||||
return "success";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一收单下单并支付页面接口 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("统一收单下单并支付页面接口")]
|
||||
[ApiDescriptionSettings(Name = "AlipayTradePagePay"), HttpPost]
|
||||
public string AlipayTradePagePay(AlipayTradePagePayInput input)
|
||||
{
|
||||
// 创建交易记录,状态为等待支付
|
||||
var transactionRecord = new SysAlipayTransaction
|
||||
{
|
||||
AppId = _option.AccountList.First().AppId,
|
||||
OutTradeNo = input.OutTradeNo,
|
||||
TotalAmount = input.TotalAmount.ToDecimal(),
|
||||
TradeStatus = "WAIT_PAY", // 等待支付
|
||||
CreateTime = DateTime.Now,
|
||||
Subject = input.Subject,
|
||||
Body = input.Body,
|
||||
Remark = "等待用户支付"
|
||||
};
|
||||
_db.Insertable(transactionRecord).ExecuteCommand();
|
||||
|
||||
// 设置支付页面请求,并组装业务参数model,设置异步通知接收地址
|
||||
AlipayTradeWapPayRequest request = new();
|
||||
request.SetBizModel(new AlipayTradeWapPayModel()
|
||||
{
|
||||
Subject = input.Subject,
|
||||
OutTradeNo = input.OutTradeNo,
|
||||
TotalAmount = input.TotalAmount,
|
||||
Body = input.Body,
|
||||
ProductCode = "QUICK_WAP_WAY",
|
||||
TimeExpire = input.TimeoutExpress
|
||||
});
|
||||
request.SetNotifyUrl(_option.NotifyUrl);
|
||||
|
||||
var alipayClient = _alipayClientList.First();
|
||||
var response = alipayClient.SdkExecute(request);
|
||||
if (response.IsError) throw Oops.Oh(response.SubMsg);
|
||||
return $"{_option.ServerUrl}?{response.Body}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 交易预创建 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("交易预创建")]
|
||||
[ApiDescriptionSettings(Name = "AlipayPreCreate"), HttpPost]
|
||||
public string AlipayPreCreate(AlipayPreCreateInput input)
|
||||
{
|
||||
// 创建交易记录,状态为等待支付
|
||||
var transactionRecord = new SysAlipayTransaction
|
||||
{
|
||||
AppId = _option.AccountList.First().AppId,
|
||||
OutTradeNo = input.OutTradeNo,
|
||||
TotalAmount = input.TotalAmount.ToDecimal(),
|
||||
TradeStatus = "WAIT_PAY", // 等待支付
|
||||
CreateTime = DateTime.Now,
|
||||
Subject = input.Subject,
|
||||
Remark = "等待用户支付"
|
||||
};
|
||||
_db.Insertable(transactionRecord).ExecuteCommand();
|
||||
|
||||
// 设置异步通知接收地址,并组装业务参数model
|
||||
AlipayTradePrecreateRequest request = new();
|
||||
request.SetNotifyUrl(_option.NotifyUrl);
|
||||
request.SetBizModel(new AlipayTradePrecreateModel()
|
||||
{
|
||||
Subject = input.Subject,
|
||||
OutTradeNo = input.OutTradeNo,
|
||||
TotalAmount = input.TotalAmount,
|
||||
TimeoutExpress = input.TimeoutExpress
|
||||
});
|
||||
|
||||
var alipayClient = _alipayClientList.First();
|
||||
var response = alipayClient.CertificateExecute(request);
|
||||
if (response.IsError) throw Oops.Oh(response.SubMsg);
|
||||
return response.QrCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单笔转账到支付宝账户
|
||||
/// https://opendocs.alipay.com/open/62987723_alipay.fund.trans.uni.transfer
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public async Task<AlipayFundTransUniTransferResponse> Transfer(AlipayFundTransUniTransferInput input)
|
||||
{
|
||||
var account = _option.AccountList.FirstOrDefault(u => u.AppId == input.AppId) ?? throw Oops.Oh("未找到商户支付宝账号");
|
||||
var alipayClient = _option.GetClient(account);
|
||||
|
||||
// 构造请求参数以调用接口
|
||||
AlipayFundTransUniTransferRequest request = new();
|
||||
AlipayFundTransUniTransferModel model = new()
|
||||
{
|
||||
BizScene = AlipayConst.BizScene,
|
||||
ProductCode = AlipayConst.ProductCode,
|
||||
OutBizNo = input.OutBizNo, // 商家订单
|
||||
TransAmount = $"{input.TransAmount}:F2", // 订单总金额
|
||||
OrderTitle = input.OrderTitle, // 业务标题
|
||||
Remark = input.Remark, // 业务备注
|
||||
PayeeInfo = new() // 收款方信息
|
||||
{
|
||||
CertType = input.CertType?.ToString(),
|
||||
CertNo = input.CertNo,
|
||||
Identity = input.Identity,
|
||||
Name = input.Name,
|
||||
IdentityType = input.IdentityType.ToString()
|
||||
},
|
||||
BusinessParams = input.PayerShowNameUseAlias ? "{\"payer_show_name_use_alias\":\"true\"}" : null
|
||||
};
|
||||
|
||||
request.SetBizModel(model);
|
||||
var response = alipayClient.CertificateExecute(request);
|
||||
|
||||
// 保存转账记录
|
||||
await _db.Insertable(new SysAlipayTransaction
|
||||
{
|
||||
UserId = input.UserId,
|
||||
AppId = input.AppId,
|
||||
TradeNo = response.OrderId,
|
||||
OutTradeNo = input.OutBizNo,
|
||||
TotalAmount = response.Amount.ToDecimal(),
|
||||
TradeStatus = response.Code == "10000" ? "SUCCESS" : "FAILED",
|
||||
Subject = input.OrderTitle,
|
||||
ErrorInfo = response.SubMsg,
|
||||
Remark = input.Remark
|
||||
}).ExecuteCommandAsync();
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 用户登录参数
|
||||
/// </summary>
|
||||
public class LoginInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号
|
||||
/// </summary>
|
||||
/// <example>admin</example>
|
||||
[Required(ErrorMessage = "账号不能为空"), MinLength(2, ErrorMessage = "账号不能少于2个字符")]
|
||||
public string Account { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密码
|
||||
/// </summary>
|
||||
/// <example>123456</example>
|
||||
[Required(ErrorMessage = "密码不能为空"), MinLength(3, ErrorMessage = "密码不能少于3个字符")]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户
|
||||
/// </summary>
|
||||
public long? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 验证码Id
|
||||
/// </summary>
|
||||
public long CodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 验证码
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
}
|
||||
|
||||
public class LoginPhoneInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 手机号码
|
||||
/// </summary>
|
||||
/// <example>admin</example>
|
||||
[Required(ErrorMessage = "手机号码不能为空")]
|
||||
[DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")]
|
||||
public string Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 验证码
|
||||
/// </summary>
|
||||
/// <example>123456</example>
|
||||
[Required(ErrorMessage = "验证码不能为空"), MinLength(4, ErrorMessage = "验证码不能少于4个字符")]
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "租户不能为空")]
|
||||
public long? TenantId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户注册输入参数
|
||||
/// </summary>
|
||||
public class UserRegistrationInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 真实姓名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "真实姓名不能为空"), MinLength(2, ErrorMessage = "真实姓名不能少于2个字符")]
|
||||
public string RealName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "账号不能为空"), MinLength(6, ErrorMessage = "账号不能少于6个字符")]
|
||||
public string Account { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机号码
|
||||
/// </summary>
|
||||
/// <example>admin</example>
|
||||
[Required(ErrorMessage = "手机号码不能为空")]
|
||||
[DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")]
|
||||
public string Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 验证码
|
||||
/// </summary>
|
||||
/// <example>123456</example>
|
||||
[Required(ErrorMessage = "验证码不能为空")]
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 验证码Id
|
||||
/// </summary>
|
||||
public long CodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "租户不能为空")]
|
||||
public long TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密码
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 注册方案
|
||||
/// </summary>
|
||||
public long WayId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 用户登录结果
|
||||
/// </summary>
|
||||
public class LoginOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 令牌Token
|
||||
/// </summary>
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 刷新Token
|
||||
/// </summary>
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 个性化首页
|
||||
/// </summary>
|
||||
public string? Homepage { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 用户登录信息
|
||||
/// </summary>
|
||||
public class LoginUserOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账号名称
|
||||
/// </summary>
|
||||
public string Account { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 真实姓名
|
||||
/// </summary>
|
||||
public string RealName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 电话
|
||||
/// </summary>
|
||||
public string Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 身份证
|
||||
/// </summary>
|
||||
public string IdCardNum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账号类型
|
||||
/// </summary>
|
||||
public AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser;
|
||||
|
||||
/// <summary>
|
||||
/// 头像
|
||||
/// </summary>
|
||||
public string Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 个人简介
|
||||
/// </summary>
|
||||
public string Introduction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地址
|
||||
/// </summary>
|
||||
public string Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 电子签名
|
||||
/// </summary>
|
||||
public string Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 机构Id
|
||||
/// </summary>
|
||||
public long OrgId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 机构名称
|
||||
/// </summary>
|
||||
public string OrgName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 机构类型
|
||||
/// </summary>
|
||||
public string OrgType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 职位名称
|
||||
/// </summary>
|
||||
public string PosName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 按钮权限集合
|
||||
/// </summary>
|
||||
public List<string> Buttons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合
|
||||
/// </summary>
|
||||
public List<long> RoleIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 水印文字
|
||||
/// </summary>
|
||||
public string WatermarkText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
public long? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前切换到的租户Id
|
||||
/// </summary>
|
||||
public long? CurrentTenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
public string LangCode { get; internal set; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统域登录信息配置输入参数
|
||||
/// </summary>
|
||||
public class SysLdapInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主机
|
||||
/// </summary>
|
||||
public string? Host { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
public long TenantId { get; set; }
|
||||
}
|
||||
|
||||
public class AddSysLdapInput : SysLdap
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateSysLdapInput : SysLdap
|
||||
{
|
||||
}
|
||||
|
||||
public class DeleteSysLdapInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
public class DetailSysLdapInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
public class SyncSysLdapInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Furion.SpecificationDocument;
|
||||
using Lazy.Captcha.Core;
|
||||
using NewLife.Reflection;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统登录授权服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 500)]
|
||||
public class SysAuthService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly UserManager _userManager;
|
||||
private readonly SqlSugarRepository<SysUser> _sysUserRep;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly SysMenuService _sysMenuService;
|
||||
private readonly SysOnlineUserService _sysOnlineUserService;
|
||||
private readonly SysConfigService _sysConfigService;
|
||||
private readonly SysUserService _sysUserService;
|
||||
private readonly SysTenantService _sysTenantService;
|
||||
private readonly SysSmsService _sysSmsService;
|
||||
private readonly SysLdapService _sysLdapService;
|
||||
private readonly ICaptcha _captcha;
|
||||
private readonly IEventPublisher _eventPublisher;
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
|
||||
public SysAuthService(
|
||||
SqlSugarRepository<SysUser> sysUserRep,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
SysOnlineUserService sysOnlineUserService,
|
||||
SysConfigService sysConfigService,
|
||||
SysLdapService sysLdapService,
|
||||
IEventPublisher eventPublisher,
|
||||
SysSmsService sysSmsService,
|
||||
SysCacheService sysCacheService,
|
||||
SysMenuService sysMenuService,
|
||||
SysUserService sysUserService,
|
||||
SysTenantService sysTenantService,
|
||||
UserManager userManager,
|
||||
ICaptcha captcha)
|
||||
{
|
||||
_captcha = captcha;
|
||||
_sysUserRep = sysUserRep;
|
||||
_userManager = userManager;
|
||||
_sysSmsService = sysSmsService;
|
||||
_eventPublisher = eventPublisher;
|
||||
_sysUserService = sysUserService;
|
||||
_sysTenantService = sysTenantService;
|
||||
_sysMenuService = sysMenuService;
|
||||
_sysCacheService = sysCacheService;
|
||||
_sysConfigService = sysConfigService;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_sysOnlineUserService = sysOnlineUserService;
|
||||
_sysLdapService = sysLdapService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 账号密码登录 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <remarks>用户名/密码:superadmin/123456</remarks>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[DisplayName("账号密码登录")]
|
||||
public virtual async Task<LoginOutput> Login([Required] LoginInput input)
|
||||
{
|
||||
// 判断密码错误次数(缓存30分钟)
|
||||
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{input.Account}";
|
||||
var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
|
||||
var passwordMaxErrorTimes = await _sysConfigService.GetConfigValue<int>(ConfigConst.SysPasswordMaxErrorTimes);
|
||||
// 若未配置或误配置为0、负数, 则默认密码错误次数最大为5次
|
||||
if (passwordMaxErrorTimes < 1) passwordMaxErrorTimes = 5;
|
||||
if (passwordErrorTimes > passwordMaxErrorTimes) throw Oops.Oh(ErrorCodeEnum.D1027);
|
||||
|
||||
// 判断是否开启验证码,其校验验证码
|
||||
if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysCaptcha) && !_captcha.Validate(input.CodeId.ToString(), input.Code)) throw Oops.Oh(ErrorCodeEnum.D0008);
|
||||
|
||||
// 获取登录租户和用户
|
||||
var (tenant, user) = await GetLoginUserAndTenant(input.TenantId, account: input.Account);
|
||||
|
||||
// 账号是否被冻结
|
||||
if (user.Status == StatusEnum.Disable) throw Oops.Oh(ErrorCodeEnum.D1017);
|
||||
|
||||
// 是否开启域登录验证
|
||||
if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysDomainLogin))
|
||||
{
|
||||
var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == tenant.Id);
|
||||
if (userLdap == null)
|
||||
{
|
||||
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
||||
}
|
||||
else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(tenant.Id, userLdap.Account, CryptogramUtil.Decrypt(input.Password)))
|
||||
{
|
||||
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
||||
throw Oops.Oh(ErrorCodeEnum.D1000);
|
||||
}
|
||||
}
|
||||
else
|
||||
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
||||
|
||||
// 登录成功则清空密码错误次数
|
||||
_sysCacheService.Remove(keyPasswordErrorTimes);
|
||||
|
||||
return await CreateToken(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取登录租户和用户
|
||||
/// </summary>
|
||||
/// <param name="tenantId"></param>
|
||||
/// <param name="account"></param>
|
||||
/// <param name="phone"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<(SysTenant tenant, SysUser user)> GetLoginUserAndTenant(long? tenantId, string account = null, string phone = null)
|
||||
{
|
||||
// 账号是否存在
|
||||
var user = await _sysUserRep.AsQueryable().Includes(u => u.SysOrg).ClearFilter()
|
||||
.WhereIF(tenantId > 0, u => (u.AccountType == AccountTypeEnum.SuperAdmin || u.TenantId == tenantId))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(account), u => u.Account.Equals(account))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(phone), u => u.Phone.Equals(phone)).FirstAsync();
|
||||
_ = user ?? throw Oops.Oh(ErrorCodeEnum.D1000);
|
||||
|
||||
// 租户是否存在或已禁用
|
||||
var tenant = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().AsQueryable()
|
||||
.WhereIF(tenantId > 0, u => u.Id == tenantId).WhereIF(tenantId.ToLong() == 0, u => u.Id == user.TenantId).FirstAsync();
|
||||
if (tenant?.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.Z1003);
|
||||
|
||||
// 如果是超级管理员,则引用登录选择的租户进入系统
|
||||
if (tenantId > 0 && user.AccountType == AccountTypeEnum.SuperAdmin)
|
||||
user.TenantId = tenantId;
|
||||
|
||||
return (tenant, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证用户密码
|
||||
/// </summary>
|
||||
/// <param name="password"></param>
|
||||
/// <param name="keyPasswordErrorTimes"></param>
|
||||
/// <param name="passwordErrorTimes"></param>
|
||||
/// <param name="user"></param>
|
||||
private void VerifyPassword(string password, string keyPasswordErrorTimes, int passwordErrorTimes, SysUser user)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 国密SM2解密(前端密码传输SM2加密后的)
|
||||
password = CryptogramUtil.SM2Decrypt(password);
|
||||
if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString())
|
||||
{
|
||||
if (user.Password.Equals(MD5Encryption.Encrypt(password))) return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CryptogramUtil.Decrypt(user.Password).Equals(password)) return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("用户密码验证异常:", ex);
|
||||
}
|
||||
|
||||
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
||||
throw Oops.Oh(ErrorCodeEnum.D1000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证锁屏密码 🔖
|
||||
/// </summary>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("验证锁屏密码")]
|
||||
public virtual async Task<bool> UnLockScreen([Required, FromQuery] string password)
|
||||
{
|
||||
// 账号是否存在
|
||||
var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId);
|
||||
_ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
|
||||
|
||||
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";
|
||||
var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
|
||||
|
||||
// 是否开启域登录验证
|
||||
if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysDomainLogin))
|
||||
{
|
||||
var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId);
|
||||
if (userLdap == null)
|
||||
{
|
||||
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
||||
}
|
||||
else if (!await _sysLdapService.AuthAccount(user.TenantId!.Value, userLdap.Account, CryptogramUtil.Decrypt(password)))
|
||||
{
|
||||
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
||||
throw Oops.Oh(ErrorCodeEnum.D1000);
|
||||
}
|
||||
}
|
||||
else
|
||||
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手机号登录 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[DisplayName("手机号登录")]
|
||||
public virtual async Task<LoginOutput> LoginPhone([Required] LoginPhoneInput input)
|
||||
{
|
||||
// 校验短信验证码
|
||||
_sysSmsService.VerifyCode(new SmsVerifyCodeInput { Phone = input.Phone, Code = input.Code });
|
||||
|
||||
// 获取登录租户和用户
|
||||
var (_, user) = await GetLoginUserAndTenant(input.TenantId, phone: input.Phone);
|
||||
|
||||
return await CreateToken(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成Token令牌 🔖
|
||||
/// </summary>
|
||||
/// <param name="user"></param>\
|
||||
/// <param name="sysUserEventTypeEnum"></param>\
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
internal virtual async Task<LoginOutput> CreateToken(SysUser user, SysUserEventTypeEnum sysUserEventTypeEnum = SysUserEventTypeEnum.Login)
|
||||
{
|
||||
// 单用户登录
|
||||
await _sysOnlineUserService.SingleLogin(user.Id);
|
||||
|
||||
// 生成Token令牌
|
||||
var tokenExpire = await _sysConfigService.GetTokenExpire();
|
||||
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
|
||||
{
|
||||
{ ClaimConst.UserId, user.Id },
|
||||
{ ClaimConst.TenantId, user.TenantId },
|
||||
{ ClaimConst.Account, user.Account },
|
||||
{ ClaimConst.RealName, user.RealName },
|
||||
{ ClaimConst.AccountType, user.AccountType },
|
||||
{ ClaimConst.OrgId, user.OrgId },
|
||||
{ ClaimConst.OrgName, user.SysOrg?.Name },
|
||||
{ ClaimConst.OrgType, user.SysOrg?.Type },
|
||||
{ ClaimConst.LangCode, user.LangCode }
|
||||
}, tokenExpire);
|
||||
|
||||
// 生成刷新Token令牌
|
||||
var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
|
||||
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
|
||||
|
||||
// 设置响应报文头
|
||||
_httpContextAccessor.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken);
|
||||
|
||||
// Swagger Knife4UI-AfterScript登录脚本
|
||||
// ke.global.setAllHeader('Authorization', 'Bearer ' + ke.response.headers['access-token']);
|
||||
|
||||
// 更新用户登录信息
|
||||
user.LastLoginIp = _httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true);
|
||||
(user.LastLoginAddress, double? longitude, double? latitude) = CommonUtil.GetIpAddress(user.LastLoginIp);
|
||||
user.LastLoginTime = DateTime.Now;
|
||||
user.LastLoginDevice = CommonUtil.GetClientDeviceInfo(_httpContextAccessor.HttpContext?.Request?.Headers?.UserAgent);
|
||||
await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new
|
||||
{
|
||||
u.LastLoginIp,
|
||||
u.LastLoginAddress,
|
||||
u.LastLoginTime,
|
||||
u.LastLoginDevice,
|
||||
}).ExecuteCommandAsync();
|
||||
|
||||
var payload = new
|
||||
{
|
||||
Entity = user,
|
||||
Output = new LoginOutput
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken,
|
||||
Homepage = user.Homepage
|
||||
}
|
||||
};
|
||||
|
||||
// 发布系统用户操作事件
|
||||
await _eventPublisher.PublishAsync(sysUserEventTypeEnum, payload);
|
||||
return payload.Output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取登录账号 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取登录账号")]
|
||||
public virtual async Task<LoginUserOutput> GetUserInfo()
|
||||
{
|
||||
var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401);
|
||||
// 获取机构
|
||||
var org = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysOrg>>().GetFirstAsync(u => u.Id == user.OrgId);
|
||||
// 获取职位
|
||||
var pos = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysPos>>().GetFirstAsync(u => u.Id == user.PosId);
|
||||
// 获取按钮集合
|
||||
var buttons = await _sysMenuService.GetOwnBtnPermList();
|
||||
// 获取角色集合
|
||||
var roleIds = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserRole>>().AsQueryable()
|
||||
.Where(u => u.UserId == user.Id).Select(u => u.RoleId).ToListAsync();
|
||||
// 获取水印文字(若系统水印为空则全局为空)
|
||||
var watermarkText = (await _sysUserRep.Context.Queryable<SysTenant>().FirstAsync(u => u.Id == user.TenantId))?.Watermark;
|
||||
if (!string.IsNullOrWhiteSpace(watermarkText)) watermarkText += $"-{user.RealName}";
|
||||
var loginUser = new LoginUserOutput
|
||||
{
|
||||
Id = user.Id,
|
||||
Account = user.Account,
|
||||
RealName = user.RealName,
|
||||
Phone = user.Phone,
|
||||
IdCardNum = user.IdCardNum,
|
||||
Email = user.Email,
|
||||
AccountType = user.AccountType,
|
||||
Avatar = user.Avatar,
|
||||
Address = user.Address,
|
||||
Signature = user.Signature,
|
||||
OrgId = user.OrgId,
|
||||
OrgName = org?.Name,
|
||||
OrgType = org?.Type,
|
||||
PosName = pos?.Name,
|
||||
Buttons = buttons,
|
||||
RoleIds = roleIds,
|
||||
TenantId = user.TenantId,
|
||||
WatermarkText = watermarkText,
|
||||
LangCode = user.LangCode,
|
||||
};
|
||||
|
||||
//将登录信息中的当前租户id,更新为当前所切换到的租户
|
||||
long? currentTenantId = App.User.FindFirst(ClaimConst.TenantId)?.Value?.ToLong(0);
|
||||
loginUser.CurrentTenantId = currentTenantId > 0 ? currentTenantId : user.TenantId;
|
||||
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取刷新Token 🔖
|
||||
/// </summary>
|
||||
/// <param name="accessToken">旧的AccessToken</param>
|
||||
/// <returns>新的AccessToken和RefreshToken</returns>
|
||||
[DisplayName("获取刷新Token")]
|
||||
public virtual async Task<LoginOutput> GetRefreshToken([FromQuery] string accessToken)
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext == null) throw Oops.Oh(ErrorCodeEnum.D1016);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(accessToken)) throw Oops.Oh(ErrorCodeEnum.D1011);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_userManager.Account)) throw Oops.Oh(ErrorCodeEnum.D1011);
|
||||
|
||||
// 黑名单校验
|
||||
if (_sysCacheService.ExistKey($"blacklist:token:{accessToken}")) throw Oops.Oh(ErrorCodeEnum.D1011);
|
||||
|
||||
// 解析Token
|
||||
var (isValid, tokenData, validationResult) = JWTEncryption.Validate(accessToken);
|
||||
if (!isValid) throw Oops.Oh(ErrorCodeEnum.D1016);
|
||||
|
||||
// 获取用户Id
|
||||
var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401);
|
||||
return await CreateToken(user, SysUserEventTypeEnum.RefreshToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出系统 🔖
|
||||
/// </summary>
|
||||
[DisplayName("退出系统")]
|
||||
public async Task Logout()
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext == null) throw Oops.Oh(ErrorCodeEnum.D1016);
|
||||
|
||||
var token = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
throw Oops.Oh(ErrorCodeEnum.D1011);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_userManager.Account))
|
||||
throw Oops.Oh(ErrorCodeEnum.D1011);
|
||||
|
||||
// 写入黑名单(设置过期时间,避免Redis膨胀)
|
||||
var tokenExpire = await _sysConfigService.GetTokenExpire();
|
||||
_sysCacheService.Set($"blacklist:token:{token}", "1", TimeSpan.FromMinutes(tokenExpire));
|
||||
|
||||
// 发布登出事件(用户退出)
|
||||
var user = await _sysUserRep.GetByIdAsync(_userManager.UserId);
|
||||
await _eventPublisher.PublishAsync(SysUserEventTypeEnum.LoginOut, new { Entity = user });
|
||||
|
||||
// 清除 Swagger 登录信息
|
||||
httpContext.SignoutToSwagger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取验证码 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[SuppressMonitor]
|
||||
[DisplayName("获取验证码")]
|
||||
public dynamic GetCaptcha()
|
||||
{
|
||||
var codeId = YitIdHelper.NextId().ToString();
|
||||
var captcha = _captcha.Generate(codeId);
|
||||
var expirySeconds = App.GetOptions<CaptchaOptions>()?.ExpirySeconds ?? 60;
|
||||
return new { Id = codeId, Img = captcha.Base64, ExpirySeconds = expirySeconds };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户注册 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[AllowAnonymous]
|
||||
[HttpPost, ApiDescriptionSettings(Description = "用户注册", DisableInherite = true)]
|
||||
public async Task UserRegistration(UserRegistrationInput input)
|
||||
{
|
||||
// 校验验证码
|
||||
if (!_captcha.Validate(input.CodeId.ToString(), input.Code)) throw Oops.Oh(ErrorCodeEnum.D0008);
|
||||
_captcha.Generate(input.CodeId.ToString());
|
||||
|
||||
// 登录时隐藏租户,查找对应租户信息
|
||||
input.TenantId = input.TenantId <= 0 ? (await _sysTenantService.GetCurrentTenantSysInfo()).Id : input.TenantId;
|
||||
|
||||
// 判断租户是否有效且启用注册功能
|
||||
var tenant = await _sysUserRep.Context.Queryable<SysTenant>().FirstAsync(u => u.Id == input.TenantId && u.Status == StatusEnum.Enable);
|
||||
if (tenant?.EnableReg != YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D1034);
|
||||
|
||||
// 查找注册方案
|
||||
var wayId = input.WayId <= 0 ? tenant.RegWayId : input.WayId;
|
||||
var regWay = await _sysUserRep.Context.Queryable<SysUserRegWay>().FirstAsync(u => u.Id == wayId) ?? throw Oops.Oh(ErrorCodeEnum.D1035);
|
||||
|
||||
var addUserInput = new AddUserInput
|
||||
{
|
||||
AccountType = regWay.AccountType,
|
||||
NickName = "注册用户-" + input.Account,
|
||||
OrgId = regWay.OrgId,
|
||||
PosId = regWay.PosId,
|
||||
TenantId = input.TenantId,
|
||||
RoleIdList = new List<long> { regWay.RoleId },
|
||||
};
|
||||
addUserInput.Copy(input);
|
||||
await _sysUserService.RegisterUser(addUserInput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swagger登录检查 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("/api/swagger/checkUrl"), NonUnify]
|
||||
[ApiDescriptionSettings(Description = "Swagger登录检查", DisableInherite = true)]
|
||||
public int SwaggerCheckUrl()
|
||||
{
|
||||
return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated ? 200 : 401;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swagger登录提交 🔖
|
||||
/// </summary>
|
||||
/// <param name="auth"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("/api/swagger/submitUrl"), NonUnify]
|
||||
[ApiDescriptionSettings(Description = "Swagger登录提交", DisableInherite = true)]
|
||||
public async Task<int> SwaggerSubmitUrl([FromForm] SpecificationAuth auth)
|
||||
{
|
||||
try
|
||||
{
|
||||
_sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}", false);
|
||||
|
||||
await Login(new LoginInput
|
||||
{
|
||||
Account = auth.UserName,
|
||||
Password = CryptogramUtil.SM2Encrypt(auth.Password)
|
||||
});
|
||||
|
||||
_sysCacheService.Remove($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}");
|
||||
|
||||
return 200;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return 401;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Novell.Directory.Ldap;
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 系统域登录配置服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 496, Description = "域登录配置")]
|
||||
public class SysLdapService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysLdap> _sysLdapRep;
|
||||
|
||||
public SysLdapService(SqlSugarRepository<SysLdap> sysLdapRep)
|
||||
{
|
||||
_sysLdapRep = sysLdapRep;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统域登录配置分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取系统域登录配置分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysLdap>> Page(SysLdapInput input)
|
||||
{
|
||||
return await _sysLdapRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Host.Contains(input.Keyword.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Host), u => u.Host.Contains(input.Host.Trim()))
|
||||
.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加系统域登录配置 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加系统域登录配置")]
|
||||
public async Task<long> Add(AddSysLdapInput input)
|
||||
{
|
||||
var entity = input.Adapt<SysLdap>();
|
||||
entity.BindPass = CryptogramUtil.Encrypt(input.BindPass);
|
||||
await _sysLdapRep.InsertAsync(entity);
|
||||
return entity.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新系统域登录配置 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新系统域登录配置")]
|
||||
public async Task Update(UpdateSysLdapInput input)
|
||||
{
|
||||
var entity = input.Adapt<SysLdap>();
|
||||
if (!string.IsNullOrEmpty(input.BindPass) && input.BindPass.Length < 32)
|
||||
{
|
||||
entity.BindPass = CryptogramUtil.Encrypt(input.BindPass); // 加密
|
||||
}
|
||||
|
||||
await _sysLdapRep.AsUpdateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除系统域登录配置 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除系统域登录配置")]
|
||||
public async Task Delete(DeleteSysLdapInput input)
|
||||
{
|
||||
var entity = await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
await _sysLdapRep.FakeDeleteAsync(entity); // 假删除
|
||||
//await _rep.DeleteAsync(entity); // 真删除
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统域登录配置详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取系统域登录配置详情")]
|
||||
public async Task<SysLdap> GetDetail([FromQuery] DetailSysLdapInput input)
|
||||
{
|
||||
return await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统域登录配置列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取系统域登录配置列表")]
|
||||
public async Task<List<SysLdap>> GetList()
|
||||
{
|
||||
return await _sysLdapRep.AsQueryable().Select<SysLdap>().ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证账号
|
||||
/// </summary>
|
||||
/// <param name="account">域用户</param>
|
||||
/// <param name="password">密码</param>
|
||||
/// <param name="tenantId">租户</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<bool> AuthAccount(long? tenantId, string account, string password)
|
||||
{
|
||||
var sysLdap = await _sysLdapRep.GetFirstAsync(u => u.TenantId == tenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
var ldapConn = new LdapConnection();
|
||||
try
|
||||
{
|
||||
await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port);
|
||||
string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass);
|
||||
await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass);
|
||||
var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeSub, sysLdap.AuthFilter.Replace("%s", account), null, false);
|
||||
string dn = string.Empty;
|
||||
while (await ldapSearchResults.HasMoreAsync())
|
||||
{
|
||||
var ldapEntry = await ldapSearchResults.NextAsync();
|
||||
var sAmAccountName = ldapEntry.GetAttributeSet().GetAttribute(sysLdap.BindAttrAccount)?.StringValue;
|
||||
if (string.IsNullOrEmpty(sAmAccountName)) continue;
|
||||
dn = ldapEntry.Dn;
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(dn)) throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
// var attr = new LdapAttribute("userPassword", password);
|
||||
await ldapConn.BindAsync(dn, password);
|
||||
}
|
||||
catch (LdapException e)
|
||||
{
|
||||
return e.ResultCode switch
|
||||
{
|
||||
LdapException.NoSuchObject or LdapException.NoSuchAttribute => throw Oops.Oh(ErrorCodeEnum.D0009),
|
||||
LdapException.InvalidCredentials => false,
|
||||
_ => throw Oops.Oh(e.Message),
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
ldapConn.Disconnect();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步域用户 🔖
|
||||
/// </summary>
|
||||
/// <param name="tenantId"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("同步域用户")]
|
||||
[NonAction]
|
||||
public async Task<List<SysUserLdap>> SyncUserTenant(long tenantId)
|
||||
{
|
||||
var sysLdap = await _sysLdapRep.GetFirstAsync(c => c.TenantId == tenantId && c.IsDelete == false && c.Status == StatusEnum.Enable) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
return await SysLdapService.SyncUser(sysLdap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步域用户 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("同步域用户")]
|
||||
public async Task<List<SysUserLdap>> SyncUser(SyncSysLdapInput input)
|
||||
{
|
||||
var sysLdap = await _sysLdapRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
return await SysLdapService.SyncUser(sysLdap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步域用户 🔖
|
||||
/// </summary>
|
||||
/// <param name="sysLdap"></param>
|
||||
/// <returns></returns>
|
||||
private static async Task<List<SysUserLdap>> SyncUser(SysLdap sysLdap)
|
||||
{
|
||||
if (sysLdap == null) throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
var ldapConn = new LdapConnection();
|
||||
try
|
||||
{
|
||||
await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port);
|
||||
string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass);
|
||||
await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass);
|
||||
var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
|
||||
var userLdapList = new List<SysUserLdap>();
|
||||
while (await ldapSearchResults.HasMoreAsync())
|
||||
{
|
||||
LdapEntry ldapEntry;
|
||||
try
|
||||
{
|
||||
ldapEntry = await ldapSearchResults.NextAsync();
|
||||
if (ldapEntry == null) continue;
|
||||
}
|
||||
catch (LdapException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attrs = ldapEntry.GetAttributeSet();
|
||||
var deptCode = GetDepartmentCode(attrs, sysLdap.BindAttrCode);
|
||||
if (attrs.Count == 0 || attrs.ContainsKey("OU"))
|
||||
{
|
||||
await SearchDnLdapUser(ldapConn, sysLdap, userLdapList, ldapEntry.Dn, deptCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
var sysUserLdap = CreateSysUserLdap(attrs, sysLdap.BindAttrAccount, sysLdap.BindAttrEmployeeId, deptCode);
|
||||
sysUserLdap.Dn = ldapEntry.Dn;
|
||||
sysUserLdap.TenantId = sysLdap.TenantId;
|
||||
userLdapList.Add(sysUserLdap);
|
||||
}
|
||||
}
|
||||
|
||||
if (userLdapList.Count == 0) return null;
|
||||
|
||||
await App.GetRequiredService<SysUserLdapService>().InsertUserLdapList(sysLdap.TenantId!.Value, userLdapList);
|
||||
return userLdapList;
|
||||
}
|
||||
catch (LdapException e)
|
||||
{
|
||||
throw e.ResultCode switch
|
||||
{
|
||||
LdapException.NoSuchObject or LdapException.NoSuchAttribute => Oops.Oh(ErrorCodeEnum.D0009),
|
||||
_ => Oops.Oh(e.Message),
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
ldapConn.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取部门代码
|
||||
/// </summary>
|
||||
/// <param name="attrs"></param>
|
||||
/// <param name="bindAttrCode"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetDepartmentCode(LdapAttributeSet attrs, string bindAttrCode)
|
||||
{
|
||||
return bindAttrCode == "objectGUID"
|
||||
? new Guid(attrs.GetAttribute(bindAttrCode)?.ByteValue!).ToString()
|
||||
: attrs.GetAttribute(bindAttrCode)?.StringValue ?? "0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建同步对象
|
||||
/// </summary>
|
||||
/// <param name="attrs"></param>
|
||||
/// <param name="bindAttrAccount"></param>
|
||||
/// <param name="bindAttrEmployeeId"></param>
|
||||
/// <param name="deptCode"></param>
|
||||
/// <returns></returns>
|
||||
private static SysUserLdap CreateSysUserLdap(LdapAttributeSet attrs, string bindAttrAccount, string bindAttrEmployeeId, string deptCode)
|
||||
{
|
||||
var userLdap = new SysUserLdap
|
||||
{
|
||||
Account = attrs.ContainsKey(bindAttrAccount) ? attrs.GetAttribute(bindAttrAccount)?.StringValue : null,
|
||||
EmployeeId = attrs.ContainsKey(bindAttrEmployeeId) ? attrs.GetAttribute(bindAttrEmployeeId)?.StringValue : null,
|
||||
DeptCode = deptCode,
|
||||
UserName = attrs.ContainsKey("name") ? attrs.GetAttribute("name")?.StringValue : null,
|
||||
Mail = attrs.ContainsKey("mail") ? attrs.GetAttribute("mail")?.StringValue : null
|
||||
};
|
||||
var pwdLastSet = attrs.ContainsKey("pwdLastSet") ? attrs.GetAttribute("pwdLastSet")?.StringValue : null;
|
||||
if (pwdLastSet != null && !pwdLastSet.Equals("0")) userLdap.PwdLastSetTime = DateTime.FromFileTime(Convert.ToInt64(pwdLastSet));
|
||||
var userAccountControl = attrs.ContainsKey("userAccountControl") ? attrs.GetAttribute("userAccountControl")?.StringValue : null;
|
||||
if ((Convert.ToInt32(userAccountControl) & 0x2) == 0x2) // 检查账户是否已过期(通过检查userAccountControl属性的特定位)
|
||||
userLdap.AccountExpiresFlag = true;
|
||||
if ((Convert.ToInt32(userAccountControl) & 0x10000) == 0x10000) // 检查账户密码设置是否永不过期
|
||||
userLdap.DontExpiresFlag = true;
|
||||
return userLdap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 遍历查询域用户
|
||||
/// </summary>
|
||||
/// <param name="ldapConn"></param>
|
||||
/// <param name="sysLdap"></param>
|
||||
/// <param name="userLdapList"></param>
|
||||
/// <param name="baseDn"></param>
|
||||
/// <param name="deptCode"></param>
|
||||
private static async Task SearchDnLdapUser(LdapConnection ldapConn, SysLdap sysLdap, List<SysUserLdap> userLdapList, string baseDn, string deptCode)
|
||||
{
|
||||
var ldapSearchResults = await ldapConn.SearchAsync(baseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
|
||||
while (await ldapSearchResults.HasMoreAsync())
|
||||
{
|
||||
LdapEntry ldapEntry;
|
||||
try
|
||||
{
|
||||
ldapEntry = await ldapSearchResults.NextAsync();
|
||||
if (ldapEntry == null) continue;
|
||||
}
|
||||
catch (LdapException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attrs = ldapEntry.GetAttributeSet();
|
||||
deptCode = GetDepartmentCode(attrs, sysLdap.BindAttrCode);
|
||||
|
||||
if (attrs.Count == 0 || attrs.ContainsKey("OU"))
|
||||
await SearchDnLdapUser(ldapConn, sysLdap, userLdapList, ldapEntry.Dn, deptCode);
|
||||
else
|
||||
{
|
||||
var sysUserLdap = CreateSysUserLdap(attrs, sysLdap.BindAttrAccount, sysLdap.BindAttrEmployeeId, deptCode);
|
||||
sysUserLdap.Dn = ldapEntry.Dn;
|
||||
sysUserLdap.TenantId = sysLdap.TenantId;
|
||||
if (string.IsNullOrEmpty(sysUserLdap.EmployeeId)) continue;
|
||||
userLdapList.Add(sysUserLdap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步域组织 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("同步域组织")]
|
||||
public async Task SyncDept(SyncSysLdapInput input)
|
||||
{
|
||||
var sysLdap = await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
var ldapConn = new LdapConnection();
|
||||
try
|
||||
{
|
||||
await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port);
|
||||
string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass);
|
||||
await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass);
|
||||
var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
|
||||
var orgList = new List<SysOrg>();
|
||||
while (await ldapSearchResults.HasMoreAsync())
|
||||
{
|
||||
LdapEntry ldapEntry;
|
||||
try
|
||||
{
|
||||
ldapEntry = await ldapSearchResults.NextAsync();
|
||||
if (ldapEntry == null) continue;
|
||||
}
|
||||
catch (LdapException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attrs = ldapEntry.GetAttributeSet();
|
||||
if (attrs.Count != 0 && !attrs.ContainsKey("OU")) continue;
|
||||
|
||||
var sysOrg = CreateSysOrg(attrs, sysLdap, orgList, new SysOrg { Id = 0, Level = 0 });
|
||||
orgList.Add(sysOrg);
|
||||
|
||||
await SearchDnLdapDept(ldapConn, sysLdap, orgList, ldapEntry.Dn, sysOrg);
|
||||
}
|
||||
|
||||
if (orgList.Count == 0)
|
||||
return;
|
||||
|
||||
await App.GetRequiredService<SysOrgService>().BatchAddOrgs(orgList);
|
||||
}
|
||||
catch (LdapException e)
|
||||
{
|
||||
throw e.ResultCode switch
|
||||
{
|
||||
LdapException.NoSuchObject or LdapException.NoSuchAttribute => Oops.Oh(ErrorCodeEnum.D0009),
|
||||
_ => Oops.Oh(e.Message),
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
ldapConn.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 遍历查询域用户
|
||||
/// </summary>
|
||||
/// <param name="ldapConn"></param>
|
||||
/// <param name="sysLdap"></param>
|
||||
/// <param name="listOrgs"></param>
|
||||
/// <param name="baseDn"></param>
|
||||
/// <param name="org"></param>
|
||||
private static async Task SearchDnLdapDept(LdapConnection ldapConn, SysLdap sysLdap, List<SysOrg> listOrgs, string baseDn, SysOrg org)
|
||||
{
|
||||
var ldapSearchResults = await ldapConn.SearchAsync(baseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false);
|
||||
while (await ldapSearchResults.HasMoreAsync())
|
||||
{
|
||||
LdapEntry ldapEntry;
|
||||
try
|
||||
{
|
||||
ldapEntry = await ldapSearchResults.NextAsync();
|
||||
if (ldapEntry == null) continue;
|
||||
}
|
||||
catch (LdapException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attrs = ldapEntry.GetAttributeSet();
|
||||
if (attrs.Count != 0 && !attrs.ContainsKey("OU")) continue;
|
||||
|
||||
var sysOrg = CreateSysOrg(attrs, sysLdap, listOrgs, org);
|
||||
listOrgs.Add(sysOrg);
|
||||
|
||||
await SearchDnLdapDept(ldapConn, sysLdap, listOrgs, ldapEntry.Dn, sysOrg);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建架构对象
|
||||
/// </summary>
|
||||
/// <param name="attrs"></param>
|
||||
/// <param name="sysLdap"></param>
|
||||
/// <param name="listOrgs"></param>
|
||||
/// <param name="org"></param>
|
||||
/// <returns></returns>
|
||||
private static SysOrg CreateSysOrg(LdapAttributeSet attrs, SysLdap sysLdap, List<SysOrg> listOrgs, SysOrg org)
|
||||
{
|
||||
return new SysOrg
|
||||
{
|
||||
Pid = org.Id,
|
||||
Id = YitIdHelper.NextId(),
|
||||
Code = attrs.ContainsKey(sysLdap.BindAttrCode) ? new Guid(attrs.GetAttribute(sysLdap.BindAttrCode)?.ByteValue).ToString() : null,
|
||||
Level = org.Level + 1,
|
||||
Name = attrs.ContainsKey(sysLdap.BindAttrAccount) ? attrs.GetAttribute(sysLdap.BindAttrAccount)?.StringValue : null,
|
||||
OrderNo = listOrgs.Count + 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 实体操作基服务
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity"></typeparam>
|
||||
public class BaseService<TEntity> : IDynamicApiController where TEntity : class, new()
|
||||
{
|
||||
private readonly SqlSugarRepository<TEntity> _rep;
|
||||
|
||||
public BaseService(SqlSugarRepository<TEntity> rep)
|
||||
{
|
||||
_rep = rep;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取详情")]
|
||||
public virtual async Task<TEntity> GetDetail(long id)
|
||||
{
|
||||
return await _rep.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取集合 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取集合")]
|
||||
public virtual async Task<List<TEntity>> GetList()
|
||||
{
|
||||
return await _rep.GetListAsync();
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 获取实体分页 🔖
|
||||
///// </summary>
|
||||
///// <param name="input"></param>
|
||||
///// <returns></returns>
|
||||
//[ApiDescriptionSettings(Name = "Page")]
|
||||
//[DisplayName("获取实体分页")]
|
||||
//public async Task<SqlSugarPagedList<TEntity>> GetPage([FromQuery] BasePageInput input)
|
||||
//{
|
||||
// return await _rep.AsQueryable().ToPagedListAsync(input.Page, input.PageSize);
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 增加 🔖
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加")]
|
||||
public virtual async Task<bool> Add(TEntity entity)
|
||||
{
|
||||
return await _rep.InsertAsync(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新 🔖
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新")]
|
||||
public virtual async Task<int> Update(TEntity entity)
|
||||
{
|
||||
return await _rep.AsUpdateable(entity).IgnoreColumns(true).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除 🔖
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除")]
|
||||
public virtual async Task<bool> Delete(long id)
|
||||
{
|
||||
return await _rep.DeleteByIdAsync(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统缓存服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 400, Description = "系统缓存")]
|
||||
public class SysCacheService : IDynamicApiController, ISingleton
|
||||
{
|
||||
private static ICacheProvider _cacheProvider;
|
||||
private readonly CacheOptions _cacheOptions;
|
||||
|
||||
public SysCacheService(ICacheProvider cacheProvider, IOptions<CacheOptions> cacheOptions)
|
||||
{
|
||||
_cacheProvider = cacheProvider;
|
||||
_cacheOptions = cacheOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 申请分布式锁 🔖
|
||||
/// </summary>
|
||||
/// <param name="key">要锁定的key</param>
|
||||
/// <param name="msTimeout">申请锁等待的时间,单位毫秒</param>
|
||||
/// <param name="msExpire">锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒</param>
|
||||
/// <param name="throwOnFailure">失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("申请分布式锁")]
|
||||
public IDisposable? BeginCacheLock(string key, int msTimeout = 500, int msExpire = 10000, bool throwOnFailure = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _cacheProvider.Cache.AcquireLock(key, msTimeout, msExpire, throwOnFailure);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存键名集合 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取缓存键名集合")]
|
||||
public List<string> GetKeyList()
|
||||
{
|
||||
return _cacheProvider.Cache == Cache.Default
|
||||
? [.. _cacheProvider.Cache.Keys.Where(u => u.StartsWith(_cacheOptions.Prefix)).Select(u => u[_cacheOptions.Prefix.Length..]).OrderBy(u => u)]
|
||||
: [.. ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}*", 0, int.MaxValue).Select(u => u[_cacheOptions.Prefix.Length..]).OrderBy(u => u)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加缓存
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public bool Set(string key, object value)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加缓存并设置过期时间
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="expire"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public bool Set(string key, object value, TimeSpan expire)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value, expire);
|
||||
}
|
||||
|
||||
public async Task<TR> AdGetAsync<TR>(String cacheName, Func<Task<TR>> del, TimeSpan? expiry = default) where TR : class
|
||||
{
|
||||
return await AdGetAsync<TR>(cacheName, del, [], expiry);
|
||||
}
|
||||
|
||||
public async Task<TR> AdGetAsync<TR, T1>(String cacheName, Func<T1, Task<TR>> del, T1 t1, TimeSpan? expiry = default) where TR : class
|
||||
{
|
||||
return await AdGetAsync<TR>(cacheName, del, [t1], expiry);
|
||||
}
|
||||
|
||||
public async Task<TR> AdGetAsync<TR, T1, T2>(String cacheName, Func<T1, T2, Task<TR>> del, T1 t1, T2 t2, TimeSpan? expiry = default) where TR : class
|
||||
{
|
||||
return await AdGetAsync<TR>(cacheName, del, [t1, t2], expiry);
|
||||
}
|
||||
|
||||
public async Task<TR> AdGetAsync<TR, T1, T2, T3>(String cacheName, Func<T1, T2, T3, Task<TR>> del, T1 t1, T2 t2, T3 t3, TimeSpan? expiry = default) where TR : class
|
||||
{
|
||||
return await AdGetAsync<TR>(cacheName, del, [t1, t2, t3], expiry);
|
||||
}
|
||||
|
||||
private async Task<T> AdGetAsync<T>(string cacheName, Delegate del, Object[] obs, TimeSpan? expiry) where T : class
|
||||
{
|
||||
var key = Key(cacheName, obs);
|
||||
// 使用分布式锁
|
||||
using (_cacheProvider.Cache.AcquireLock($@"lock:AdGetAsync:{cacheName}", 1000))
|
||||
{
|
||||
var value = Get<T>(key);
|
||||
if (value == null)
|
||||
{
|
||||
value = await ((dynamic)del).DynamicInvoke(obs);
|
||||
if (expiry == null)
|
||||
{
|
||||
Set(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Set(key, value, (TimeSpan)expiry);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public T Get<T>(String cacheName, object t1)
|
||||
{
|
||||
return Get<T>(cacheName, [t1]);
|
||||
}
|
||||
|
||||
public T Get<T>(String cacheName, object t1, object t2)
|
||||
{
|
||||
return Get<T>(cacheName, [t1, t2]);
|
||||
}
|
||||
|
||||
public T Get<T>(String cacheName, object t1, object t2, object t3)
|
||||
{
|
||||
return Get<T>(cacheName, [t1, t2, t3]);
|
||||
}
|
||||
|
||||
private T Get<T>(String cacheName, Object[] obs)
|
||||
{
|
||||
var key = Key(cacheName, obs);
|
||||
return Get<T>(key);
|
||||
}
|
||||
|
||||
private static string Key(string cacheName, object[] obs)
|
||||
{
|
||||
if (obs.OfType<TimeSpan>().Any()) throw new Exception("缓存参数类型不能是:TimeSpan类型");
|
||||
StringBuilder sb = new(cacheName);
|
||||
if (obs is { Length: > 0 })
|
||||
{
|
||||
sb.Append(':');
|
||||
foreach (var a in obs) sb.Append($"<{KeySingle(a)}>");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string KeySingle(object t)
|
||||
{
|
||||
return t.GetType().IsClass && !t.GetType().IsPrimitive ? JsonConvert.SerializeObject(t) : t.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存的剩余生存时间
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public TimeSpan GetExpire(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.GetExpire(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public T Get<T>(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.Get<T>($"{_cacheOptions.Prefix}{key}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除缓存 🔖
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除缓存")]
|
||||
public int Remove(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.Remove($"{_cacheOptions.Prefix}{key}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有缓存 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("清空所有缓存")]
|
||||
[ApiDescriptionSettings(Name = "Clear"), HttpPost]
|
||||
public void Clear()
|
||||
{
|
||||
_cacheProvider.Cache.Clear();
|
||||
|
||||
Cache.Default.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查缓存是否存在
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public bool ExistKey(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.ContainsKey($"{_cacheOptions.Prefix}{key}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据键名前缀删除缓存 🔖
|
||||
/// </summary>
|
||||
/// <param name="prefixKey">键名前缀</param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "DeleteByPreKey"), HttpPost]
|
||||
[DisplayName("根据键名前缀删除缓存")]
|
||||
public int RemoveByPrefixKey(string prefixKey)
|
||||
{
|
||||
var delKeys = _cacheProvider.Cache == Cache.Default
|
||||
? _cacheProvider.Cache.Keys.Where(u => u.StartsWith($"{_cacheOptions.Prefix}{prefixKey}")).ToArray()
|
||||
: ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}{prefixKey}*", 0, int.MaxValue).ToArray();
|
||||
return _cacheProvider.Cache.Remove(delKeys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据键名前缀获取键名集合 🔖
|
||||
/// </summary>
|
||||
/// <param name="prefixKey">键名前缀</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据键名前缀获取键名集合")]
|
||||
public List<string> GetKeysByPrefixKey(string prefixKey)
|
||||
{
|
||||
return _cacheProvider.Cache == Cache.Default
|
||||
? _cacheProvider.Cache.Keys.Where(u => u.StartsWith($"{_cacheOptions.Prefix}{prefixKey}")).Select(u => u[_cacheOptions.Prefix.Length..]).ToList()
|
||||
: ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}{prefixKey}*", 0, int.MaxValue).Select(u => u[_cacheOptions.Prefix.Length..]).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存值 🔖
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取缓存值")]
|
||||
public object GetValue(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return null;
|
||||
|
||||
if (Regex.IsMatch(key, @"%[0-9a-fA-F]{2}"))
|
||||
key = HttpUtility.UrlDecode(key);
|
||||
|
||||
var fullKey = $"{_cacheOptions.Prefix}{key}";
|
||||
|
||||
if (_cacheProvider.Cache == Cache.Default)
|
||||
return _cacheProvider.Cache.Get<object>(fullKey);
|
||||
|
||||
if (_cacheProvider.Cache is FullRedis redisCache)
|
||||
{
|
||||
if (!redisCache.ContainsKey(fullKey))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var keyType = redisCache.TYPE(fullKey)?.ToLower();
|
||||
switch (keyType)
|
||||
{
|
||||
case "string":
|
||||
return redisCache.Get<string>(fullKey);
|
||||
|
||||
case "list":
|
||||
var list = redisCache.GetList<string>(fullKey);
|
||||
return list?.ToList();
|
||||
|
||||
case "hash":
|
||||
var hash = redisCache.GetDictionary<string>(fullKey);
|
||||
return hash?.ToDictionary(k => k.Key, v => v.Value);
|
||||
|
||||
case "set":
|
||||
var set = redisCache.GetSet<string>(fullKey);
|
||||
return set?.ToArray();
|
||||
|
||||
case "zset":
|
||||
var sortedSet = redisCache.GetSortedSet<string>(fullKey);
|
||||
return sortedSet?.Range(0, -1)?.ToList();
|
||||
|
||||
case "none":
|
||||
return null;
|
||||
|
||||
default:
|
||||
// 未知类型或特殊类型
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "key", key },
|
||||
{ "type", keyType ?? "unknown" },
|
||||
{ "message", "无法使用标准方式获取此类型数据" }
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "key", key },
|
||||
{ "error", ex.Message },
|
||||
{ "type", "exception" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return _cacheProvider.Cache.Get<object>(fullKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或添加缓存(在数据不存在时执行委托请求数据)
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="callback"></param>
|
||||
/// <param name="expire">过期时间,单位秒</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public T GetOrAdd<T>(string key, Func<string, T> callback, int expire = -1)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return default;
|
||||
return _cacheProvider.Cache.GetOrAdd($"{_cacheOptions.Prefix}{key}", callback, expire);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hash匹配
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public IDictionary<String, T> GetHashMap<T>(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.GetDictionary<T>(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="dic"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public bool HashSet<T>(string key, Dictionary<string, T> dic)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
foreach (var v in dic)
|
||||
{
|
||||
hash.Add(v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加一条HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="hashKey"></param>
|
||||
/// <param name="value"></param>
|
||||
[NonAction]
|
||||
public void HashAdd<T>(string key, string hashKey, T value)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
hash.Add(hashKey, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加或更新一条HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="hashKey"></param>
|
||||
/// <param name="value"></param>
|
||||
[NonAction]
|
||||
public void HashAddOrUpdate<T>(string key, string hashKey, T value)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
if (hash.ContainsKey(hashKey))
|
||||
hash[hashKey] = value;
|
||||
else
|
||||
hash.Add(hashKey, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取多条HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="fields"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public List<T> HashGet<T>(string key, params string[] fields)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
return hash.Where(t => fields.Any(c => t.Key == c)).Select(t => t.Value).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取一条HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="field"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public T HashGetOne<T>(string key, string field)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
return hash.TryGetValue(field, out T value) ? value : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据KEY获取所有HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public IDictionary<string, T> HashGetAll<T>(string key)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="fields"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public int HashDel<T>(string key, params string[] fields)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
fields.ToList().ForEach(t => hash.Remove(t));
|
||||
return fields.Length;
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 搜索HASH
|
||||
///// </summary>
|
||||
///// <typeparam name="T"></typeparam>
|
||||
///// <param name="key"></param>
|
||||
///// <param name="searchModel"></param>
|
||||
///// <returns></returns>
|
||||
//[NonAction]
|
||||
//public List<KeyValuePair<string, T>> HashSearch<T>(string key, SearchModel searchModel)
|
||||
//{
|
||||
// var hash = GetHashMap<T>(key);
|
||||
// return hash.Search(searchModel).ToList();
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// 搜索HASH
|
||||
///// </summary>
|
||||
///// <typeparam name="T"></typeparam>
|
||||
///// <param name="key"></param>
|
||||
///// <param name="pattern"></param>
|
||||
///// <param name="count"></param>
|
||||
///// <returns></returns>
|
||||
//[NonAction]
|
||||
//public List<KeyValuePair<string, T>> HashSearch<T>(string key, string pattern, int count)
|
||||
//{
|
||||
// var hash = GetHashMap<T>(key);
|
||||
// return hash.Search(pattern, count).ToList();
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义模板引擎
|
||||
/// </summary>
|
||||
public class CustomViewEngine : ViewEngineModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 库定位器
|
||||
/// </summary>
|
||||
public string ConfigId { get; set; } = SqlSugarConst.MainConfigId;
|
||||
|
||||
public string AuthorName { get; set; }
|
||||
|
||||
public string BusName { get; set; }
|
||||
|
||||
public string NameSpace { get; set; }
|
||||
|
||||
public string ClassName { get; set; }
|
||||
|
||||
public string LowerClassName { get; set; }
|
||||
|
||||
public string ProjectLastName { get; set; }
|
||||
|
||||
public string PagePath { get; set; } = "main";
|
||||
|
||||
public string PrintType { get; set; }
|
||||
|
||||
public string PrintName { get; set; }
|
||||
|
||||
public bool HasLikeQuery { get; set; }
|
||||
|
||||
public bool HasJoinTable { get; set; }
|
||||
|
||||
public bool HasEnumField { get; set; }
|
||||
|
||||
public bool HasDictField { get; set; }
|
||||
|
||||
public bool HasConstField { get; set; }
|
||||
|
||||
public bool HasSetStatus => TableField.Any(IsStatus);
|
||||
|
||||
public List<CodeGenConfig> TableField { get; set; }
|
||||
|
||||
public List<CodeGenConfig> ImportFieldList { get; set; }
|
||||
|
||||
public List<CodeGenConfig> UploadFieldList { get; set; }
|
||||
|
||||
public List<CodeGenConfig> QueryWhetherList { get; set; }
|
||||
|
||||
public List<CodeGenConfig> ApiTreeFieldList { get; set; }
|
||||
|
||||
public List<CodeGenConfig> DropdownFieldList { get; set; }
|
||||
|
||||
public List<CodeGenConfig> AddUpdateFieldList { get; set; }
|
||||
|
||||
public List<CodeGenConfig> PrimaryKeyFieldList { get; set; }
|
||||
|
||||
public List<TableUniqueConfigItem> TableUniqueConfigList { get; set; }
|
||||
|
||||
public List<CodeGenConfig> IgnoreUpdateFieldList => TableField.Where(u => u.WhetherAddUpdate == "N" && u.ColumnKey != "True" && u.WhetherCommon != "Y").ToList();
|
||||
|
||||
/// <summary>
|
||||
/// 格式化主键查询条件
|
||||
/// 例: PrimaryKeysFormat(" || ", "u.{0} == input.{0}")
|
||||
/// 单主键返回 u.Id == input.Id
|
||||
/// 组合主键返回 u.Id == input.Id || u.FkId == input.FkId
|
||||
/// </summary>
|
||||
/// <param name="separator">分隔符</param>
|
||||
/// <param name="format">模板字符串</param>
|
||||
/// <param name="lowerFirstLetter">字段首字母小写</param>
|
||||
/// <returns></returns>
|
||||
public string PrimaryKeysFormat(string separator, string format, bool lowerFirstLetter = false) => string.Join(separator, PrimaryKeyFieldList.Select(u => string.Format(format, lowerFirstLetter ? u.LowerPropertyName : u.PropertyName)));
|
||||
|
||||
/// <summary>
|
||||
/// 注入的服务
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Dictionary<string, string> InjectServiceMap
|
||||
{
|
||||
get
|
||||
{
|
||||
var injectMap = new Dictionary<string, string>();
|
||||
if (UploadFieldList.Count > 0) injectMap.Add(nameof(SysFileService), ToLowerFirstLetter(nameof(SysFileService)));
|
||||
if (DropdownFieldList.Count > 0 || ImportFieldList.Count > 0) injectMap.Add(nameof(ISqlSugarClient), ToLowerFirstLetter(nameof(ISqlSugarClient).TrimStart('I')));
|
||||
if (ImportFieldList.Any(c => c.EffectType == "DictSelector")) injectMap.Add(nameof(SysDictTypeService), ToLowerFirstLetter(nameof(SysDictTypeService)));
|
||||
return injectMap;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务构造参数
|
||||
/// </summary>
|
||||
public string InjectServiceArgs => InjectServiceMap.Count > 0 ? ", " + string.Join(", ", InjectServiceMap.Select(kv => $"{kv.Key} {kv.Value}")) : "";
|
||||
|
||||
/// <summary>
|
||||
/// 默认值列表
|
||||
/// </summary>
|
||||
public List<CodeGenConfig> DefaultValueList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 判断字段是否为状态字段
|
||||
/// </summary>
|
||||
/// <param name="column"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsStatus(CodeGenConfig column) => column.NetType == nameof(StatusEnum);
|
||||
|
||||
/// <summary>
|
||||
/// 获取首字母小写字符串
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
public string ToLowerFirstLetter(string text) => string.IsNullOrWhiteSpace(text) ? text : text[..1].ToLower() + text[1..];
|
||||
|
||||
/// <summary>
|
||||
/// 将基本字段类型转为可空类型
|
||||
/// </summary>
|
||||
/// <param name="netType"></param>
|
||||
/// <returns></returns>
|
||||
public string GetNullableNetType(string netType) => Regex.IsMatch(netType, "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? netType.TrimEnd('?') + "?" : netType;
|
||||
|
||||
/// <summary>
|
||||
/// 获取前端表格列定义的属性
|
||||
/// </summary>
|
||||
/// <param name="column"></param>
|
||||
/// <returns></returns>
|
||||
public string GetElTableColumnCustomProperty(CodeGenConfig column)
|
||||
{
|
||||
var content = $"prop='{column.LowerPropertyName}' label='{column.ColumnComment}'";
|
||||
if (IsStatus(column)) content += $" v-auth=\"'{LowerClassName}:setStatus'\"";
|
||||
if (column.WhetherSortable == "Y") content += " sortable='custom'";
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认值
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string GetAddDefaultValue()
|
||||
{
|
||||
var content = "";
|
||||
if (DefaultValueList.Count == 0)
|
||||
{
|
||||
var status = TableField.FirstOrDefault(IsStatus);
|
||||
var orderNo = TableField.FirstOrDefault(c => c.NetType.TrimEnd('?') == "int" && c.PropertyName == nameof(SysUser.OrderNo));
|
||||
if (status != null) content += $"{status.LowerPropertyName}: {(int)StatusEnum.Enable},";
|
||||
if (orderNo != null) content += $"{orderNo.LowerPropertyName}: 100,";
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in DefaultValueList)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(item.DefaultValue))
|
||||
{
|
||||
switch (item.EffectType)
|
||||
{
|
||||
case "InputNumber":
|
||||
case "EnumSelector"://枚举和数字框,通过正则提取 数字:如 ('0')
|
||||
content += $"{item.LowerPropertyName}: {Regex.Match(item.DefaultValue, @"\d+").Value},";
|
||||
break;
|
||||
|
||||
case "Switch":
|
||||
content += $"{item.LowerPropertyName}: {(item.DefaultValue == "1" ? true.ToString().ToLower() : false.ToString().ToLower())},";
|
||||
break;
|
||||
|
||||
case "DatePicker"://忽略适配日期格式
|
||||
break;
|
||||
|
||||
default:
|
||||
content += $"{item.LowerPropertyName}: \"{item.DefaultValue}\",";// 如果是字符串 DefaultValue=('男')
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 代码生成详细配置参数
|
||||
/// </summary>
|
||||
public class CodeGenConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 代码生成主表ID
|
||||
/// </summary>
|
||||
public long CodeGenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库字段名
|
||||
/// </summary>
|
||||
public string ColumnName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主外键
|
||||
/// </summary>
|
||||
public string ColumnKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实体属性名
|
||||
/// </summary>
|
||||
public string PropertyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段数据长度
|
||||
/// </summary>
|
||||
public int ColumnLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库字段名(首字母小写)
|
||||
/// </summary>
|
||||
public string LowerPropertyName => string.IsNullOrWhiteSpace(PropertyName) ? null : PropertyName[..1].ToLower() + PropertyName[1..];
|
||||
|
||||
/// <summary>
|
||||
/// 字段描述
|
||||
/// </summary>
|
||||
public string ColumnComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// .NET类型
|
||||
/// </summary>
|
||||
public string NetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库中类型(物理类型)
|
||||
/// </summary>
|
||||
public string DataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段数据默认值
|
||||
/// </summary>
|
||||
public string DefaultValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可空.NET类型
|
||||
/// </summary>
|
||||
public string NullableNetType => Regex.IsMatch(NetType ?? "", "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? NetType.TrimEnd('?') + "?" : NetType;
|
||||
|
||||
/// <summary>
|
||||
/// 作用类型(字典)
|
||||
/// </summary>
|
||||
public string EffectType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外键库标识
|
||||
/// </summary>
|
||||
public string FkConfigId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外键实体名称
|
||||
/// </summary>
|
||||
public string FkEntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外键表名称
|
||||
/// </summary>
|
||||
public string FkTableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外键实体名称(首字母小写)
|
||||
/// </summary>
|
||||
public string LowerFkEntityName => string.IsNullOrWhiteSpace(FkEntityName) ? null : FkEntityName[..1].ToLower() + FkEntityName[1..];
|
||||
|
||||
/// <summary>
|
||||
/// 外键链接字段
|
||||
/// </summary>
|
||||
public string FkLinkColumnName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外键显示字段
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public string FkDisplayColumns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外键显示字段
|
||||
/// </summary>
|
||||
public List<string> FkDisplayColumnList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外键显示字段(首字母小写)
|
||||
/// </summary>
|
||||
public List<string> LowerFkDisplayColumnsList => FkDisplayColumnList?.Select(name => name[..1].ToLower() + name[1..]).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// 外键显示字段.NET类型
|
||||
/// </summary>
|
||||
public string FkColumnNetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级字段
|
||||
/// </summary>
|
||||
public string PidColumn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典code
|
||||
/// </summary>
|
||||
public string DictTypeCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 查询方式
|
||||
/// </summary>
|
||||
public string QueryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否是查询条件
|
||||
/// </summary>
|
||||
public string WhetherQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 列表是否缩进(字典)
|
||||
/// </summary>
|
||||
public string WhetherRetract { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填(字典)
|
||||
/// </summary>
|
||||
public string WhetherRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可排序(字典)
|
||||
/// </summary>
|
||||
public string WhetherSortable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 列表显示
|
||||
/// </summary>
|
||||
public string WhetherTable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 增改
|
||||
/// </summary>
|
||||
public string WhetherAddUpdate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 导入
|
||||
/// </summary>
|
||||
public string WhetherImport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否是通用字段
|
||||
/// </summary>
|
||||
public string WhetherCommon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
public int OrderNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否是选择器控件
|
||||
/// </summary>
|
||||
public bool IsSelectorEffectType => Regex.IsMatch(EffectType ?? "", "Selector$|ForeignKey", RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 去掉尾部Id的属性名
|
||||
/// </summary>
|
||||
public string PropertyNameTrimEndId => PropertyName.TrimEnd("Id");
|
||||
|
||||
/// <summary>
|
||||
/// 去掉尾部Id的属性名
|
||||
/// </summary>
|
||||
public string LowerPropertyNameTrimEndId => LowerPropertyName.TrimEnd("Id");
|
||||
|
||||
/// <summary>
|
||||
/// 扩展属性名称
|
||||
/// </summary>
|
||||
public string ExtendedPropertyName => EffectType switch
|
||||
{
|
||||
"ForeignKey" => $"{PropertyName.TrimEnd("Id")}FkDisplayName",
|
||||
"ApiTreeSelector" => $"{PropertyName.TrimEnd("Id")}DisplayName",
|
||||
"DictSelector" => $"{PropertyName.TrimEnd("Id")}DictLabel",
|
||||
"Upload" => $"{PropertyName.TrimEnd("Id")}Attachment",
|
||||
"Upload_SingleFile" => $"{PropertyName.TrimEnd("Id")}Attachment",
|
||||
_ => PropertyName
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 首字母小写的扩展属性名称
|
||||
/// </summary>
|
||||
public string LowerExtendedPropertyName
|
||||
{
|
||||
get
|
||||
{
|
||||
var displayPropertyName = ExtendedPropertyName;
|
||||
if (string.IsNullOrWhiteSpace(displayPropertyName)) return null;
|
||||
return displayPropertyName[..1].ToLower() + displayPropertyName[1..];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取外键显示值语句
|
||||
/// </summary>
|
||||
/// <param name="tableAlias">表别名</param>
|
||||
/// <param name="separator">多字段时的连接符</param>
|
||||
/// <returns></returns>
|
||||
public string GetDisplayColumn(string tableAlias, string separator = "-") => "$\"" + string.Join(separator, FkDisplayColumnList?.Select(name => $"{{{tableAlias}.{name}}}") ?? new List<string>()) + "\"";
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 代码生成参数类
|
||||
/// </summary>
|
||||
public class CodeGenInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 作者姓名
|
||||
/// </summary>
|
||||
public virtual string AuthorName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类名
|
||||
/// </summary>
|
||||
public virtual string ClassName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否移除表前缀
|
||||
/// </summary>
|
||||
public virtual string TablePrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 库定位器名
|
||||
/// </summary>
|
||||
public virtual string ConfigId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库名(保留字段)
|
||||
/// </summary>
|
||||
public virtual string DbName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库类型
|
||||
/// </summary>
|
||||
public virtual string DbType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库链接
|
||||
/// </summary>
|
||||
public virtual string ConnectionString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生成方式
|
||||
/// </summary>
|
||||
public virtual string GenerateType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库表名
|
||||
/// </summary>
|
||||
public virtual string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 命名空间
|
||||
/// </summary>
|
||||
public virtual string NameSpace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务名(业务代码包名称)
|
||||
/// </summary>
|
||||
public virtual string BusName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 功能名(数据库表名称)
|
||||
/// </summary>
|
||||
public virtual string TableComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 表唯一字段
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public virtual string TableUniqueConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 表唯一字段列表
|
||||
/// </summary>
|
||||
public virtual List<TableUniqueConfigItem> TableUniqueList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单应用分类(应用编码)
|
||||
/// </summary>
|
||||
public virtual string MenuApplication { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否生成菜单
|
||||
/// </summary>
|
||||
public virtual bool GenerateMenu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单父级
|
||||
/// </summary>
|
||||
public virtual long? MenuPid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单图标
|
||||
/// </summary>
|
||||
public virtual string MenuIcon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页面目录
|
||||
/// </summary>
|
||||
public virtual string PagePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支持打印类型
|
||||
/// </summary>
|
||||
public virtual string PrintType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 打印模版名称
|
||||
/// </summary>
|
||||
public virtual string PrintName { get; set; }
|
||||
}
|
||||
|
||||
public class AddCodeGenInput : CodeGenInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库表名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "数据库表名不能为空")]
|
||||
public override string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务名(业务代码包名称)
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "业务名不能为空")]
|
||||
public override string BusName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 命名空间
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "命名空间不能为空")]
|
||||
public override string NameSpace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作者姓名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "作者姓名不能为空")]
|
||||
public override string AuthorName { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 类名
|
||||
///// </summary>
|
||||
//[Required(ErrorMessage = "类名不能为空")]
|
||||
//public override string ClassName { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 是否移除表前缀
|
||||
///// </summary>
|
||||
//[Required(ErrorMessage = "是否移除表前缀不能为空")]
|
||||
//public override string TablePrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生成方式
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "生成方式不能为空")]
|
||||
public override string GenerateType { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 功能名(数据库表名称)
|
||||
///// </summary>
|
||||
//[Required(ErrorMessage = "数据库表名不能为空")]
|
||||
//public override string TableComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否生成菜单
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "是否生成菜单不能为空")]
|
||||
public override bool GenerateMenu { get; set; }
|
||||
}
|
||||
|
||||
public class DeleteCodeGenInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 代码生成器Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "代码生成器Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateCodeGenInput : AddCodeGenInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 代码生成器Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "代码生成器Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
public class QueryCodeGenInput : DeleteCodeGenInput
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 代码生成参数类
|
||||
/// </summary>
|
||||
public class CodeGenOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 代码生成器Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作者姓名
|
||||
/// </summary>
|
||||
public string AuthorName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类名
|
||||
/// </summary>
|
||||
public string ClassName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否移除表前缀
|
||||
/// </summary>
|
||||
public string TablePrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生成方式
|
||||
/// </summary>
|
||||
public string GenerateType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库表名
|
||||
/// </summary>
|
||||
public string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包名
|
||||
/// </summary>
|
||||
public string PackageName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务名(业务代码包名称)
|
||||
/// </summary>
|
||||
public string BusName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 功能名(数据库表名称)
|
||||
/// </summary>
|
||||
public string TableComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单应用分类(应用编码)
|
||||
/// </summary>
|
||||
public string MenuApplication { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否生成菜单
|
||||
/// </summary>
|
||||
public bool GenerateMenu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单父级
|
||||
/// </summary>
|
||||
public long? MenuPid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支持打印类型
|
||||
/// </summary>
|
||||
public string PrintType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 打印模版名称
|
||||
/// </summary>
|
||||
public string PrintName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库表列
|
||||
/// </summary>
|
||||
public class ColumnOuput
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
public string ColumnName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实体的Property名
|
||||
/// </summary>
|
||||
public string PropertyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段数据长度
|
||||
/// </summary>
|
||||
public int ColumnLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库中类型
|
||||
/// </summary>
|
||||
public string DataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段数据默认值
|
||||
/// </summary>
|
||||
public string DefaultValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为主键
|
||||
/// </summary>
|
||||
public bool IsPrimarykey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许为空
|
||||
/// </summary>
|
||||
public bool IsNullable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// .NET字段类型
|
||||
/// </summary>
|
||||
public string NetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典编码
|
||||
/// </summary>
|
||||
public string DictTypeCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段描述
|
||||
/// </summary>
|
||||
public string ColumnComment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主外键
|
||||
/// </summary>
|
||||
public string ColumnKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库
|
||||
/// </summary>
|
||||
public class DatabaseOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 库定位器名
|
||||
/// </summary>
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 库名
|
||||
/// </summary>
|
||||
public string DbNickName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库类型
|
||||
/// </summary>
|
||||
public SqlSugar.DbType DbType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库连接字符串
|
||||
/// </summary>
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库表
|
||||
/// </summary>
|
||||
public class TableOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 库定位器名
|
||||
/// </summary>
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 表名(字母形式的)
|
||||
/// </summary>
|
||||
public string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实体名称
|
||||
/// </summary>
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public string CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public string UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 表名称描述(功能名)
|
||||
/// </summary>
|
||||
public string TableComment { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 表唯一配置项
|
||||
/// </summary>
|
||||
public class TableUniqueConfigItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段列表
|
||||
/// </summary>
|
||||
public List<string> Columns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 描述信息
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 格式化查询条件
|
||||
/// </summary>
|
||||
/// <param name="separator">分隔符</param>
|
||||
/// <param name="format">模板字符串</param>
|
||||
/// <returns></returns>
|
||||
public string Format(string separator, string format) => string.Join(separator, Columns.Select(name => string.Format(format, name)));
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统代码生成配置服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 260)]
|
||||
public class SysCodeGenConfigService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly ISqlSugarClient _db;
|
||||
|
||||
public SysCodeGenConfigService(ISqlSugarClient db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取代码生成配置列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取代码生成配置列表")]
|
||||
public async Task<List<CodeGenConfig>> GetList([FromQuery] CodeGenConfig input)
|
||||
{
|
||||
return await _db.Queryable<SysCodeGenConfig>()
|
||||
.Where(u => u.CodeGenId == input.CodeGenId)
|
||||
.Select<CodeGenConfig>()
|
||||
.Mapper(u =>
|
||||
{
|
||||
u.NetType = (u.EffectType == "EnumSelector" ? u.DictTypeCode : u.NetType);
|
||||
u.FkDisplayColumnList = u.FkDisplayColumns?.Split(",").ToList();
|
||||
})
|
||||
.OrderBy(u => new { u.OrderNo, u.Id })
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新代码生成配置 🔖
|
||||
/// </summary>
|
||||
/// <param name="inputList"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新代码生成配置")]
|
||||
public async Task UpdateCodeGenConfig(List<CodeGenConfig> inputList)
|
||||
{
|
||||
if (inputList == null || inputList.Count < 1) return;
|
||||
inputList.ForEach(e =>
|
||||
{
|
||||
e.FkDisplayColumns = e.FkDisplayColumnList?.Count > 0 ? string.Join(",", e.FkDisplayColumnList) : null;
|
||||
});
|
||||
await _db.Updateable(inputList.Adapt<List<SysCodeGenConfig>>())
|
||||
.IgnoreColumns(u => new { u.ColumnLength, u.ColumnName, u.PropertyName })
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除代码生成配置
|
||||
/// </summary>
|
||||
/// <param name="codeGenId"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task DeleteCodeGenConfig(long codeGenId)
|
||||
{
|
||||
await _db.Deleteable<SysCodeGenConfig>().Where(u => u.CodeGenId == codeGenId).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取代码生成配置详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取代码生成配置详情")]
|
||||
public async Task<SysCodeGenConfig> GetDetail([FromQuery] CodeGenConfig input)
|
||||
{
|
||||
return await _db.Queryable<SysCodeGenConfig>().FirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量增加代码生成配置
|
||||
/// </summary>
|
||||
/// <param name="tableColumnOutputList"></param>
|
||||
/// <param name="codeGenerate"></param>
|
||||
[NonAction]
|
||||
public void AddList(List<ColumnOuput> tableColumnOutputList, SysCodeGen codeGenerate)
|
||||
{
|
||||
if (tableColumnOutputList == null) return;
|
||||
|
||||
var codeGenConfigs = new List<SysCodeGenConfig>();
|
||||
var orderNo = 100;
|
||||
foreach (var tableColumn in tableColumnOutputList)
|
||||
{
|
||||
var codeGenConfig = new SysCodeGenConfig();
|
||||
|
||||
var yesOrNo = YesNoEnum.Y.ToString();
|
||||
if (Convert.ToBoolean(tableColumn.ColumnKey)) yesOrNo = YesNoEnum.N.ToString();
|
||||
|
||||
if (CodeGenUtil.IsCommonColumn(tableColumn.PropertyName))
|
||||
{
|
||||
codeGenConfig.WhetherCommon = YesNoEnum.Y.ToString();
|
||||
yesOrNo = YesNoEnum.N.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
codeGenConfig.WhetherCommon = YesNoEnum.N.ToString();
|
||||
}
|
||||
|
||||
codeGenConfig.CodeGenId = codeGenerate.Id;
|
||||
codeGenConfig.ColumnName = tableColumn.ColumnName; // 字段名
|
||||
codeGenConfig.PropertyName = tableColumn.PropertyName;// 实体属性名
|
||||
codeGenConfig.ColumnLength = tableColumn.ColumnLength;// 长度
|
||||
codeGenConfig.ColumnComment = tableColumn.ColumnComment;
|
||||
codeGenConfig.NetType = tableColumn.NetType;
|
||||
codeGenConfig.DefaultValue = tableColumn.DefaultValue;
|
||||
codeGenConfig.WhetherRetract = YesNoEnum.N.ToString();
|
||||
|
||||
// 生成代码时,主键并不是必要输入项,故一定要排除主键字段
|
||||
codeGenConfig.WhetherRequired = (tableColumn.IsNullable || tableColumn.IsPrimarykey) ? YesNoEnum.N.ToString() : YesNoEnum.Y.ToString();
|
||||
codeGenConfig.WhetherQuery = yesOrNo;
|
||||
codeGenConfig.WhetherImport = yesOrNo;
|
||||
codeGenConfig.WhetherAddUpdate = yesOrNo;
|
||||
codeGenConfig.WhetherTable = yesOrNo;
|
||||
|
||||
codeGenConfig.ColumnKey = tableColumn.ColumnKey;
|
||||
|
||||
codeGenConfig.DataType = tableColumn.DataType;
|
||||
codeGenConfig.EffectType = CodeGenUtil.DataTypeToEff(codeGenConfig.NetType);
|
||||
codeGenConfig.QueryType = GetDefaultQueryType(codeGenConfig); // QueryTypeEnum.eq.ToString();
|
||||
codeGenConfig.OrderNo = orderNo;
|
||||
codeGenConfigs.Add(codeGenConfig);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tableColumn.DictTypeCode))
|
||||
{
|
||||
codeGenConfig.QueryType = "==";
|
||||
codeGenConfig.DictTypeCode = tableColumn.DictTypeCode;
|
||||
codeGenConfig.EffectType = tableColumn.DictTypeCode.EndsWith("Enum") ? "EnumSelector" : "DictSelector";
|
||||
}
|
||||
|
||||
orderNo += 10; // 每个配置排序间隔10
|
||||
}
|
||||
// 多库代码生成---这里要切回主库
|
||||
var provider = _db.AsTenant().GetConnectionScope(SqlSugarConst.MainConfigId);
|
||||
provider.Insertable(codeGenConfigs).ExecuteCommand();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新代码字段:先删除再新增,会保留历史字段操作类型
|
||||
/// </summary>
|
||||
/// <param name="tableColumnOutputList"></param>
|
||||
/// <param name="codeGenId"></param>
|
||||
[NonAction]
|
||||
public async Task UpdateList(List<ColumnOuput> tableColumnOutputList, long codeGenId)
|
||||
{
|
||||
if (tableColumnOutputList == null) return;
|
||||
|
||||
//获取历史数据
|
||||
var oldList = await GetList(new CodeGenConfig() { CodeGenId = codeGenId, });
|
||||
//删除历史数据
|
||||
await DeleteCodeGenConfig(codeGenId);
|
||||
|
||||
var codeGenConfigs = new List<SysCodeGenConfig>();
|
||||
var orderNo = 100;
|
||||
foreach (var tableColumn in tableColumnOutputList)
|
||||
{
|
||||
var oldItem = oldList.FirstOrDefault(u => u.ColumnName == tableColumn.ColumnName);
|
||||
|
||||
var codeGenConfig = new SysCodeGenConfig();
|
||||
|
||||
var yesOrNo = YesNoEnum.Y.ToString();
|
||||
if (Convert.ToBoolean(tableColumn.ColumnKey)) yesOrNo = YesNoEnum.N.ToString();
|
||||
|
||||
if (CodeGenUtil.IsCommonColumn(tableColumn.PropertyName))
|
||||
{
|
||||
codeGenConfig.WhetherCommon = YesNoEnum.Y.ToString();
|
||||
yesOrNo = YesNoEnum.N.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
codeGenConfig.WhetherCommon = YesNoEnum.N.ToString();
|
||||
}
|
||||
|
||||
codeGenConfig.CodeGenId = codeGenId;
|
||||
codeGenConfig.ColumnName = tableColumn.ColumnName; // 字段名
|
||||
codeGenConfig.PropertyName = tableColumn.PropertyName;// 实体属性名
|
||||
codeGenConfig.ColumnLength = tableColumn.ColumnLength;// 长度
|
||||
codeGenConfig.ColumnComment = tableColumn.ColumnComment;
|
||||
codeGenConfig.NetType = tableColumn.NetType;
|
||||
codeGenConfig.DefaultValue = tableColumn.DefaultValue;
|
||||
codeGenConfig.WhetherRetract = YesNoEnum.N.ToString();
|
||||
|
||||
// 生成代码时,主键并不是必要输入项,故一定要排除主键字段
|
||||
codeGenConfig.WhetherRequired = (tableColumn.IsNullable || tableColumn.IsPrimarykey) ? YesNoEnum.N.ToString() : YesNoEnum.Y.ToString();
|
||||
codeGenConfig.WhetherQuery = yesOrNo;
|
||||
codeGenConfig.WhetherImport = yesOrNo;
|
||||
codeGenConfig.WhetherAddUpdate = yesOrNo;
|
||||
codeGenConfig.WhetherTable = yesOrNo;
|
||||
|
||||
codeGenConfig.ColumnKey = tableColumn.ColumnKey;
|
||||
|
||||
codeGenConfig.DataType = tableColumn.DataType;
|
||||
codeGenConfig.EffectType = CodeGenUtil.DataTypeToEff(codeGenConfig.NetType);
|
||||
codeGenConfig.QueryType = GetDefaultQueryType(codeGenConfig); // QueryTypeEnum.eq.ToString();
|
||||
codeGenConfig.OrderNo = orderNo;
|
||||
|
||||
if (oldItem != null)
|
||||
{
|
||||
//如果历史存在,则继承
|
||||
codeGenConfig.WhetherQuery = oldItem.WhetherQuery;
|
||||
codeGenConfig.WhetherImport = oldItem.WhetherImport;
|
||||
codeGenConfig.WhetherAddUpdate = oldItem.WhetherAddUpdate;
|
||||
codeGenConfig.WhetherTable = oldItem.WhetherTable;
|
||||
|
||||
codeGenConfig.EffectType = oldItem.EffectType;
|
||||
codeGenConfig.FkConfigId = oldItem.FkConfigId;
|
||||
codeGenConfig.FkEntityName = oldItem.FkEntityName;
|
||||
codeGenConfig.FkTableName = oldItem.FkTableName;
|
||||
codeGenConfig.FkDisplayColumns = oldItem.FkDisplayColumns;
|
||||
codeGenConfig.FkLinkColumnName = oldItem.FkLinkColumnName;
|
||||
codeGenConfig.FkColumnNetType = oldItem.FkColumnNetType;
|
||||
}
|
||||
|
||||
codeGenConfigs.Add(codeGenConfig);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tableColumn.DictTypeCode))
|
||||
{
|
||||
codeGenConfig.QueryType = "==";
|
||||
codeGenConfig.DictTypeCode = tableColumn.DictTypeCode;
|
||||
codeGenConfig.EffectType = tableColumn.DictTypeCode.EndsWith("Enum") ? "EnumSelector" : "DictSelector";
|
||||
}
|
||||
|
||||
orderNo += 10; // 每个配置排序间隔10
|
||||
}
|
||||
// 多库代码生成---这里要切回主库
|
||||
var provider = _db.AsTenant().GetConnectionScope(SqlSugarConst.MainConfigId);
|
||||
provider.Insertable(codeGenConfigs).ExecuteCommand();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认查询类型
|
||||
/// </summary>
|
||||
/// <param name="codeGenConfig"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetDefaultQueryType(SysCodeGenConfig codeGenConfig)
|
||||
{
|
||||
return (codeGenConfig.NetType?.TrimEnd('?')) switch
|
||||
{
|
||||
"string" => "like",
|
||||
"DateTime" => "~",
|
||||
_ => "==",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,825 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统代码生成器服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 270)]
|
||||
public class SysCodeGenService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly ISqlSugarClient _db;
|
||||
|
||||
private readonly SysCodeGenConfigService _codeGenConfigService;
|
||||
private readonly DbConnectionOptions _dbConnectionOptions;
|
||||
private readonly CodeGenOptions _codeGenOptions;
|
||||
private readonly SysMenuService _sysMenuService;
|
||||
private readonly IViewEngine _viewEngine;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public SysCodeGenService(ISqlSugarClient db,
|
||||
IOptions<DbConnectionOptions> dbConnectionOptions,
|
||||
SysCodeGenConfigService codeGenConfigService,
|
||||
IOptions<CodeGenOptions> codeGenOptions,
|
||||
SysMenuService sysMenuService,
|
||||
UserManager userManager,
|
||||
IViewEngine viewEngine)
|
||||
{
|
||||
_db = db;
|
||||
_viewEngine = viewEngine;
|
||||
_userManager = userManager;
|
||||
_sysMenuService = sysMenuService;
|
||||
_codeGenOptions = codeGenOptions.Value;
|
||||
_codeGenConfigService = codeGenConfigService;
|
||||
_dbConnectionOptions = dbConnectionOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取代码生成分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取代码生成分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysCodeGen>> Page(CodeGenInput input)
|
||||
{
|
||||
return await _db.Queryable<SysCodeGen>()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.TableName), u => u.TableName.Contains(input.TableName.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.BusName), u => u.BusName.Contains(input.BusName.Trim()))
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加代码生成 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加代码生成")]
|
||||
public async Task AddCodeGen(AddCodeGenInput input)
|
||||
{
|
||||
var isExist = await _db.Queryable<SysCodeGen>().Where(u => u.TableName == input.TableName).AnyAsync();
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
|
||||
|
||||
if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList);
|
||||
|
||||
var codeGen = input.Adapt<SysCodeGen>();
|
||||
var newCodeGen = await _db.Insertable(codeGen).ExecuteReturnEntityAsync();
|
||||
|
||||
// 增加配置表
|
||||
_codeGenConfigService.AddList(GetColumnList(input), newCodeGen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新代码生成 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新代码生成")]
|
||||
public async Task UpdateCodeGen(UpdateCodeGenInput input)
|
||||
{
|
||||
var isExist = await _db.Queryable<SysCodeGen>().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
|
||||
|
||||
var oldRecord = await _db.Queryable<SysCodeGen>().FirstAsync(u => u.Id == input.Id);
|
||||
try
|
||||
{
|
||||
// 开启事务
|
||||
_db.AsTenant().BeginTran();
|
||||
if (input.GenerateMenu)
|
||||
{
|
||||
var oldTitle = $"{oldRecord.BusName}管理";
|
||||
var newTitle = $"{input.BusName}管理";
|
||||
var updateObj = await _db.Queryable<SysMenu>().FirstAsync(u => u.Title == oldTitle);
|
||||
if (updateObj != null)
|
||||
{
|
||||
updateObj.Title = newTitle;
|
||||
var result = _db.Updateable(updateObj).UpdateColumns(it => new { it.Title }).ExecuteCommand();
|
||||
_sysMenuService.DeleteMenuCache();
|
||||
}
|
||||
}
|
||||
if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList);
|
||||
var codeGen = input.Adapt<SysCodeGen>();
|
||||
await _db.Updateable(codeGen).ExecuteCommandAsync();
|
||||
|
||||
// 仅当数据表名称发生了变化,才更新配置表
|
||||
//if (oldRecord.TableName != input.TableName)
|
||||
//{
|
||||
await _codeGenConfigService.DeleteCodeGenConfig(codeGen.Id);
|
||||
_codeGenConfigService.AddList(GetColumnList(input.Adapt<AddCodeGenInput>()), codeGen);
|
||||
//}
|
||||
_db.AsTenant().CommitTran();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_db.AsTenant().RollbackTran();
|
||||
throw Oops.Oh(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步代码字段(保留历史作用类型) 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "SyncField"), HttpPost]
|
||||
[DisplayName("同步代码字段")]
|
||||
public async Task SyncCodeFieldGen(UpdateCodeGenInput input)
|
||||
{
|
||||
var isExist = await _db.Queryable<SysCodeGen>().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
|
||||
try
|
||||
{
|
||||
// 开启事务
|
||||
_db.AsTenant().BeginTran();
|
||||
await _codeGenConfigService.UpdateList(GetColumnList(input.Adapt<AddCodeGenInput>()), input.Id);
|
||||
_db.AsTenant().CommitTran();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_db.AsTenant().RollbackTran();
|
||||
throw Oops.Oh(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除代码生成 🔖
|
||||
/// </summary>
|
||||
/// <param name="inputs"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除代码生成")]
|
||||
public async Task DeleteCodeGen(List<DeleteCodeGenInput> inputs)
|
||||
{
|
||||
if (inputs == null || inputs.Count < 1) return;
|
||||
|
||||
var codeGenConfigTaskList = new List<Task>();
|
||||
inputs.ForEach(u =>
|
||||
{
|
||||
_db.Deleteable<SysCodeGen>().In(u.Id).ExecuteCommand();
|
||||
|
||||
// 删除配置表
|
||||
codeGenConfigTaskList.Add(_codeGenConfigService.DeleteCodeGenConfig(u.Id));
|
||||
});
|
||||
await Task.WhenAll(codeGenConfigTaskList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取代码生成详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取代码生成详情")]
|
||||
public async Task<SysCodeGen> GetDetail([FromQuery] QueryCodeGenInput input)
|
||||
{
|
||||
return await _db.Queryable<SysCodeGen>().SingleAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据库库集合 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取数据库库集合")]
|
||||
public async Task<List<DatabaseOutput>> GetDatabaseList()
|
||||
{
|
||||
var dbConfigs = _dbConnectionOptions.ConnectionConfigs;
|
||||
return await Task.FromResult(dbConfigs.Adapt<List<DatabaseOutput>>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据库表(实体)集合 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取数据库表(实体)集合")]
|
||||
public async Task<List<TableOutput>> GetTableList(string configId = SqlSugarConst.MainConfigId)
|
||||
{
|
||||
var provider = _db.AsTenant().GetConnectionScope(configId);
|
||||
var dbTableInfos = provider.DbMaintenance.GetTableInfoList(false); // 不能走缓存,否则切库不起作用
|
||||
var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => configId.Equals(u.ConfigId));
|
||||
|
||||
// var dbTableNames = dbTableInfos.Select(u => u.Name.ToLower()).ToList();
|
||||
IEnumerable<EntityInfo> entityInfos = await GetEntityInfos(configId);
|
||||
|
||||
var tableOutputList = new List<TableOutput>();
|
||||
foreach (var item in entityInfos)
|
||||
{
|
||||
var tbConfigId = item.Type.GetCustomAttribute<TenantAttribute>()?.configId as string ?? SqlSugarConst.MainConfigId;
|
||||
if (item.Type.IsDefined(typeof(LogTableAttribute))) tbConfigId = SqlSugarConst.LogConfigId;
|
||||
if (tbConfigId != configId) continue;
|
||||
|
||||
var table = dbTableInfos.FirstOrDefault(u => string.Equals(u.Name, (config!.DbSettings.EnableUnderLine ? item.DbTableName.ToUnderLine() : item.DbTableName), StringComparison.CurrentCultureIgnoreCase));
|
||||
if (table == null) continue;
|
||||
tableOutputList.Add(new TableOutput
|
||||
{
|
||||
ConfigId = configId,
|
||||
EntityName = item.EntityName,
|
||||
TableName = table.Name,
|
||||
TableComment = item.TableDescription
|
||||
});
|
||||
}
|
||||
return tableOutputList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据表名获取列集合 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据表名获取列集合")]
|
||||
public List<ColumnOuput> GetColumnListByTableName([Required] string tableName, string configId = SqlSugarConst.MainConfigId)
|
||||
{
|
||||
// 切库---多库代码生成用
|
||||
var provider = _db.AsTenant().GetConnectionScope(configId);
|
||||
var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId) ?? throw Oops.Oh(ErrorCodeEnum.D1401);
|
||||
if (config.DbSettings.EnableUnderLine) tableName = tableName.ToUnderLine();
|
||||
// 获取实体类型属性
|
||||
var entityType = provider.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == tableName);
|
||||
if (entityType == null) return null;
|
||||
var properties = GetEntityInfos(configId).Result.First(e => e.DbTableName.EndsWithIgnoreCase(tableName)).Type.GetProperties()
|
||||
.Where(e => e.GetCustomAttribute<SugarColumn>()?.IsIgnore == false).Select(e => new
|
||||
{
|
||||
PropertyName = e.Name,
|
||||
ColumnComment = e.GetCustomAttribute<SugarColumn>()?.ColumnDescription,
|
||||
ColumnName = e.GetCustomAttribute<SugarColumn>()?.ColumnName ?? e.Name
|
||||
}).ToList();
|
||||
// 按原始类型的顺序获取所有实体类型属性(不包含导航属性,会返回null)
|
||||
var columnList = provider.DbMaintenance.GetColumnInfosByTableName(tableName).Select(u => new ColumnOuput
|
||||
{
|
||||
ColumnName = config!.DbSettings.EnableUnderLine ? u.DbColumnName.ToUnderLine() : u.DbColumnName,
|
||||
ColumnKey = u.IsPrimarykey.ToString(),
|
||||
DataType = u.DataType.ToString(),
|
||||
NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
|
||||
ColumnComment = u.ColumnDescription
|
||||
}).ToList();
|
||||
foreach (var column in columnList)
|
||||
{
|
||||
// ToLowerInvariant 将字段名转成小写再比较,避免因大小写不一致导致无法匹配(pgsql创建表会默认全小写,而我们的实体中又是大写,就会匹配不上)
|
||||
var property = properties.FirstOrDefault(e => (config!.DbSettings.EnableUnderLine ? e.ColumnName.ToUnderLine() : e.ColumnName).ToLowerInvariant() == column.ColumnName.ToLowerInvariant());
|
||||
column.ColumnComment ??= property?.ColumnComment;
|
||||
column.PropertyName = property?.PropertyName;
|
||||
}
|
||||
return columnList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据表列(实体属性)集合
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private List<ColumnOuput> GetColumnList([FromQuery] AddCodeGenInput input)
|
||||
{
|
||||
var entityType = GetEntityInfos(input.ConfigId).GetAwaiter().GetResult().FirstOrDefault(u => u.EntityName == input.TableName);
|
||||
if (entityType == null) return null;
|
||||
|
||||
var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
|
||||
var dbTableName = config!.DbSettings.EnableUnderLine ? entityType.DbTableName.ToUnderLine() : entityType.DbTableName;
|
||||
|
||||
// 切库---多库代码生成用
|
||||
var provider = _db.AsTenant().GetConnectionScope(!string.IsNullOrEmpty(input.ConfigId) ? input.ConfigId : SqlSugarConst.MainConfigId);
|
||||
|
||||
var entityBasePropertyNames = CodeGenUtil.GetPropertyInfoArray(typeof(EntityBaseTenant))?.Select(p => p.Name).ToArray();
|
||||
var columnInfos = provider.DbMaintenance.GetColumnInfosByTableName(dbTableName, false);
|
||||
var result = columnInfos.Select(u => new ColumnOuput
|
||||
{
|
||||
// 转下划线后的列名需要再转回来(暂时不转)
|
||||
//ColumnName = config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, entityBasePropertyNames) : u.DbColumnName,
|
||||
ColumnName = u.DbColumnName,
|
||||
ColumnLength = u.Length,
|
||||
IsPrimarykey = u.IsPrimarykey,
|
||||
IsNullable = u.IsNullable,
|
||||
ColumnKey = u.IsPrimarykey.ToString(),
|
||||
NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
|
||||
DataType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
|
||||
ColumnComment = string.IsNullOrWhiteSpace(u.ColumnDescription) ? u.DbColumnName : u.ColumnDescription,
|
||||
DefaultValue = u.DefaultValue,
|
||||
}).ToList();
|
||||
|
||||
// 获取实体的属性信息,赋值给PropertyName属性(CodeFirst模式应以PropertyName为实际使用名称)
|
||||
var entityProperties = entityType.Type.GetProperties();
|
||||
|
||||
for (int i = result.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var columnOutput = result[i];
|
||||
// 先找自定义字段名的,如果找不到就再找自动生成字段名的(并且过滤掉没有SugarColumn的属性)
|
||||
var propertyInfo = entityProperties.FirstOrDefault(u => string.Equals((u.GetCustomAttribute<SugarColumn>()?.ColumnName ?? ""), columnOutput.ColumnName, StringComparison.CurrentCultureIgnoreCase)) ??
|
||||
entityProperties.FirstOrDefault(u => u.GetCustomAttribute<SugarColumn>() != null && u.Name.ToLower() == (config.DbSettings.EnableUnderLine
|
||||
? CodeGenUtil.CamelColumnName(columnOutput.ColumnName, entityBasePropertyNames).ToLower()
|
||||
: columnOutput.ColumnName.ToLower()));
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
columnOutput.PropertyName = propertyInfo.Name;
|
||||
columnOutput.ColumnComment = propertyInfo.GetCustomAttribute<SugarColumn>()!.ColumnDescription;
|
||||
var propertyType = Nullable.GetUnderlyingType(propertyInfo.PropertyType);
|
||||
if (propertyInfo.PropertyType.IsEnum || (propertyType?.IsEnum ?? false))
|
||||
{
|
||||
columnOutput.DictTypeCode = (propertyType ?? propertyInfo.PropertyType).Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
var dict = propertyInfo.GetCustomAttribute<DictAttribute>();
|
||||
if (dict != null) columnOutput.DictTypeCode = dict.DictTypeCode;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.RemoveAt(i); // 移除没有定义此属性的字段
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取库表信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task<IEnumerable<EntityInfo>> GetEntityInfos(string configId)
|
||||
{
|
||||
var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId) ?? throw Oops.Oh(ErrorCodeEnum.D1401);
|
||||
var entityInfos = new List<EntityInfo>();
|
||||
|
||||
var type = typeof(SugarTable);
|
||||
var types = new List<Type>();
|
||||
if (_codeGenOptions.EntityAssemblyNames != null)
|
||||
{
|
||||
types = App.EffectiveTypes.Where(c => c.IsClass)
|
||||
.Where(c => _codeGenOptions.EntityAssemblyNames.Contains(c.Assembly.GetName().Name) || _codeGenOptions.EntityAssemblyNames.Any(name => c.Assembly.GetName().Name!.Contains(name)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
Type[] cosType = types.Where(o => IsMyAttribute(Attribute.GetCustomAttributes(o, true))).ToArray();
|
||||
|
||||
foreach (var ct in cosType)
|
||||
{
|
||||
var sugarAttribute = ct.GetCustomAttributes(type, true).FirstOrDefault();
|
||||
|
||||
var description = "";
|
||||
var des = ct.GetCustomAttributes(typeof(DescriptionAttribute), true);
|
||||
if (des.Length > 0) description = ((DescriptionAttribute)des[0]).Description;
|
||||
|
||||
var dbTableName = sugarAttribute == null || string.IsNullOrWhiteSpace(((SugarTable)sugarAttribute).TableName) ? ct.Name : ((SugarTable)sugarAttribute).TableName;
|
||||
if (config.DbSettings.EnableUnderLine) dbTableName = dbTableName.ToUnderLine();
|
||||
|
||||
entityInfos.Add(new EntityInfo
|
||||
{
|
||||
EntityName = ct.Name,
|
||||
DbTableName = dbTableName,
|
||||
TableDescription = sugarAttribute == null ? description : ((SugarTable)sugarAttribute).TableDescription,
|
||||
Type = ct
|
||||
});
|
||||
}
|
||||
return await Task.FromResult(entityInfos);
|
||||
|
||||
bool IsMyAttribute(Attribute[] o) => o.Any(a => a.GetType() == type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取程序保存位置 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取程序保存位置")]
|
||||
public List<string> GetApplicationNamespaces()
|
||||
{
|
||||
return _codeGenOptions.BackendApplicationNamespaces;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 代码生成到本地 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[DisplayName("代码生成到本地")]
|
||||
public async Task<dynamic> RunLocal(SysCodeGen input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input.GenerateType))
|
||||
input.GenerateType = "200";
|
||||
|
||||
// 先删除该表已生成的菜单列表
|
||||
List<string> targetPathList;
|
||||
var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!);
|
||||
if (input.GenerateType.StartsWith('1'))
|
||||
{
|
||||
targetPathList = GetZipPathList(input);
|
||||
if (Directory.Exists(zipPath)) Directory.Delete(zipPath, true);
|
||||
}
|
||||
else
|
||||
targetPathList = GetTargetPathList(input);
|
||||
|
||||
var (tableFieldList, result) = await RenderTemplateAsync(input);
|
||||
var templatePathList = GetTemplatePathList(input);
|
||||
for (var i = 0; i < templatePathList.Count; i++)
|
||||
{
|
||||
var content = result.GetValueOrDefault(templatePathList[i]?.TrimEnd(".vm"));
|
||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
||||
var dirPath = new DirectoryInfo(targetPathList[i]).Parent!.FullName;
|
||||
if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
|
||||
_ = File.WriteAllTextAsync(targetPathList[i], content, Encoding.UTF8);
|
||||
}
|
||||
|
||||
if (input.GenerateMenu) await AddOrUpdateMenu(input.TableName, input.BusName, input.MenuPid ?? 0, input.MenuIcon, input.PagePath, tableFieldList);
|
||||
|
||||
// 非ZIP压缩返回空
|
||||
if (!input.GenerateType.StartsWith('1')) return null;
|
||||
|
||||
// 判断是否存在同名称文件
|
||||
string downloadPath = zipPath + ".zip";
|
||||
if (File.Exists(downloadPath)) File.Delete(downloadPath);
|
||||
|
||||
// 创建zip文件并返回下载地址
|
||||
ZipFile.CreateFromDirectory(zipPath, downloadPath);
|
||||
return new { url = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}/codeGen/{input.TableName}.zip" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取代码生成预览 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取代码生成预览")]
|
||||
public async Task<Dictionary<string, string>> Preview(SysCodeGen input)
|
||||
{
|
||||
var (_, result) = await RenderTemplateAsync(input);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渲染模板
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<(List<CodeGenConfig> tableFieldList, Dictionary<string, string> result)> RenderTemplateAsync(SysCodeGen input)
|
||||
{
|
||||
var tableFieldList = await _codeGenConfigService.GetList(new CodeGenConfig { CodeGenId = input.Id }); // 字段集合
|
||||
var joinTableList = tableFieldList.Where(u => u.EffectType is "Upload" or "ForeignKey" or "ApiTreeSelector").ToList(); // 需要连表查询的字段
|
||||
|
||||
var data = new CustomViewEngine
|
||||
{
|
||||
ConfigId = input.ConfigId,
|
||||
BusName = input.BusName,
|
||||
PagePath = input.PagePath,
|
||||
NameSpace = input.NameSpace,
|
||||
ClassName = input.TableName,
|
||||
PrintType = input.PrintType,
|
||||
PrintName = input.PrintName,
|
||||
AuthorName = input.AuthorName,
|
||||
ProjectLastName = input.NameSpace!.Split('.').Last(),
|
||||
LowerClassName = input.TableName!.ToFirstLetterLowerCase(),
|
||||
TableUniqueConfigList = input.TableUniqueList ?? new(),
|
||||
|
||||
TableField = tableFieldList,
|
||||
QueryWhetherList = tableFieldList.Where(u => u.WhetherQuery == "Y").ToList(),
|
||||
ImportFieldList = tableFieldList.Where(u => u.WhetherImport == "Y").ToList(),
|
||||
UploadFieldList = tableFieldList.Where(u => u.EffectType == "Upload" || u.EffectType == "Upload_SingleFile").ToList(),
|
||||
PrimaryKeyFieldList = tableFieldList.Where(c => c.ColumnKey == "True").ToList(),
|
||||
AddUpdateFieldList = tableFieldList.Where(u => u.WhetherAddUpdate == "Y").ToList(),
|
||||
ApiTreeFieldList = tableFieldList.Where(u => u.EffectType == "ApiTreeSelector").ToList(),
|
||||
DropdownFieldList = tableFieldList.Where(u => u.EffectType is "ForeignKey" or "ApiTreeSelector").ToList(),
|
||||
DefaultValueList = tableFieldList.Where(u => u.DefaultValue != null && u.DefaultValue.Length > 0).ToList(),
|
||||
|
||||
HasJoinTable = joinTableList.Count > 0,
|
||||
HasDictField = tableFieldList.Any(u => u.EffectType == "DictSelector"),
|
||||
HasEnumField = tableFieldList.Any(u => u.EffectType == "EnumSelector"),
|
||||
HasConstField = tableFieldList.Any(u => u.EffectType == "ConstSelector"),
|
||||
HasLikeQuery = tableFieldList.Any(c => c.WhetherQuery == "Y" && c.QueryType == "like")
|
||||
};
|
||||
|
||||
// 获取模板文件并替换
|
||||
var templatePathList = GetTemplatePathList();
|
||||
var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template");
|
||||
|
||||
var result = new Dictionary<string, string>();
|
||||
foreach (var path in templatePathList)
|
||||
{
|
||||
var templateFilePath = Path.Combine(templatePath, path);
|
||||
if (!File.Exists(templateFilePath)) continue;
|
||||
var tContent = await File.ReadAllTextAsync(templateFilePath);
|
||||
var tResult = await _viewEngine.RunCompileFromCachedAsync(tContent, data, builderAction: builder =>
|
||||
{
|
||||
builder.AddAssemblyReferenceByName("System.Text.RegularExpressions");
|
||||
builder.AddAssemblyReferenceByName("System.Collections");
|
||||
builder.AddAssemblyReferenceByName("System.Linq");
|
||||
|
||||
builder.AddUsing("System.Text.RegularExpressions");
|
||||
builder.AddUsing("System.Collections.Generic");
|
||||
builder.AddUsing("System.Linq");
|
||||
});
|
||||
result.Add(path?.TrimEnd(".vm"), tResult);
|
||||
}
|
||||
return (tableFieldList, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加或更新菜单
|
||||
/// </summary>
|
||||
/// <param name="className"></param>
|
||||
/// <param name="busName"></param>
|
||||
/// <param name="pid"></param>
|
||||
/// <param name="menuIcon"></param>
|
||||
/// <param name="pagePath"></param>
|
||||
/// <param name="tableFieldList"></param>
|
||||
/// <returns></returns>
|
||||
private async Task AddOrUpdateMenu(string className, string busName, long pid, string menuIcon, string pagePath, List<CodeGenConfig> tableFieldList)
|
||||
{
|
||||
var title = $"{busName}管理";
|
||||
var lowerClassName = className.ToFirstLetterLowerCase();
|
||||
var menuType = pid == 0 ? MenuTypeEnum.Dir : MenuTypeEnum.Menu;
|
||||
|
||||
// 查询是否已有主菜单
|
||||
var existingMenu = await _db.Queryable<SysMenu>()
|
||||
.Where(m => m.Title == title && m.Type == menuType && m.Pid == pid)
|
||||
.FirstAsync();
|
||||
|
||||
string parentPath = "";
|
||||
if (pid != 0)
|
||||
{
|
||||
var parent = await _db.Queryable<SysMenu>().FirstAsync(m => m.Id == pid) ?? throw Oops.Oh(ErrorCodeEnum.D1505);
|
||||
parentPath = parent.Path;
|
||||
}
|
||||
|
||||
long menuId;
|
||||
if (existingMenu == null)
|
||||
{
|
||||
// 不存在则新增
|
||||
var newMenu = new SysMenu
|
||||
{
|
||||
Pid = pid,
|
||||
Title = title,
|
||||
Type = menuType,
|
||||
Icon = menuIcon ?? "menu",
|
||||
Path = (pid == 0 ? "/" : parentPath + "/") + className.ToLower(),
|
||||
Component = pid == 0 ? "Layout" : $"/{pagePath}/{lowerClassName}/index"
|
||||
};
|
||||
menuId = await _sysMenuService.AddMenu(newMenu.Adapt<AddMenuInput>());
|
||||
}
|
||||
else
|
||||
{
|
||||
// 存在则更新
|
||||
existingMenu.Icon = menuIcon;
|
||||
existingMenu.Path = (pid == 0 ? "/" : parentPath + "/") + className.ToLower();
|
||||
existingMenu.Component = pid == 0 ? "Layout" : $"/{pagePath}/{lowerClassName}/index";
|
||||
await _sysMenuService.UpdateMenu(existingMenu.Adapt<UpdateMenuInput>());
|
||||
menuId = existingMenu.Id;
|
||||
}
|
||||
|
||||
// 定义应有的按钮
|
||||
var orderNo = 100;
|
||||
var newButtons = new List<SysMenu>
|
||||
{
|
||||
new() { Title = "查询", Permission = $"{lowerClassName}:page", OrderNo = orderNo += 10 },
|
||||
new() { Title = "详情", Permission = $"{lowerClassName}:detail", OrderNo = orderNo += 10 },
|
||||
new() { Title = "增加", Permission = $"{lowerClassName}:add", OrderNo = orderNo += 10 },
|
||||
new() { Title = "编辑", Permission = $"{lowerClassName}:update", OrderNo = orderNo += 10 },
|
||||
new() { Title = "删除", Permission = $"{lowerClassName}:delete", OrderNo = orderNo += 10 },
|
||||
new() { Title = "批量删除", Permission = $"{lowerClassName}:batchDelete", OrderNo = orderNo += 10 },
|
||||
new() { Title = "设置状态", Permission = $"{lowerClassName}:setStatus", OrderNo = orderNo += 10 },
|
||||
new() { Title = "打印", Permission = $"{lowerClassName}:print", OrderNo = orderNo += 10 },
|
||||
new() { Title = "导入", Permission = $"{lowerClassName}:import", OrderNo = orderNo += 10 },
|
||||
new() { Title = "导出", Permission = $"{lowerClassName}:export", OrderNo = orderNo += 10 }
|
||||
};
|
||||
|
||||
if (tableFieldList.Any(u => u.EffectType is "ForeignKey" or "ApiTreeSelector" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y")))
|
||||
{
|
||||
newButtons.Add(new SysMenu
|
||||
{
|
||||
Title = "下拉列表数据",
|
||||
Permission = $"{lowerClassName}:dropdownData",
|
||||
OrderNo = orderNo += 10
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var column in tableFieldList.Where(u => u.EffectType == "Upload" || u.EffectType == "Upload_SingleFile"))
|
||||
{
|
||||
newButtons.Add(new SysMenu
|
||||
{
|
||||
Title = $"上传{column.ColumnComment}",
|
||||
Permission = $"{lowerClassName}:upload{column.PropertyName}",
|
||||
OrderNo = orderNo += 10
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前菜单下的按钮
|
||||
var existingButtons = await _db.Queryable<SysMenu>()
|
||||
.Where(m => m.Pid == menuId && m.Type == MenuTypeEnum.Btn)
|
||||
.ToListAsync();
|
||||
|
||||
var newPermissions = newButtons.Select(b => b.Permission).ToHashSet();
|
||||
|
||||
// 添加或更新按钮
|
||||
foreach (var btn in newButtons)
|
||||
{
|
||||
var match = existingButtons.FirstOrDefault(b => b.Permission == btn.Permission);
|
||||
if (match == null)
|
||||
{
|
||||
btn.Type = MenuTypeEnum.Btn;
|
||||
btn.Pid = menuId;
|
||||
btn.Icon = "";
|
||||
await _sysMenuService.AddMenu(btn.Adapt<AddMenuInput>());
|
||||
}
|
||||
else
|
||||
{
|
||||
match.Title = btn.Title;
|
||||
match.OrderNo = btn.OrderNo;
|
||||
await _sysMenuService.UpdateMenu(match.Adapt<UpdateMenuInput>());
|
||||
}
|
||||
}
|
||||
|
||||
// 删除多余的旧按钮
|
||||
var toDelete = existingButtons.Where(b => !newPermissions.Contains(b.Permission)).ToList();
|
||||
foreach (var del in toDelete)
|
||||
await _sysMenuService.DeleteMenu(new DeleteMenuInput { Id = del.Id });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加菜单
|
||||
/// </summary>
|
||||
/// <param name="className"></param>
|
||||
/// <param name="busName"></param>
|
||||
/// <param name="pid"></param>
|
||||
/// <param name="menuIcon"></param>
|
||||
/// <param name="pagePath"></param>
|
||||
/// <param name="tableFieldList"></param>
|
||||
/// <returns></returns>
|
||||
private async Task AddMenu(string className, string busName, long pid, string menuIcon, string pagePath, List<CodeGenConfig> tableFieldList)
|
||||
{
|
||||
// 删除已存在的菜单
|
||||
var title = $"{busName}管理";
|
||||
await DeleteMenuTree(title, pid == 0 ? MenuTypeEnum.Dir : MenuTypeEnum.Menu);
|
||||
|
||||
var parentMenuPath = "";
|
||||
var lowerClassName = className!.ToFirstLetterLowerCase();
|
||||
if (pid == 0)
|
||||
{
|
||||
// 新增目录,并记录Id
|
||||
var dirMenu = new SysMenu { Pid = 0, Title = title, Type = MenuTypeEnum.Dir, Icon = "robot", Path = "/" + className.ToLower(), Component = "Layout" };
|
||||
pid = await _sysMenuService.AddMenu(dirMenu.Adapt<AddMenuInput>());
|
||||
}
|
||||
else
|
||||
{
|
||||
var parentMenu = await _db.Queryable<SysMenu>().FirstAsync(u => u.Id == pid) ?? throw Oops.Oh(ErrorCodeEnum.D1505);
|
||||
parentMenuPath = parentMenu.Path;
|
||||
}
|
||||
|
||||
// 新增菜单,并记录Id
|
||||
var rootMenu = new SysMenu { Pid = pid, Title = title, Type = MenuTypeEnum.Menu, Icon = menuIcon, Path = $"{parentMenuPath}/{className.ToLower()}", Component = $"/{pagePath}/{lowerClassName}/index" };
|
||||
pid = await _sysMenuService.AddMenu(rootMenu.Adapt<AddMenuInput>());
|
||||
|
||||
var orderNo = 100;
|
||||
var menuList = new List<SysMenu>
|
||||
{
|
||||
new() { Title="查询", Permission=$"{lowerClassName}:page", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="详情", Permission=$"{lowerClassName}:detail", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="增加", Permission=$"{lowerClassName}:add", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="编辑", Permission=$"{lowerClassName}:update", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="删除", Permission=$"{lowerClassName}:delete", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="批量删除", Permission=$"{lowerClassName}:batchDelete", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="设置状态", Permission=$"{lowerClassName}:setStatus", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="打印", Permission=$"{lowerClassName}:print", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="导入", Permission=$"{lowerClassName}:import", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10},
|
||||
new() { Title="导出", Permission=$"{lowerClassName}:export", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}
|
||||
};
|
||||
|
||||
if (tableFieldList.Any(u => u.EffectType is "ForeignKey" or "ApiTreeSelector" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y")))
|
||||
menuList.Add(new SysMenu { Title = "下拉列表数据", Permission = $"{lowerClassName}:dropdownData", Pid = pid, Type = MenuTypeEnum.Btn, OrderNo = orderNo += 10 });
|
||||
|
||||
foreach (var column in tableFieldList.Where(u => u.EffectType == "Upload"))
|
||||
menuList.Add(new SysMenu { Title = $"上传{column.ColumnComment}", Permission = $"{lowerClassName}:upload{column.PropertyName}", Pid = pid, Type = MenuTypeEnum.Btn, OrderNo = orderNo += 10 });
|
||||
|
||||
foreach (var menu in menuList) await _sysMenuService.AddMenu(menu.Adapt<AddMenuInput>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据菜单名称和类型删除关联的菜单树
|
||||
/// </summary>
|
||||
/// <param name="title"></param>
|
||||
/// <param name="type"></param>
|
||||
private async Task DeleteMenuTree(string title, MenuTypeEnum type)
|
||||
{
|
||||
var menuList = await _db.Queryable<SysMenu>().Where(u => u.Title == title && u.Type == type).ToListAsync() ?? new();
|
||||
foreach (var menu in menuList) await _sysMenuService.DeleteMenu(new DeleteMenuInput { Id = menu.Id });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取模板文件路径集合
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static List<string> GetTemplatePathList(SysCodeGen input)
|
||||
{
|
||||
if (input.GenerateType!.Substring(1, 1).Contains('1')) return new() { "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" };
|
||||
if (input.GenerateType.Substring(1, 1).Contains('2')) return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm" };
|
||||
return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取模板文件路径集合
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static List<string> GetTemplatePathList() => new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" };
|
||||
|
||||
/// <summary>
|
||||
/// 设置生成文件路径
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
private List<string> GetTargetPathList(SysCodeGen input)
|
||||
{
|
||||
//var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName);
|
||||
var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.FullName, input.NameSpace!, "Service", input.TableName!);
|
||||
var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs");
|
||||
var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs");
|
||||
var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs");
|
||||
var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs");
|
||||
var frontendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!);
|
||||
var indexPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "index.vue");//
|
||||
var formModalPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "component", "editDialog.vue");
|
||||
var apiJsPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, input.TableName[..1].ToLower() + input.TableName[1..] + ".ts");
|
||||
|
||||
if (input.GenerateType!.Substring(1, 1).Contains('1'))
|
||||
{
|
||||
// 生成到本项目(前端)
|
||||
return new List<string>
|
||||
{
|
||||
indexPath,
|
||||
formModalPath,
|
||||
apiJsPath
|
||||
};
|
||||
}
|
||||
|
||||
if (input.GenerateType.Substring(1, 1).Contains('2'))
|
||||
{
|
||||
// 生成到本项目(后端)
|
||||
return new List<string>
|
||||
{
|
||||
servicePath,
|
||||
inputPath,
|
||||
outputPath,
|
||||
viewPath,
|
||||
};
|
||||
}
|
||||
// 前后端同时生成到本项目
|
||||
return new List<string>
|
||||
{
|
||||
servicePath,
|
||||
inputPath,
|
||||
outputPath,
|
||||
viewPath,
|
||||
indexPath,
|
||||
formModalPath,
|
||||
apiJsPath
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置生成文件路径
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
private List<string> GetZipPathList(SysCodeGen input)
|
||||
{
|
||||
var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!);
|
||||
|
||||
var firstLowerTableName = input.TableName!.ToFirstLetterLowerCase();
|
||||
//var backendPath = Path.Combine(zipPath, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName);
|
||||
var backendPath = Path.Combine(zipPath, input.NameSpace!, "Service", input.TableName);
|
||||
var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs");
|
||||
var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs");
|
||||
var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs");
|
||||
var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs");
|
||||
var frontendPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!);
|
||||
var indexPath = Path.Combine(frontendPath, firstLowerTableName, "index.vue");
|
||||
var formModalPath = Path.Combine(frontendPath, firstLowerTableName, "component", "editDialog.vue");
|
||||
var apiJsPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, firstLowerTableName + ".ts");
|
||||
if (input.GenerateType!.StartsWith("11"))
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
indexPath,
|
||||
formModalPath,
|
||||
apiJsPath
|
||||
};
|
||||
}
|
||||
|
||||
if (input.GenerateType.StartsWith("12"))
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
servicePath,
|
||||
inputPath,
|
||||
outputPath,
|
||||
viewPath
|
||||
};
|
||||
}
|
||||
|
||||
return new List<string>
|
||||
{
|
||||
servicePath,
|
||||
inputPath,
|
||||
outputPath,
|
||||
viewPath,
|
||||
indexPath,
|
||||
formModalPath,
|
||||
apiJsPath
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 接口/动态API输出
|
||||
/// </summary>
|
||||
public class ApiOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 组名称
|
||||
/// </summary>
|
||||
public string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接口名称
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 路由名称
|
||||
/// </summary>
|
||||
public string RouteName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Magicodes.ExporterAndImporter.Core.Filters;
|
||||
using Magicodes.ExporterAndImporter.Core.Models;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 基础存储过程输入类
|
||||
/// </summary>
|
||||
public class BaseProcInput
|
||||
{
|
||||
/// <summary>
|
||||
/// ProcId
|
||||
/// </summary>
|
||||
public string ProcId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据库配置Id
|
||||
/// </summary>
|
||||
public string ConfigId { get; set; } = SqlSugarConst.MainConfigId;
|
||||
|
||||
/// <summary>
|
||||
/// 存储过程输入参数
|
||||
/// </summary>
|
||||
/// <example>{"id":"351060822794565"}</example>
|
||||
public Dictionary<string, object> ProcParams { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带表头名称存储过程输入类
|
||||
/// </summary>
|
||||
public class ExportProcByTMPInput : BaseProcInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 模板名称
|
||||
/// </summary>
|
||||
public string Template { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带表头名称存储过程输入类
|
||||
/// </summary>
|
||||
public class ExportProcInput : BaseProcInput
|
||||
{
|
||||
public Dictionary<string, string> EHeader { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指定导出类名(有排序)存储过程输入类
|
||||
/// </summary>
|
||||
public class ExportProcInput2 : BaseProcInput
|
||||
{
|
||||
public List<string> EHeader { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 前端指定列
|
||||
/// </summary>
|
||||
public class ProcExporterHeaderFilter : IExporterHeaderFilter
|
||||
{
|
||||
private Dictionary<string, Tuple<string, int>> _includeHeader;
|
||||
|
||||
public ProcExporterHeaderFilter(Dictionary<string, Tuple<string, int>> includeHeader)
|
||||
{
|
||||
_includeHeader = includeHeader;
|
||||
}
|
||||
|
||||
public ExporterHeaderInfo Filter(ExporterHeaderInfo exporterHeaderInfo)
|
||||
{
|
||||
if (_includeHeader != null && _includeHeader.Count > 0)
|
||||
{
|
||||
var key = exporterHeaderInfo.PropertyName.ToUpper();
|
||||
if (_includeHeader.ContainsKey(key))
|
||||
{
|
||||
exporterHeaderInfo.DisplayName = _includeHeader[key].Item1;
|
||||
return exporterHeaderInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
exporterHeaderInfo.ExporterHeaderAttribute.Hidden = true;
|
||||
}
|
||||
}
|
||||
return exporterHeaderInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 国密公钥私钥对输出
|
||||
/// </summary>
|
||||
public class SmKeyPairOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 私匙
|
||||
/// </summary>
|
||||
public string PrivateKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公匙
|
||||
/// </summary>
|
||||
public string PublicKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 接口压测输入参数
|
||||
/// </summary>
|
||||
public class StressTestInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 接口请求地址
|
||||
/// </summary>
|
||||
/// <example>https://gitee.com/zuohuaijun/Admin.NET</example>
|
||||
[Required(ErrorMessage = "接口请求地址不能为空")]
|
||||
public string RequestUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求方式
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "请求方式不能为空")]
|
||||
public string RequestMethod { get; set; } = nameof(HttpMethod.Get);
|
||||
|
||||
/// <summary>
|
||||
/// 每轮请求量
|
||||
/// </summary>
|
||||
/// <example>100</example>
|
||||
[Required(ErrorMessage = "每轮请求量不能为空")]
|
||||
[Range(1, 100000, ErrorMessage = "每轮请求量必须为1-100000")]
|
||||
public int? NumberOfRequests { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 压测轮数
|
||||
/// </summary>
|
||||
/// <example>5</example>
|
||||
[Required(ErrorMessage = "压测轮数不能为空")]
|
||||
[Range(1, 10000, ErrorMessage = "压测轮数必须为1-10000")]
|
||||
public int? NumberOfRounds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大并行量(默认为当前主机逻辑处理器的数量)
|
||||
/// </summary>
|
||||
/// <example>500</example>
|
||||
[Range(0, 10000, ErrorMessage = "最大并行量必须为0-10000")]
|
||||
public int? MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount;
|
||||
|
||||
/// <summary>
|
||||
/// 请求参数
|
||||
/// </summary>
|
||||
public List<KeyValuePair<string, string>> RequestParameters { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 请求头参数
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Headers { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 路径参数
|
||||
/// </summary>
|
||||
public Dictionary<string, string> PathParameters { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Query参数
|
||||
/// </summary>
|
||||
public Dictionary<string, string> QueryParameters { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 接口压测输出参数
|
||||
/// </summary>
|
||||
public class StressTestOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 总请求次数
|
||||
/// </summary>
|
||||
public long TotalRequests { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总用时(秒)
|
||||
/// </summary>
|
||||
public double TotalTimeInSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成功请求次数
|
||||
/// </summary>
|
||||
public long SuccessfulRequests { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败请求次数
|
||||
/// </summary>
|
||||
public long FailedRequests { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每秒查询率(QPS)
|
||||
/// </summary>
|
||||
public double QueriesPerSecond { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最小响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double MinResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double MaxResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 平均响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double AverageResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// P10 响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double Percentile10ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// P25 响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double Percentile25ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// P50 响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double Percentile50ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// P75 响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double Percentile75ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// P90 响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double Percentile90ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// P99 响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double Percentile99ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// P999 响应时间(毫秒)
|
||||
/// </summary>
|
||||
public double Percentile999ResponseTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Utilities.Encoders;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统通用服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 101)]
|
||||
[AllowAnonymous]
|
||||
public class SysCommonService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly IApiDescriptionGroupCollectionProvider _apiProvider;
|
||||
private readonly SqlSugarRepository<SysUser> _sysUserRep;
|
||||
private readonly CDConfigOptions _cdConfigOptions;
|
||||
private readonly UserManager _userManager;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public SysCommonService(IApiDescriptionGroupCollectionProvider apiProvider,
|
||||
SqlSugarRepository<SysUser> sysUserRep,
|
||||
IOptions<CDConfigOptions> giteeOptions,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
UserManager userManager)
|
||||
{
|
||||
_sysUserRep = sysUserRep;
|
||||
_apiProvider = apiProvider;
|
||||
_userManager = userManager;
|
||||
_cdConfigOptions = giteeOptions.Value;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取国密公钥私钥对 🏆
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取国密公钥私钥对")]
|
||||
public SmKeyPairOutput GetSmKeyPair()
|
||||
{
|
||||
var kp = GM.GenerateKeyPair();
|
||||
var privateKey = Hex.ToHexString(((ECPrivateKeyParameters)kp.Private).D.ToByteArray()).ToUpper();
|
||||
var publicKey = Hex.ToHexString(((ECPublicKeyParameters)kp.Public).Q.GetEncoded()).ToUpper();
|
||||
|
||||
return new SmKeyPairOutput
|
||||
{
|
||||
PrivateKey = privateKey,
|
||||
PublicKey = publicKey,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有接口/动态API 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取所有接口/动态API")]
|
||||
public List<ApiOutput> GetApiList()
|
||||
{
|
||||
var apiList = new List<ApiOutput>();
|
||||
foreach (var item in _apiProvider.ApiDescriptionGroups.Items)
|
||||
{
|
||||
foreach (var apiDescription in item.Items)
|
||||
{
|
||||
var displayName = apiDescription.TryGetMethodInfo(out MethodInfo apiMethodInfo) ? apiMethodInfo.GetCustomAttribute<DisplayNameAttribute>(true)?.DisplayName : "";
|
||||
|
||||
apiList.Add(new ApiOutput
|
||||
{
|
||||
GroupName = item.GroupName,
|
||||
DisplayName = displayName,
|
||||
RouteName = apiDescription.RelativePath
|
||||
});
|
||||
}
|
||||
}
|
||||
return apiList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载标记错误的临时Excel(全局)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("下载标记错误的临时Excel(全局)")]
|
||||
public async Task<IActionResult> DownloadErrorExcelTemp([FromQuery] string fileName = null)
|
||||
{
|
||||
var userId = App.User?.FindFirst(ClaimConst.UserId)?.Value;
|
||||
var resultStream = App.GetRequiredService<SysCacheService>().Get<MemoryStream>(CacheConst.KeyExcelTemp + userId);
|
||||
|
||||
if (resultStream == null) throw Oops.Oh("错误标记文件已过期。");
|
||||
|
||||
return await Task.FromResult(new FileStreamResult(resultStream, "application/octet-stream")
|
||||
{
|
||||
FileDownloadName = $"{(string.IsNullOrEmpty(fileName) ? "错误标记_" + DateTime.Now.ToString("yyyyMMddhhmmss") : fileName)}.xlsx"
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加密字符串 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[DisplayName("加密字符串")]
|
||||
public dynamic EncryptPlainText([Required] string plainText)
|
||||
{
|
||||
return CryptogramUtil.Encrypt(plainText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接口压测 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("接口压测")]
|
||||
public async Task<StressTestOutput> StressTest(StressTestInput input)
|
||||
{
|
||||
// 限制仅超管用户才能使用此功能
|
||||
if (!_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.SA001);
|
||||
|
||||
var stopwatch = new Stopwatch();
|
||||
var responseTimes = new List<double>(); //响应时间集合
|
||||
input.RequestMethod = input.RequestMethod.ToUpper();
|
||||
long totalRequests = 0, successfulRequests = 0, failedRequests = 0;
|
||||
|
||||
stopwatch.Start();
|
||||
var semaphore = new SemaphoreSlim(input.MaxDegreeOfParallelism!.Value > 0 ? input.MaxDegreeOfParallelism.Value : Environment.ProcessorCount);
|
||||
|
||||
#region 参数构建
|
||||
|
||||
// 构建基础URI(不包括路径和查询参数)
|
||||
var baseUriBuilder = new UriBuilder(input.RequestUri);
|
||||
var queryString = HttpUtility.ParseQueryString(baseUriBuilder.Query);
|
||||
|
||||
// 替换路径参数到baseUriBuilder.Path
|
||||
foreach (var param in input.PathParameters)
|
||||
{
|
||||
baseUriBuilder.Path = baseUriBuilder.Path.Replace($"{{{param.Key}}}", param.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// 构建Query参数
|
||||
foreach (var param in input.QueryParameters)
|
||||
{
|
||||
queryString[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
baseUriBuilder.Query = queryString.ToString() ?? string.Empty;
|
||||
var fullUri = baseUriBuilder.Uri;
|
||||
|
||||
// 创建一次性的HttpRequestMessage模板
|
||||
HttpRequestMessage requestTemplate = CreateRequestMessage(input, fullUri);
|
||||
|
||||
#endregion 参数构建
|
||||
|
||||
var tasks = Enumerable.Range(0, input.NumberOfRounds!.Value * input.NumberOfRequests!.Value).Select(async _ =>
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
var requestStopwatch = new Stopwatch();
|
||||
requestStopwatch.Start();
|
||||
|
||||
using (var request = requestTemplate.DeepCopy())
|
||||
{
|
||||
if (!string.Equals(input.RequestMethod, "GET", StringComparison.OrdinalIgnoreCase) && input.RequestParameters.Any())
|
||||
{
|
||||
var content = new FormUrlEncodedContent(input.RequestParameters);
|
||||
request.Content = content;
|
||||
}
|
||||
|
||||
using (var response = await _httpClient.SendAsync(request))
|
||||
{
|
||||
response.EnsureSuccessStatusCode(); // 抛出错误状态码异常
|
||||
|
||||
requestStopwatch.Stop();
|
||||
responseTimes.Add(requestStopwatch.Elapsed.TotalMilliseconds);
|
||||
Interlocked.Increment(ref successfulRequests);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Interlocked.Increment(ref failedRequests);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Increment(ref totalRequests);
|
||||
semaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
stopwatch.Stop();
|
||||
|
||||
var totalTimeInSeconds = stopwatch.Elapsed.TotalSeconds;
|
||||
var qps = totalTimeInSeconds > 0 ? totalRequests / totalTimeInSeconds : 0;
|
||||
var orderResponseTimes = responseTimes.OrderBy(t => t).ToList();
|
||||
var averageResponseTime = responseTimes.Any() ? responseTimes.Average() : 0;
|
||||
var minResponseTime = responseTimes.Any() ? responseTimes.Min() : 0;
|
||||
var maxResponseTime = responseTimes.Any() ? responseTimes.Max() : 0;
|
||||
|
||||
return new StressTestOutput
|
||||
{
|
||||
TotalRequests = totalRequests,
|
||||
TotalTimeInSeconds = totalTimeInSeconds,
|
||||
SuccessfulRequests = successfulRequests,
|
||||
FailedRequests = failedRequests,
|
||||
QueriesPerSecond = qps,
|
||||
MinResponseTime = minResponseTime,
|
||||
MaxResponseTime = maxResponseTime,
|
||||
AverageResponseTime = averageResponseTime,
|
||||
Percentile10ResponseTime = CalculatePercentile(orderResponseTimes, 0.1),
|
||||
Percentile25ResponseTime = CalculatePercentile(orderResponseTimes, 0.25),
|
||||
Percentile50ResponseTime = CalculatePercentile(orderResponseTimes, 0.5),
|
||||
Percentile75ResponseTime = CalculatePercentile(orderResponseTimes, 0.75),
|
||||
Percentile90ResponseTime = CalculatePercentile(orderResponseTimes, 0.9),
|
||||
Percentile99ResponseTime = CalculatePercentile(orderResponseTimes, 0.99),
|
||||
Percentile999ResponseTime = CalculatePercentile(orderResponseTimes, 0.999)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建请求消息
|
||||
/// </summary>
|
||||
/// <param name="input">输入参数</param>
|
||||
/// <param name="fullUri">url</param>
|
||||
/// <returns></returns>
|
||||
private HttpRequestMessage CreateRequestMessage(StressTestInput input, Uri fullUri)
|
||||
{
|
||||
HttpRequestMessage request = input.RequestMethod switch
|
||||
{
|
||||
"GET" => new HttpRequestMessage(HttpMethod.Get, fullUri),
|
||||
"PUT" => new HttpRequestMessage(HttpMethod.Put, fullUri),
|
||||
"POST" => new HttpRequestMessage(HttpMethod.Post, fullUri),
|
||||
"DELETE" => new HttpRequestMessage(HttpMethod.Delete, fullUri),
|
||||
_ => throw Oops.Bah("请求方式异常")
|
||||
};
|
||||
|
||||
// 设置请求头
|
||||
foreach (var header in input.Headers)
|
||||
{
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算百分位请求耗时
|
||||
/// </summary>
|
||||
/// <param name="times">请求耗时列表</param>
|
||||
/// <param name="percentile">百分位</param>
|
||||
/// <returns></returns>
|
||||
private double CalculatePercentile(List<double> times, double percentile)
|
||||
{
|
||||
if (!times.Any()) return 0;
|
||||
var index = (int)Math.Ceiling(percentile * times.Count) - 1;
|
||||
return times[index < times.Count ? index : times.Count - 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统存储过程服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 102)]
|
||||
public class SysProcService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly ISqlSugarClient _db;
|
||||
|
||||
public SysProcService(ISqlSugarClient db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出存储过程数据-指定列,没有指定的字段会被隐藏 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IActionResult> PocExport2(ExportProcInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
var dt = await db.Ado.UseStoredProcedure().GetDataTableAsync(input.ProcId, input.ProcParams);
|
||||
|
||||
var headers = new Dictionary<string, Tuple<string, int>>();
|
||||
var index = 1;
|
||||
foreach (var val in input.EHeader)
|
||||
{
|
||||
headers.Add(val.Key.ToUpper(), new Tuple<string, int>(val.Value, index));
|
||||
index++;
|
||||
}
|
||||
var excelExporter = new ExcelExporter();
|
||||
var da = await excelExporter.ExportAsByteArray(dt, new ProcExporterHeaderFilter(headers));
|
||||
return new FileContentResult(da, "application/octet-stream") { FileDownloadName = input.ProcId + ".xlsx" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据模板导出存储过程数据 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IActionResult> PocExport(ExportProcByTMPInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
var dt = await db.Ado.UseStoredProcedure().GetDataTableAsync(input.ProcId, input.ProcParams);
|
||||
|
||||
var excelExporter = new ExcelExporter();
|
||||
string template = AppDomain.CurrentDomain.BaseDirectory + "/wwwroot/template/" + input.Template + ".xlsx";
|
||||
var bs = await excelExporter.ExportBytesByTemplate(dt, template);
|
||||
return new FileContentResult(bs, "application/octet-stream") { FileDownloadName = input.ProcId + ".xlsx" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储过程返回表-Oracle、达梦参数顺序不能错 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<DataTable> ProcTable(BaseProcInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
return await db.Ado.UseStoredProcedure().GetDataTableAsync(input.ProcId, input.ProcParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储过程返回数据集-Oracle、达梦参数顺序不能错
|
||||
/// Oracle 返回table、table1,其他返回table1、table2。适用于报表、复杂详细页面等 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<DataSet> CommonDataSet(BaseProcInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
return await db.Ado.UseStoredProcedure().GetDataSetAllAsync(input.ProcId, input.ProcParams);
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 根据配置表获取对映存储过程
|
||||
///// </summary>
|
||||
///// <param name="input"></param>
|
||||
///// <returns></returns>
|
||||
//public async Task<DataTable> ProcEnitybyConfig(BaseProcInput input)
|
||||
//{
|
||||
// var key = "ProcConfig";
|
||||
// var ds = _sysCacheService.Get<Dictionary<string, string>>(key);
|
||||
// if (ds == null || ds.Count == 0 || !ds.ContainsKey(input.ProcId))
|
||||
// {
|
||||
// var datas = await _db.Queryable<ProcConfig>().ToListAsync();
|
||||
// ds = datas.ToDictionary(m => m.ProcId, m => m.ProcName);
|
||||
// _sysCacheService.Set(key, ds);
|
||||
// }
|
||||
// var procName = ds[input.ProcId];
|
||||
// return await _db.Ado.UseStoredProcedure().GetDataTableAsync(procName, input.ProcParams);
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class ConfigInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
public class PageConfigInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组编码
|
||||
/// </summary>
|
||||
public string GroupCode { get; set; }
|
||||
}
|
||||
|
||||
public class AddConfigInput : SysConfig
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateConfigInput : AddConfigInput
|
||||
{
|
||||
}
|
||||
|
||||
public class DeleteConfigInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量配置参数输入
|
||||
/// </summary>
|
||||
public class BatchConfigInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 编码
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性值
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统信息保存输入参数
|
||||
/// </summary>
|
||||
public class InfoSaveInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统图标(Data URI scheme base64 编码)
|
||||
/// </summary>
|
||||
public string LogoBase64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 系统图标文件名
|
||||
/// </summary>
|
||||
public string LogoFileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 水印内容
|
||||
/// </summary>
|
||||
public string Watermark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 系统主标题
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "系统主标题不能为空")]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 系统副标题
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "系统副标题不能为空")]
|
||||
public string ViceTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 系统描述
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "系统描述不能为空")]
|
||||
public string ViceDesc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 版权说明
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "版权说明不能为空")]
|
||||
public string Copyright { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ICP备案号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "ICP备案号不能为空")]
|
||||
public string Icp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ICP地址
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "ICP地址不能为空")]
|
||||
public string IcpUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启用注册功能
|
||||
/// </summary>
|
||||
public YesNoEnum EnableReg { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录二次验证
|
||||
/// </summary>
|
||||
public YesNoEnum SecondVer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图形验证码
|
||||
/// </summary>
|
||||
public YesNoEnum Captcha { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认注册方案Id
|
||||
/// </summary>
|
||||
public virtual long RegWayId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using NewLife.Reflection;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 平台参数配置服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 440)]
|
||||
public class SysConfigService : IDynamicApiController, ITransient
|
||||
{
|
||||
private static readonly SysTenantService SysTenantService = App.GetService<SysTenantService>();
|
||||
private readonly SqlSugarRepository<SysConfig> _sysConfigRep;
|
||||
private readonly SqlSugarRepository<SysTenant> _sysTenantRep;
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public SysConfigService(
|
||||
SqlSugarRepository<SysTenant> sysTenantRep,
|
||||
SqlSugarRepository<SysConfig> sysConfigRep,
|
||||
SysCacheService sysCacheService,
|
||||
UserManager userManager)
|
||||
{
|
||||
_sysCacheService = sysCacheService;
|
||||
_sysConfigRep = sysConfigRep;
|
||||
_sysTenantRep = sysTenantRep;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取参数配置分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取参数配置分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysConfig>> Page(PageConfigInput input)
|
||||
{
|
||||
return await _sysConfigRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name?.Trim()), u => u.Name.Contains(input.Name))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Code?.Trim()), u => u.Code.Contains(input.Code))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
|
||||
.OrderBuilder(input)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取参数配置列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取参数配置列表")]
|
||||
public async Task<List<SysConfig>> List(PageConfigInput input)
|
||||
{
|
||||
return await _sysConfigRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加参数配置 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加参数配置")]
|
||||
public async Task AddConfig(AddConfigInput input)
|
||||
{
|
||||
if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
|
||||
|
||||
var isExist = await _sysConfigRep.IsAnyAsync(u => u.Name == input.Name || u.Code == input.Code);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000);
|
||||
|
||||
await _sysConfigRep.InsertAsync(input.Adapt<SysConfig>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新参数配置 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新参数配置")]
|
||||
public async Task UpdateConfig(UpdateConfigInput input)
|
||||
{
|
||||
if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
|
||||
|
||||
var isExist = await _sysConfigRep.IsAnyAsync(u => (u.Name == input.Name || u.Code == input.Code) && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000);
|
||||
|
||||
var config = input.Adapt<SysConfig>();
|
||||
await _sysConfigRep.AsUpdateable(config).IgnoreColumns(true).ExecuteCommandAsync();
|
||||
|
||||
Remove(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除参数配置 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除参数配置")]
|
||||
public async Task DeleteConfig(DeleteConfigInput input)
|
||||
{
|
||||
var config = await _sysConfigRep.GetFirstAsync(u => u.Id == input.Id);
|
||||
|
||||
// 禁止删除系统参数
|
||||
if (config.SysFlag == YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D9001);
|
||||
|
||||
await _sysConfigRep.DeleteAsync(config);
|
||||
|
||||
Remove(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量删除参数配置 🔖
|
||||
/// </summary>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
|
||||
[DisplayName("批量删除参数配置")]
|
||||
public async Task BatchDeleteConfig(List<long> ids)
|
||||
{
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var config = await _sysConfigRep.GetFirstAsync(u => u.Id == id);
|
||||
|
||||
// 禁止删除系统参数
|
||||
if (config.SysFlag == YesNoEnum.Y) continue;
|
||||
|
||||
await _sysConfigRep.DeleteAsync(config);
|
||||
|
||||
Remove(config);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取参数配置详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取参数配置详情")]
|
||||
public async Task<SysConfig> GetDetail([FromQuery] ConfigInput input)
|
||||
{
|
||||
return await _sysConfigRep.GetFirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取参数配置值
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<T> GetConfigValue<T>(string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code)) return default;
|
||||
|
||||
var value = _sysCacheService.Get<string>($"{CacheConst.KeyConfig}{code}");
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
value = (await _sysConfigRep.AsQueryable().FirstAsync(u => u.Code == code))?.Value;
|
||||
_sysCacheService.Set($"{CacheConst.KeyConfig}{code}", value);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(value)) return default;
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新参数配置值
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task UpdateConfigValue(string code, string value)
|
||||
{
|
||||
var config = await _sysConfigRep.AsQueryable().FirstAsync(u => u.Code == code);
|
||||
if (config == null) return;
|
||||
|
||||
config.Value = value;
|
||||
await _sysConfigRep.AsUpdateable(config).ExecuteCommandAsync();
|
||||
|
||||
Remove(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取分组列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取分组列表")]
|
||||
public async Task<List<string>> GetGroupList()
|
||||
{
|
||||
return await _sysConfigRep.AsQueryable()
|
||||
.GroupBy(u => u.GroupCode)
|
||||
.Select(u => u.GroupCode).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Token 过期时间
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<int> GetTokenExpire()
|
||||
{
|
||||
var tokenExpireStr = await GetConfigValue<string>(ConfigConst.SysTokenExpire);
|
||||
_ = int.TryParse(tokenExpireStr, out var tokenExpire);
|
||||
return tokenExpire == 0 ? 20 : tokenExpire;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 RefreshToken 过期时间
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<int> GetRefreshTokenExpire()
|
||||
{
|
||||
var refreshTokenExpireStr = await GetConfigValue<string>(ConfigConst.SysRefreshTokenExpire);
|
||||
_ = int.TryParse(refreshTokenExpireStr, out var refreshTokenExpire);
|
||||
return refreshTokenExpire == 0 ? 40 : refreshTokenExpire;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新参数配置值
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost]
|
||||
[DisplayName("批量更新参数配置值")]
|
||||
public async Task BatchUpdateConfig(List<BatchConfigInput> input)
|
||||
{
|
||||
foreach (var config in input)
|
||||
{
|
||||
var info = await _sysConfigRep.AsQueryable().FirstAsync(c => c.Code == config.Code);
|
||||
if (info == null || info.SysFlag == YesNoEnum.Y) continue;
|
||||
|
||||
await _sysConfigRep.AsUpdateable().SetColumns(u => u.Value == config.Value).Where(u => u.Code == config.Code).ExecuteCommandAsync();
|
||||
Remove(info);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统信息 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[AllowAnonymous]
|
||||
[DisplayName("获取系统信息")]
|
||||
public async Task<dynamic> GetSysInfo()
|
||||
{
|
||||
var tenant = await SysTenantService.GetCurrentTenantSysInfo();
|
||||
var wayList = await _sysConfigRep.Context.Queryable<SysUserRegWay>()
|
||||
.Where(u => u.TenantId == tenant.Id)
|
||||
.Select(u => new { Label = u.Name, Value = u.Id })
|
||||
.ToListAsync();
|
||||
|
||||
var captcha = await GetConfigValue<bool>(ConfigConst.SysCaptcha);
|
||||
var secondVer = await GetConfigValue<bool>(ConfigConst.SysSecondVer);
|
||||
var hideTenantForLogin = await GetConfigValue<bool>(ConfigConst.SysHideTenantLogin);
|
||||
return new
|
||||
{
|
||||
tenant.Logo,
|
||||
tenant.Title,
|
||||
tenant.ViceTitle,
|
||||
tenant.ViceDesc,
|
||||
tenant.Watermark,
|
||||
tenant.Copyright,
|
||||
tenant.Icp,
|
||||
tenant.IcpUrl,
|
||||
tenant.RegWayId,
|
||||
tenant.EnableReg,
|
||||
SecondVer = secondVer ? YesNoEnum.Y : YesNoEnum.N,
|
||||
Captcha = captcha ? YesNoEnum.Y : YesNoEnum.N,
|
||||
HideTenantForLogin = hideTenantForLogin,
|
||||
WayList = wayList
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存系统信息 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[DisplayName("保存系统信息")]
|
||||
public async Task SaveSysInfo(InfoSaveInput input)
|
||||
{
|
||||
var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == _userManager.TenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
if (!string.IsNullOrEmpty(input.LogoBase64)) SysTenantService.SetLogoUrl(tenant, input.LogoBase64, input.LogoFileName);
|
||||
// await UpdateConfigValue(ConfigConst.SysCaptcha, (input.Captcha == YesNoEnum.Y).ToString());
|
||||
// await UpdateConfigValue(ConfigConst.SysSecondVer, (input.SecondVer == YesNoEnum.Y).ToString());
|
||||
|
||||
tenant.Copy(input);
|
||||
tenant.RegWayId = input.EnableReg == YesNoEnum.Y ? input.RegWayId : null;
|
||||
await _sysTenantRep.Context.Updateable(tenant).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
private void Remove(SysConfig config)
|
||||
{
|
||||
_sysCacheService.Remove($"{CacheConst.KeyConfig}Value:{config.Code}");
|
||||
_sysCacheService.Remove($"{CacheConst.KeyConfig}Remark:{config.Code}");
|
||||
_sysCacheService.Remove($"{CacheConst.KeyConfig}{config.GroupCode}:GroupWithCache");
|
||||
_sysCacheService.Remove($"{CacheConst.KeyConfig}{config.Code}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统租户配置参数服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 440)]
|
||||
public class SysTenantConfigService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly SqlSugarRepository<SysTenantConfig> _sysConfigRep;
|
||||
private readonly SqlSugarRepository<SysTenantConfigData> _sysConfigDataRep;
|
||||
public readonly ISugarQueryable<SysConfig> VSysConfig;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public SysTenantConfigService(SysCacheService sysCacheService,
|
||||
SqlSugarRepository<SysTenantConfig> sysConfigRep,
|
||||
SqlSugarRepository<SysTenantConfigData> sysConfigDataRep,
|
||||
UserManager userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sysCacheService = sysCacheService;
|
||||
_sysConfigRep = sysConfigRep;
|
||||
_sysConfigDataRep = sysConfigDataRep;
|
||||
VSysConfig = _sysConfigRep.AsQueryable()
|
||||
.InnerJoin(
|
||||
_sysConfigDataRep.AsQueryable().WhereIF(!_userManager.SuperAdmin, cv => cv.TenantId == _userManager.TenantId),
|
||||
(c, cv) => c.Id == cv.ConfigId
|
||||
)
|
||||
//.Select<SysConfig>().MergeTable();
|
||||
// 解决PostgreSQL下并启用驼峰转下划线时,报字段不存在,SqlSugar在Select后生成的sql, as后没转下划线导致.
|
||||
.SelectMergeTable((c, cv) => new SysConfig { Id = c.Id.SelectAll(), Value = cv.Value });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取配置参数分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysConfig>> Page(PageConfigInput input)
|
||||
{
|
||||
return await VSysConfig
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name?.Trim()), u => u.Name.Contains(input.Name))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Code?.Trim()), u => u.Code.Contains(input.Code))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
|
||||
.OrderBuilder(input)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取配置参数列表")]
|
||||
public async Task<List<SysConfig>> List(PageConfigInput input)
|
||||
{
|
||||
return await VSysConfig
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加配置参数")]
|
||||
public async Task AddConfig(AddConfigInput input)
|
||||
{
|
||||
var isExist = await _sysConfigRep.IsAnyAsync(u => u.Name == input.Name || u.Code == input.Code);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000);
|
||||
|
||||
var configId = _sysConfigRep.InsertReturnSnowflakeId(input.Adapt<SysTenantConfig>());
|
||||
await _sysConfigDataRep.InsertAsync(new SysTenantConfigData()
|
||||
{
|
||||
ConfigId = configId,
|
||||
Value = input.Value
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新配置参数")]
|
||||
[UnitOfWork]
|
||||
public async Task UpdateConfig(UpdateConfigInput input)
|
||||
{
|
||||
var isExist = await _sysConfigRep.IsAnyAsync(u => (u.Name == input.Name || u.Code == input.Code) && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000);
|
||||
|
||||
var config = input.Adapt<SysTenantConfig>();
|
||||
await _sysConfigRep.AsUpdateable(config).IgnoreColumns(true).ExecuteCommandAsync();
|
||||
var configData = await _sysConfigDataRep.GetFirstAsync(cv => cv.ConfigId == input.Id);
|
||||
if (configData == null)
|
||||
await _sysConfigDataRep.AsInsertable(new SysTenantConfigData() { ConfigId = input.Id, Value = input.Value }).ExecuteCommandAsync();
|
||||
else
|
||||
{
|
||||
configData.Value = input.Value;
|
||||
await _sysConfigDataRep.AsUpdateable(configData).IgnoreColumns(true).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
RemoveConfigCache(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除配置参数")]
|
||||
public async Task DeleteConfig(DeleteConfigInput input)
|
||||
{
|
||||
var config = await _sysConfigRep.GetByIdAsync(input.Id);
|
||||
// 禁止删除系统参数
|
||||
if (config.SysFlag == YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D9001);
|
||||
|
||||
await _sysConfigRep.DeleteAsync(config);
|
||||
await _sysConfigDataRep.DeleteAsync(it => it.TenantId == _userManager.TenantId && it.ConfigId == config.Id);
|
||||
|
||||
RemoveConfigCache(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量删除配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
|
||||
[DisplayName("批量删除配置参数")]
|
||||
public async Task BatchDeleteConfig(List<long> ids)
|
||||
{
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var config = await _sysConfigRep.GetByIdAsync(id);
|
||||
// 禁止删除系统参数
|
||||
if (config.SysFlag == YesNoEnum.Y) continue;
|
||||
|
||||
await _sysConfigRep.DeleteAsync(config);
|
||||
await _sysConfigDataRep.DeleteAsync(it => it.TenantId == _userManager.TenantId && it.ConfigId == config.Id);
|
||||
|
||||
RemoveConfigCache(config);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取配置参数详情")]
|
||||
public async Task<SysConfig> GetDetail([FromQuery] ConfigInput input)
|
||||
{
|
||||
return await VSysConfig.FirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据Code获取配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysConfig> GetConfig(string code)
|
||||
{
|
||||
return await VSysConfig.FirstAsync(u => u.Code == code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据Code获取配置参数值 🔖
|
||||
/// </summary>
|
||||
/// <param name="code">编码</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据Code获取配置参数值")]
|
||||
public async Task<string> GetConfigValueByCode(string code)
|
||||
{
|
||||
return await GetConfigValueByCode<string>(code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数值
|
||||
/// </summary>
|
||||
/// <param name="code">编码</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<string> GetConfigValueByCode(string code, string defaultValue = default)
|
||||
{
|
||||
return await GetConfigValueByCode<string>(code, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型</typeparam>
|
||||
/// <param name="code">编码</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<T> GetConfigValueByCode<T>(string code, T defaultValue = default)
|
||||
{
|
||||
return await GetConfigValueByCode<T>(code, _userManager.TenantId, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型</typeparam>
|
||||
/// <param name="code">编码</param>
|
||||
/// <param name="tenantId">租户Id</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<T> GetConfigValueByCode<T>(string code, long tenantId, T defaultValue = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code)) return defaultValue;
|
||||
|
||||
var value = _sysCacheService.Get<string>($"{CacheConst.KeyTenantConfig}{tenantId}{code}");
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
value = (await VSysConfig.FirstAsync(u => u.Code == code))?.Value;
|
||||
_sysCacheService.Set($"{CacheConst.KeyTenantConfig}{tenantId}{code}", value);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(value)) return defaultValue;
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新配置参数值
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task UpdateConfigValue(string code, string value)
|
||||
{
|
||||
var config = await _sysConfigRep.GetFirstAsync(u => u.Code == code);
|
||||
if (config == null) return;
|
||||
|
||||
await _sysConfigDataRep.AsUpdateable().SetColumns(it => it.Value == value).Where(it => it.TenantId == _userManager.TenantId && it.ConfigId == config.Id).ExecuteCommandAsync();
|
||||
|
||||
RemoveConfigCache(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取分组列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取分组列表")]
|
||||
public async Task<List<string>> GetGroupList()
|
||||
{
|
||||
return await VSysConfig
|
||||
.GroupBy(u => u.GroupCode)
|
||||
.Select(u => u.GroupCode).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新配置参数值 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost]
|
||||
[DisplayName("批量更新配置参数值")]
|
||||
public async Task BatchUpdateConfig(List<BatchConfigInput> input)
|
||||
{
|
||||
foreach (var config in input)
|
||||
{
|
||||
await UpdateConfigValue(config.Code, config.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除配置缓存
|
||||
/// </summary>
|
||||
/// <param name="config"></param>
|
||||
private void RemoveConfigCache(SysTenantConfig config)
|
||||
{
|
||||
_sysCacheService.Remove($"{CacheConst.KeyTenantConfig}{_userManager.TenantId}{config.Code}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统用户配置参数服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 440)]
|
||||
public class SysUserConfigService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly SqlSugarRepository<SysUserConfig> _sysConfigRep;
|
||||
private readonly SqlSugarRepository<SysUserConfigData> _sysConfigDataRep;
|
||||
public readonly ISugarQueryable<SysConfig> VSysConfig;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public SysUserConfigService(SysCacheService sysCacheService,
|
||||
SqlSugarRepository<SysUserConfig> sysConfigRep,
|
||||
SqlSugarRepository<SysUserConfigData> sysConfigDataRep,
|
||||
UserManager userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sysCacheService = sysCacheService;
|
||||
_sysConfigRep = sysConfigRep;
|
||||
_sysConfigDataRep = sysConfigDataRep;
|
||||
VSysConfig = _sysConfigRep.AsQueryable()
|
||||
.LeftJoin(
|
||||
_sysConfigDataRep.AsQueryable().WhereIF(_userManager.SuperAdmin, cv => cv.UserId == _userManager.UserId),
|
||||
(c, cv) => c.Id == cv.ConfigId
|
||||
)
|
||||
//.Select<SysConfig>().MergeTable();
|
||||
// 解决PostgreSQL下并启用驼峰转下划线时,报字段不存在,SqlSugar在Select后生成的sql, as后没转下划线导致.
|
||||
.SelectMergeTable((c, cv) => new SysConfig { Id = c.Id.SelectAll(), Value = cv.Value });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取配置参数分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysConfig>> Page(PageConfigInput input)
|
||||
{
|
||||
return await VSysConfig
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name?.Trim()), u => u.Name.Contains(input.Name))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Code?.Trim()), u => u.Code.Contains(input.Code))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
|
||||
.OrderBuilder(input)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取配置参数列表")]
|
||||
public async Task<List<SysConfig>> List(PageConfigInput input)
|
||||
{
|
||||
return await VSysConfig
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加配置参数")]
|
||||
public async Task AddConfig(AddConfigInput input)
|
||||
{
|
||||
var isExist = await _sysConfigRep.IsAnyAsync(u => u.Name == input.Name || u.Code == input.Code);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000);
|
||||
|
||||
var configId = _sysConfigRep.InsertReturnSnowflakeId(input.Adapt<SysUserConfig>());
|
||||
await _sysConfigDataRep.InsertAsync(new SysUserConfigData()
|
||||
{
|
||||
UserId = _userManager.UserId,
|
||||
ConfigId = configId,
|
||||
Value = input.Value
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新配置参数")]
|
||||
[UnitOfWork]
|
||||
public async Task UpdateConfig(UpdateConfigInput input)
|
||||
{
|
||||
var isExist = await _sysConfigRep.IsAnyAsync(u => (u.Name == input.Name || u.Code == input.Code) && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000);
|
||||
|
||||
var config = input.Adapt<SysUserConfig>();
|
||||
await _sysConfigRep.AsUpdateable(config).IgnoreColumns(true).ExecuteCommandAsync();
|
||||
var configData = await _sysConfigDataRep.GetFirstAsync(cv => cv.ConfigId == input.Id);
|
||||
if (configData == null)
|
||||
await _sysConfigDataRep.AsInsertable(new SysUserConfigData() { UserId = _userManager.UserId, ConfigId = input.Id, Value = input.Value }).ExecuteCommandAsync();
|
||||
else
|
||||
{
|
||||
configData.Value = input.Value;
|
||||
await _sysConfigDataRep.AsUpdateable(configData).IgnoreColumns(true).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
RemoveConfigCache(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除配置参数")]
|
||||
public async Task DeleteConfig(DeleteConfigInput input)
|
||||
{
|
||||
var config = await _sysConfigRep.GetByIdAsync(input.Id);
|
||||
// 禁止删除系统参数
|
||||
if (config.SysFlag == YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D9001);
|
||||
|
||||
await _sysConfigRep.DeleteAsync(config);
|
||||
await _sysConfigDataRep.DeleteAsync(it => it.UserId == _userManager.UserId && it.ConfigId == config.Id);
|
||||
|
||||
RemoveConfigCache(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量删除配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
|
||||
[DisplayName("批量删除配置参数")]
|
||||
public async Task BatchDeleteConfig(List<long> ids)
|
||||
{
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var config = await _sysConfigRep.GetByIdAsync(id);
|
||||
// 禁止删除系统参数
|
||||
if (config.SysFlag == YesNoEnum.Y) continue;
|
||||
|
||||
await _sysConfigRep.DeleteAsync(config);
|
||||
await _sysConfigDataRep.DeleteAsync(it => it.UserId == _userManager.UserId && it.ConfigId == config.Id);
|
||||
|
||||
RemoveConfigCache(config);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取配置参数详情")]
|
||||
public async Task<SysConfig> GetDetail([FromQuery] ConfigInput input)
|
||||
{
|
||||
return await VSysConfig.FirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据Code获取配置参数 🔖
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysConfig> GetConfig(string code)
|
||||
{
|
||||
return await VSysConfig.FirstAsync(u => u.Code == code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据Code获取配置参数值 🔖
|
||||
/// </summary>
|
||||
/// <param name="code">编码</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据Code获取配置参数值")]
|
||||
public async Task<string> GetConfigValueByCode(string code)
|
||||
{
|
||||
return await GetConfigValueByCode<string>(code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数值
|
||||
/// </summary>
|
||||
/// <param name="code">编码</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<string> GetConfigValueByCode(string code, string defaultValue = default)
|
||||
{
|
||||
return await GetConfigValueByCode<string>(code, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型</typeparam>
|
||||
/// <param name="code">编码</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<T> GetConfigValueByCode<T>(string code, T defaultValue = default)
|
||||
{
|
||||
return await GetConfigValueByCode<T>(code, _userManager.UserId, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置参数值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型</typeparam>
|
||||
/// <param name="code">编码</param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<T> GetConfigValueByCode<T>(string code, long userId, T defaultValue = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code)) return defaultValue;
|
||||
|
||||
var value = _sysCacheService.Get<string>($"{CacheConst.KeyUserConfig}{userId}{code}");
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
value = (await VSysConfig.FirstAsync(u => u.Code == code))?.Value;
|
||||
_sysCacheService.Set($"{CacheConst.KeyUserConfig}{userId}{code}", value);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(value)) return defaultValue;
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新配置参数值
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task UpdateConfigValue(string code, string value)
|
||||
{
|
||||
var config = await _sysConfigRep.GetFirstAsync(u => u.Code == code);
|
||||
if (config == null) return;
|
||||
|
||||
await _sysConfigDataRep.AsUpdateable().SetColumns(it => it.Value == value).Where(it => it.UserId == _userManager.UserId && it.ConfigId == config.Id).ExecuteCommandAsync();
|
||||
|
||||
RemoveConfigCache(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取分组列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取分组列表")]
|
||||
public async Task<List<string>> GetGroupList()
|
||||
{
|
||||
return await _sysConfigRep.AsQueryable()
|
||||
.GroupBy(u => u.GroupCode)
|
||||
.Select(u => u.GroupCode).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新配置参数值 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost]
|
||||
[DisplayName("批量更新配置参数值")]
|
||||
public async Task BatchUpdateConfig(List<BatchConfigInput> input)
|
||||
{
|
||||
foreach (var config in input)
|
||||
{
|
||||
await UpdateConfigValue(config.Code, config.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除配置缓存
|
||||
/// </summary>
|
||||
/// <param name="config"></param>
|
||||
private void RemoveConfigCache(SysUserConfig config)
|
||||
{
|
||||
_sysCacheService.Remove($"{CacheConst.KeyUserConfig}{_userManager.UserId}{config.Code}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class ConstOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码
|
||||
/// </summary>
|
||||
public dynamic Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展字段
|
||||
/// </summary>
|
||||
public dynamic Data { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统常量服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 280)]
|
||||
public class SysConstService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
|
||||
public SysConstService(SysCacheService sysCacheService)
|
||||
{
|
||||
_sysCacheService = sysCacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有常量列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取所有常量列表")]
|
||||
public async Task<List<ConstOutput>> GetList()
|
||||
{
|
||||
var key = $"{CacheConst.KeyConst}list";
|
||||
var constList = _sysCacheService.Get<List<ConstOutput>>(key);
|
||||
if (constList == null)
|
||||
{
|
||||
var typeList = GetConstAttributeList();
|
||||
constList = typeList.Select(u => new ConstOutput
|
||||
{
|
||||
Name = u.CustomAttributes.ToList().FirstOrDefault()?.ConstructorArguments.ToList().FirstOrDefault().Value?.ToString() ?? u.Name,
|
||||
Code = u.Name,
|
||||
Data = GetData(Convert.ToString(u.Name))
|
||||
}).ToList();
|
||||
_sysCacheService.Set(key, constList);
|
||||
}
|
||||
return await Task.FromResult(constList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据类名获取常量数据 🔖
|
||||
/// </summary>
|
||||
/// <param name="typeName"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据类名获取常量数据")]
|
||||
public async Task<List<ConstOutput>> GetData([Required] string typeName)
|
||||
{
|
||||
var key = $"{CacheConst.KeyConst}{typeName.ToUpper()}";
|
||||
var constList = _sysCacheService.Get<List<ConstOutput>>(key);
|
||||
if (constList == null)
|
||||
{
|
||||
var typeList = GetConstAttributeList();
|
||||
var type = typeList.FirstOrDefault(u => u.Name == typeName);
|
||||
if (type != null)
|
||||
{
|
||||
var isEnum = type.BaseType!.Name == "Enum";
|
||||
constList = type.GetFields()?
|
||||
.Where(isEnum, u => u.FieldType.Name == typeName)
|
||||
.Select(u => new ConstOutput
|
||||
{
|
||||
Name = u.Name,
|
||||
Code = isEnum ? (int)u.GetValue(BindingFlags.Instance)! : u.GetValue(BindingFlags.Instance)
|
||||
}).ToList();
|
||||
_sysCacheService.Set(key, constList);
|
||||
}
|
||||
}
|
||||
return await Task.FromResult(constList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取常量特性类型列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private List<Type> GetConstAttributeList()
|
||||
{
|
||||
return App.EffectiveTypes.Where(u => u.CustomAttributes.Any(c => c.AttributeType == typeof(ConstAttribute))).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class CreateEntityInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 表名
|
||||
/// </summary>
|
||||
/// <example>student</example>
|
||||
public string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实体名
|
||||
/// </summary>
|
||||
/// <example>Student</example>
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 基类名
|
||||
/// </summary>
|
||||
/// <example>AutoIncrementEntity</example>
|
||||
public string BaseClassName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 导出位置
|
||||
/// </summary>
|
||||
/// <example>Web.Application</example>
|
||||
public string Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 库标识
|
||||
/// </summary>
|
||||
public string ConfigId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class CreateSeedDataInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 库标识
|
||||
/// </summary>
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 表名
|
||||
/// </summary>
|
||||
/// <example>student</example>
|
||||
public string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实体名称
|
||||
/// </summary>
|
||||
/// <example>Student</example>
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 种子名称
|
||||
/// </summary>
|
||||
/// <example>Student</example>
|
||||
public string SeedDataName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 导出位置
|
||||
/// </summary>
|
||||
/// <example>Web.Application</example>
|
||||
public string Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 后缀
|
||||
/// </summary>
|
||||
/// <example>Web.Application</example>
|
||||
public string Suffix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过滤已有数据
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 如果数据在其它不同名的已有的种子类型的数据中出现过,就不生成这个数据
|
||||
/// 主要用于生成菜单功能,菜单功能往往与子项目绑定,如果生成完整数据就会导致菜单项多处理重复。
|
||||
/// </remarks>
|
||||
public bool FilterExistingData { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class DbColumnInput
|
||||
{
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
public string TableName { get; set; }
|
||||
|
||||
public string DbColumnName { get; set; }
|
||||
|
||||
public string DataType { get; set; }
|
||||
|
||||
public int Length { get; set; }
|
||||
|
||||
public string ColumnDescription { get; set; }
|
||||
|
||||
public int IsNullable { get; set; }
|
||||
|
||||
public int IsIdentity { get; set; }
|
||||
|
||||
public int IsPrimarykey { get; set; }
|
||||
|
||||
public int DecimalDigits { get; set; }
|
||||
|
||||
public string DefaultValue { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateDbColumnInput
|
||||
{
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
public string TableName { get; set; }
|
||||
|
||||
public string ColumnName { get; set; }
|
||||
|
||||
public string OldColumnName { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public string DefaultValue { get; set; }
|
||||
}
|
||||
|
||||
public class MoveDbColumnInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库配置ID
|
||||
/// </summary>
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标表名
|
||||
/// </summary>
|
||||
public string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///要移动的列名
|
||||
/// </summary>
|
||||
public string ColumnName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 移动到该列后方(为空时移动到首列)
|
||||
/// </summary>
|
||||
public string AfterColumnName { get; set; }
|
||||
}
|
||||
|
||||
public class DeleteDbColumnInput
|
||||
{
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
public string TableName { get; set; }
|
||||
|
||||
public string DbColumnName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class DbColumnOutput
|
||||
{
|
||||
public string TableName { get; set; }
|
||||
|
||||
public int TableId { get; set; }
|
||||
|
||||
public string DbColumnName { get; set; }
|
||||
|
||||
public string PropertyName { get; set; }
|
||||
|
||||
public string DataType { get; set; }
|
||||
|
||||
public object PropertyType { get; set; }
|
||||
|
||||
public int Length { get; set; }
|
||||
|
||||
public string ColumnDescription { get; set; }
|
||||
|
||||
public string DefaultValue { get; set; }
|
||||
|
||||
public bool IsNullable { get; set; }
|
||||
|
||||
public bool IsIdentity { get; set; }
|
||||
|
||||
public bool IsPrimarykey { get; set; }
|
||||
|
||||
public object Value { get; set; }
|
||||
|
||||
public int DecimalDigits { get; set; }
|
||||
|
||||
public int Scale { get; set; }
|
||||
|
||||
public bool IsArray { get; set; }
|
||||
|
||||
public bool IsJson { get; set; }
|
||||
|
||||
public bool? IsUnsigned { get; set; }
|
||||
|
||||
public int CreateTableFieldSort { get; set; }
|
||||
|
||||
internal object SqlParameterDbType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class DbTableInput
|
||||
{
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
public string TableName { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public List<DbColumnInput> DbColumnInfoList { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateDbTableInput
|
||||
{
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
public string TableName { get; set; }
|
||||
|
||||
public string OldTableName { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public class DeleteDbTableInput
|
||||
{
|
||||
public string ConfigId { get; set; }
|
||||
|
||||
public string TableName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class VisualDb
|
||||
{
|
||||
public string ConfigId { get; set; }
|
||||
public string DbNickName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 库表可视化
|
||||
/// </summary>
|
||||
public class VisualDbTable
|
||||
{
|
||||
public List<VisualTable> VisualTableList { get; set; }
|
||||
|
||||
public List<VisualColumn> VisualColumnList { get; set; }
|
||||
|
||||
public List<ColumnRelation> ColumnRelationList { get; set; }
|
||||
}
|
||||
|
||||
public class VisualTable
|
||||
{
|
||||
public string TableName { get; set; }
|
||||
|
||||
public string TableComents { get; set; }
|
||||
|
||||
public int X { get; set; }
|
||||
|
||||
public int Y { get; set; }
|
||||
}
|
||||
|
||||
public class VisualColumn
|
||||
{
|
||||
public string TableName { get; set; }
|
||||
|
||||
public string ColumnName { get; set; }
|
||||
|
||||
public string DataType { get; set; }
|
||||
|
||||
public string DataLength { get; set; }
|
||||
|
||||
public string ColumnDescription { get; set; }
|
||||
}
|
||||
|
||||
public class ColumnRelation
|
||||
{
|
||||
public string SourceTableName { get; set; }
|
||||
|
||||
public string SourceColumnName { get; set; }
|
||||
|
||||
public string Type { get; set; }
|
||||
|
||||
public string TargetTableName { get; set; }
|
||||
|
||||
public string TargetColumnName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 保存标注了JsonIgnore的Property的值信息
|
||||
/// </summary>
|
||||
public class JsonIgnoredPropertyData
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录索引
|
||||
/// </summary>
|
||||
public int RecordIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性名
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性值描述
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Npgsql;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统数据库管理服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 250)]
|
||||
public class SysDatabaseService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly ISqlSugarClient _db;
|
||||
private readonly IViewEngine _viewEngine;
|
||||
private readonly CodeGenOptions _codeGenOptions;
|
||||
|
||||
public SysDatabaseService(ISqlSugarClient db,
|
||||
IViewEngine viewEngine,
|
||||
IOptions<CodeGenOptions> codeGenOptions)
|
||||
{
|
||||
_db = db;
|
||||
_viewEngine = viewEngine;
|
||||
_codeGenOptions = codeGenOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取库列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取库列表")]
|
||||
public List<VisualDb> GetList()
|
||||
{
|
||||
return App.GetOptions<DbConnectionOptions>().ConnectionConfigs.Select(u => new VisualDb { ConfigId = u.ConfigId.ToString(), DbNickName = u.DbNickName }).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可视化库表结构 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取可视化库表结构")]
|
||||
public VisualDbTable GetVisualDbTable()
|
||||
{
|
||||
var visualTableList = new List<VisualTable>();
|
||||
var visualColumnList = new List<VisualColumn>();
|
||||
var columnRelationList = new List<ColumnRelation>();
|
||||
var dbOptions = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.First(u => u.ConfigId.ToString() == SqlSugarConst.MainConfigId);
|
||||
|
||||
// 遍历所有实体获取所有库表结构
|
||||
var random = new Random();
|
||||
var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false)).ToList();
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
var entityInfo = _db.EntityMaintenance.GetEntityInfoNoCache(entityType);
|
||||
|
||||
var visualTable = new VisualTable
|
||||
{
|
||||
TableName = entityInfo.DbTableName,
|
||||
TableComents = entityInfo.TableDescription + entityInfo.DbTableName,
|
||||
X = random.Next(5000),
|
||||
Y = random.Next(5000)
|
||||
};
|
||||
visualTableList.Add(visualTable);
|
||||
|
||||
foreach (EntityColumnInfo columnInfo in entityInfo.Columns)
|
||||
{
|
||||
var visualColumn = new VisualColumn
|
||||
{
|
||||
TableName = columnInfo.DbTableName,
|
||||
ColumnName = dbOptions.DbSettings.EnableUnderLine ? columnInfo.DbColumnName.ToUnderLine() : columnInfo.DbColumnName,
|
||||
DataType = columnInfo.PropertyInfo.PropertyType.Name,
|
||||
DataLength = columnInfo.Length.ToString(),
|
||||
ColumnDescription = columnInfo.ColumnDescription,
|
||||
};
|
||||
visualColumnList.Add(visualColumn);
|
||||
|
||||
// 根据导航配置获取表之间关联关系
|
||||
if (columnInfo.Navigat != null)
|
||||
{
|
||||
var name1 = columnInfo.Navigat.GetName();
|
||||
var name2 = columnInfo.Navigat.GetName2();
|
||||
var targetColumnName = string.IsNullOrEmpty(name2) ? "Id" : name2;
|
||||
var relation = new ColumnRelation
|
||||
{
|
||||
SourceTableName = columnInfo.DbTableName,
|
||||
SourceColumnName = dbOptions.DbSettings.EnableUnderLine ? name1.ToUnderLine() : name1,
|
||||
Type = columnInfo.Navigat.GetNavigateType() == NavigateType.OneToOne ? "ONE_TO_ONE" : "ONE_TO_MANY",
|
||||
TargetTableName = dbOptions.DbSettings.EnableUnderLine ? columnInfo.DbColumnName.ToUnderLine() : columnInfo.DbColumnName,
|
||||
TargetColumnName = dbOptions.DbSettings.EnableUnderLine ? targetColumnName.ToUnderLine() : targetColumnName
|
||||
};
|
||||
columnRelationList.Add(relation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new VisualDbTable { VisualTableList = visualTableList, VisualColumnList = visualColumnList, ColumnRelationList = columnRelationList };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字段列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="tableName">表名</param>
|
||||
/// <param name="configId">ConfigId</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取字段列表")]
|
||||
public List<DbColumnOutput> GetColumnList(string tableName, string configId = SqlSugarConst.MainConfigId)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(configId);
|
||||
return string.IsNullOrWhiteSpace(tableName) ? new List<DbColumnOutput>() : db.DbMaintenance.GetColumnInfosByTableName(tableName, false).Adapt<List<DbColumnOutput>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据库数据类型列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="configId"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取数据库数据类型列表")]
|
||||
public List<string> GetDbTypeList(string configId = SqlSugarConst.MainConfigId)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(configId);
|
||||
return db.DbMaintenance.GetDbTypes().OrderBy(u => u).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加列 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "AddColumn"), HttpPost]
|
||||
[DisplayName("增加列")]
|
||||
public void AddColumn(DbColumnInput input)
|
||||
{
|
||||
var column = new DbColumnInfo
|
||||
{
|
||||
ColumnDescription = input.ColumnDescription,
|
||||
DbColumnName = input.DbColumnName,
|
||||
IsIdentity = input.IsIdentity == 1,
|
||||
IsNullable = input.IsNullable == 1,
|
||||
IsPrimarykey = input.IsPrimarykey == 1,
|
||||
Length = input.Length,
|
||||
DecimalDigits = input.DecimalDigits,
|
||||
DataType = input.DataType
|
||||
};
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
db.DbMaintenance.AddColumn(input.TableName, column);
|
||||
// 默认值直接添加报错
|
||||
if (!string.IsNullOrWhiteSpace(input.DefaultValue))
|
||||
{
|
||||
db.DbMaintenance.AddDefaultValue(input.TableName, column.DbColumnName, input.DefaultValue);
|
||||
}
|
||||
db.DbMaintenance.AddColumnRemark(input.DbColumnName, input.TableName, input.ColumnDescription);
|
||||
if (column.IsPrimarykey) db.DbMaintenance.AddPrimaryKey(input.TableName, input.DbColumnName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除列 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "DeleteColumn"), HttpPost]
|
||||
[DisplayName("删除列")]
|
||||
public void DeleteColumn(DeleteDbColumnInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
db.DbMaintenance.DropColumn(input.TableName, input.DbColumnName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑列 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "UpdateColumn"), HttpPost]
|
||||
[DisplayName("编辑列")]
|
||||
public void UpdateColumn(UpdateDbColumnInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
|
||||
// 前端未修改列名时,不进行重命名操作,避免报错
|
||||
if (input.OldColumnName != input.ColumnName)
|
||||
{
|
||||
db.DbMaintenance.RenameColumn(input.TableName, input.OldColumnName, input.ColumnName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.DefaultValue))
|
||||
{
|
||||
db.DbMaintenance.AddDefaultValue(input.TableName, input.ColumnName, input.DefaultValue);
|
||||
}
|
||||
//if (db.DbMaintenance.IsAnyColumnRemark(input.ColumnName, input.TableName))
|
||||
//{
|
||||
// db.DbMaintenance.DeleteColumnRemark(input.ColumnName, input.TableName);
|
||||
//}
|
||||
|
||||
db.DbMaintenance.AddColumnRemark(input.ColumnName, input.TableName, string.IsNullOrWhiteSpace(input.Description) ? input.ColumnName : input.Description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移动列位置 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "MoveColumn"), HttpPost]
|
||||
[DisplayName("移动列")]
|
||||
public void MoveColumn(MoveDbColumnInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
var dbMaintenance = db.DbMaintenance;
|
||||
|
||||
var dbType = db.CurrentConnectionConfig.DbType;
|
||||
|
||||
var columns = dbMaintenance.GetColumnInfosByTableName(input.TableName, false);
|
||||
var targetColumn = columns.FirstOrDefault(c =>
|
||||
c.DbColumnName.Equals(input.ColumnName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (targetColumn == null)
|
||||
throw new Exception($"列 {input.ColumnName} 在表 {input.TableName} 中不存在");
|
||||
|
||||
switch (dbType)
|
||||
{
|
||||
case SqlSugar.DbType.MySql:
|
||||
MoveColumnInMySQL(db, input.TableName, input.ColumnName, input.AfterColumnName);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"暂不支持 {dbType} 数据库的列移动操作");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取列定义
|
||||
/// </summary>
|
||||
/// <param name="db"></param>
|
||||
/// <param name="tableName"></param>
|
||||
/// <param name="columnName"></param>
|
||||
/// <param name="noDefault"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
private string GetColumnDefinitionInMySQL(ISqlSugarClient db, string tableName, string columnName, bool noDefault = false)
|
||||
{
|
||||
var columnDef = db.Ado.SqlQuery<dynamic>(
|
||||
$"SHOW FULL COLUMNS FROM `{tableName}` WHERE Field = '{columnName}'"
|
||||
).FirstOrDefault();
|
||||
|
||||
if (columnDef == null)
|
||||
throw new Exception($"Column {columnName} not found");
|
||||
|
||||
var definition = new StringBuilder();
|
||||
definition.Append($"`{columnName}` "); // 列名
|
||||
definition.Append($"{columnDef.Type} "); // 数据类型
|
||||
|
||||
// 处理约束条件
|
||||
definition.Append(columnDef.Null == "YES" ? "NULL " : "NOT NULL ");
|
||||
if (columnDef.Default != null && !noDefault)
|
||||
definition.Append($"DEFAULT '{columnDef.Default}' ");
|
||||
if (!string.IsNullOrEmpty(columnDef.Extra))
|
||||
definition.Append($"{columnDef.Extra} ");
|
||||
if (!string.IsNullOrEmpty(columnDef.Comment))
|
||||
definition.Append($"COMMENT '{columnDef.Comment.Replace("'", "''")}'");
|
||||
|
||||
return definition.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MySQL 列移动实现
|
||||
/// </summary>
|
||||
/// <param name="db"></param>
|
||||
/// <param name="tableName"></param>
|
||||
/// <param name="columnName"></param>
|
||||
/// <param name="afterColumnName"></param>
|
||||
private void MoveColumnInMySQL(ISqlSugarClient db, string tableName, string columnName, string afterColumnName)
|
||||
{
|
||||
var definition = GetColumnDefinitionInMySQL(db, tableName, columnName);
|
||||
var sql = new StringBuilder();
|
||||
sql.Append($"ALTER TABLE `{tableName}` MODIFY COLUMN {definition}");
|
||||
|
||||
if (string.IsNullOrEmpty(afterColumnName))
|
||||
sql.Append(" FIRST");
|
||||
else
|
||||
sql.Append($" AFTER `{afterColumnName}`");
|
||||
|
||||
db.Ado.ExecuteCommand(sql.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取表列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="configId">ConfigId</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取表列表")]
|
||||
public List<DbTableInfo> GetTableList(string configId = SqlSugarConst.MainConfigId)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(configId);
|
||||
return db.DbMaintenance.GetTableInfoList(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "AddTable"), HttpPost]
|
||||
[DisplayName("增加表")]
|
||||
public void AddTable(DbTableInput input)
|
||||
{
|
||||
if (input.DbColumnInfoList == null || !input.DbColumnInfoList.Any())
|
||||
throw Oops.Oh(ErrorCodeEnum.db1000);
|
||||
|
||||
if (input.DbColumnInfoList.GroupBy(u => u.DbColumnName).Any(u => u.Count() > 1))
|
||||
throw Oops.Oh(ErrorCodeEnum.db1002);
|
||||
|
||||
var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
var typeBuilder = db.DynamicBuilder().CreateClass(input.TableName, new SugarTable() { TableName = input.TableName, TableDescription = input.Description });
|
||||
input.DbColumnInfoList.ForEach(u =>
|
||||
{
|
||||
var dbColumnName = config!.DbSettings.EnableUnderLine ? u.DbColumnName.Trim().ToUnderLine() : u.DbColumnName.Trim();
|
||||
// 虚拟类都默认string类型,具体以列数据类型为准
|
||||
typeBuilder.CreateProperty(dbColumnName, typeof(string), new SugarColumn()
|
||||
{
|
||||
IsPrimaryKey = u.IsPrimarykey == 1,
|
||||
IsIdentity = u.IsIdentity == 1,
|
||||
ColumnDataType = u.DataType,
|
||||
Length = u.Length,
|
||||
IsNullable = u.IsNullable == 1,
|
||||
DecimalDigits = u.DecimalDigits,
|
||||
ColumnDescription = u.ColumnDescription,
|
||||
DefaultValue = u.DefaultValue,
|
||||
});
|
||||
});
|
||||
db.CodeFirst.InitTables(typeBuilder.BuilderType());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "DeleteTable"), HttpPost]
|
||||
[DisplayName("删除表")]
|
||||
public void DeleteTable(DeleteDbTableInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
db.DbMaintenance.DropTable(input.TableName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "UpdateTable"), HttpPost]
|
||||
[DisplayName("编辑表")]
|
||||
public void UpdateTable(UpdateDbTableInput input)
|
||||
{
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
db.DbMaintenance.RenameTable(input.OldTableName, input.TableName);
|
||||
try
|
||||
{
|
||||
if (db.DbMaintenance.IsAnyTableRemark(input.TableName))
|
||||
db.DbMaintenance.DeleteTableRemark(input.TableName);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.Description))
|
||||
db.DbMaintenance.AddTableRemark(input.TableName, input.Description);
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
throw Oops.Oh(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建实体 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "CreateEntity"), HttpPost]
|
||||
[DisplayName("创建实体")]
|
||||
public void CreateEntity(CreateEntityInput input)
|
||||
{
|
||||
var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
|
||||
input.Position = string.IsNullOrWhiteSpace(input.Position) ? "Evergreen.Health.Application" : input.Position;
|
||||
input.EntityName = string.IsNullOrWhiteSpace(input.EntityName) ? (config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(input.TableName, null) : input.TableName) : input.EntityName;
|
||||
string[] dbColumnNames = Array.Empty<string>();
|
||||
// Entity.cs.vm中是允许创建没有基类的实体的,所以这里也要做出相同的判断
|
||||
if (!string.IsNullOrWhiteSpace(input.BaseClassName))
|
||||
{
|
||||
Assembly assembly = Assembly.Load("Admin.NET.Core");
|
||||
Type type = assembly.GetType($"Admin.NET.Core.{input.BaseClassName}");
|
||||
if (type is null)
|
||||
throw Oops.Oh("基类集合配置不存在此类型");
|
||||
dbColumnNames = CodeGenUtil.GetPropertyInfoArray(type)?.Select(p => p.Name).ToArray();
|
||||
if (dbColumnNames is null || dbColumnNames is { Length: 0 })
|
||||
throw Oops.Oh("基类中不存在任何字段");
|
||||
}
|
||||
var templatePath = GetEntityTemplatePath();
|
||||
var targetPath = GetEntityTargetPath(input);
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
DbTableInfo dbTableInfo = db.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == input.TableName || u.Name == input.TableName.ToLower()) ?? throw Oops.Oh(ErrorCodeEnum.db1001);
|
||||
List<DbColumnInfo> dbColumnInfos = db.DbMaintenance.GetColumnInfosByTableName(input.TableName, false);
|
||||
dbColumnInfos.ForEach(u =>
|
||||
{
|
||||
if (u.DbColumnName.ToUpper() == u.DbColumnName)
|
||||
{
|
||||
//字段全是大写的, 这种情况下生成的代码会有问题(即对 DOB 这样的字段,生成的前端代码为 dOB, 而数据序列化到前端又成了 dob,导致bug),因此抛出异常,不允许。
|
||||
throw new Exception($"错误:{u.DbColumnName} 字段全是大写字母,这样生成的代码会有bug!请更改为大写字母开头的驼峰式命名!");
|
||||
}
|
||||
u.PropertyName = config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, dbColumnNames) : u.DbColumnName; // 转下划线后的列名需要再转回来
|
||||
u.DataType = CodeGenUtil.ConvertDataType(u, config.DbType);
|
||||
});
|
||||
if (_codeGenOptions.BaseEntityNames.Contains(input.BaseClassName, StringComparer.OrdinalIgnoreCase))
|
||||
dbColumnInfos = dbColumnInfos.Where(u => !dbColumnNames.Contains(u.PropertyName, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
var tContent = File.ReadAllText(templatePath);
|
||||
var tResult = _viewEngine.RunCompileFromCached(tContent, new
|
||||
{
|
||||
NameSpace = $"{input.Position}.Entity",
|
||||
input.TableName,
|
||||
input.EntityName,
|
||||
BaseClassName = string.IsNullOrWhiteSpace(input.BaseClassName) ? "" : $": {input.BaseClassName}",
|
||||
input.ConfigId,
|
||||
dbTableInfo.Description,
|
||||
TableField = dbColumnInfos
|
||||
});
|
||||
File.WriteAllText(targetPath, tResult, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建种子数据 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[ApiDescriptionSettings(Name = "CreateSeedData"), HttpPost]
|
||||
[DisplayName("创建种子数据")]
|
||||
public async Task CreateSeedData(CreateSeedDataInput input)
|
||||
{
|
||||
var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
|
||||
input.Position = string.IsNullOrWhiteSpace(input.Position) ? "Admin.NET.Core" : input.Position;
|
||||
|
||||
var templatePath = GetSeedDataTemplatePath();
|
||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
||||
var tableInfo = db.DbMaintenance.GetTableInfoList(false).First(u => u.Name == input.TableName); // 表名
|
||||
List<DbColumnInfo> dbColumnInfos = db.DbMaintenance.GetColumnInfosByTableName(input.TableName, false); // 所有字段
|
||||
IEnumerable<EntityInfo> entityInfos = await GetEntityInfos();
|
||||
Type entityType = null;
|
||||
foreach (var item in entityInfos)
|
||||
{
|
||||
if (tableInfo.Name.ToLower() != (config.DbSettings.EnableUnderLine ? item.DbTableName.ToUnderLine() : item.DbTableName).ToLower()) continue;
|
||||
entityType = item.Type;
|
||||
break;
|
||||
}
|
||||
if (entityType == null) throw Oops.Oh(ErrorCodeEnum.db1003);
|
||||
|
||||
input.EntityName = entityType.Name;
|
||||
input.SeedDataName = entityType.Name + "SeedData";
|
||||
if (!string.IsNullOrWhiteSpace(input.Suffix)) input.SeedDataName += input.Suffix;
|
||||
|
||||
// 查询所有数据
|
||||
var query = db.QueryableByObject(entityType);
|
||||
// 优先用创建时间排序
|
||||
DbColumnInfo orderField = dbColumnInfos.FirstOrDefault(u => u.DbColumnName.ToLower() == "create_time" || u.DbColumnName.ToLower() == "createtime");
|
||||
if (orderField != null) query = query.OrderBy(orderField.DbColumnName);
|
||||
// 再使用第一个主键排序
|
||||
query = query.OrderBy(dbColumnInfos.First(u => u.IsPrimarykey).DbColumnName);
|
||||
var records = ((IEnumerable)await query.ToListAsync()).ToDynamicList();
|
||||
|
||||
// 过滤已存在的数据
|
||||
if (input.FilterExistingData && records.Any())
|
||||
{
|
||||
// 获取实体类型-所有种数据数据类型
|
||||
var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false) && u.FullName.EndsWith("." + input.EntityName))
|
||||
.Where(u => !u.GetCustomAttributes<IgnoreTableAttribute>().Any())
|
||||
.ToList();
|
||||
if (entityTypes.Count == 1) // 只有一个实体匹配才能过滤
|
||||
{
|
||||
// 获取实体的主键对应的属性名称
|
||||
var pkInfo = entityTypes[0].GetProperties().FirstOrDefault(u => u.GetCustomAttribute<SugarColumn>()?.IsPrimaryKey == true);
|
||||
if (pkInfo != null)
|
||||
{
|
||||
var seedDataTypes = App.EffectiveTypes
|
||||
.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(
|
||||
i => i.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>)) && i.GenericTypeArguments[0] == entityTypes[0]
|
||||
)
|
||||
)
|
||||
.ToList();
|
||||
// 可能会重名的种子数据不作为过滤项
|
||||
string doNotFilterFullName1 = $"{input.Position}.SeedData.{input.SeedDataName}";
|
||||
string doNotFilterFullName2 = $"{input.Position}.{input.SeedDataName}"; // Core中的命名空间没有SeedData
|
||||
|
||||
PropertyInfo idPropertySeedData = records[0].GetType().GetProperty("Id");
|
||||
|
||||
for (int i = seedDataTypes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
string fullName = seedDataTypes[i].FullName;
|
||||
if ((fullName == doNotFilterFullName1) || (fullName == doNotFilterFullName2)) continue;
|
||||
|
||||
// 删除重复数据
|
||||
var instance = Activator.CreateInstance(seedDataTypes[i]);
|
||||
var hasDataMethod = seedDataTypes[i].GetMethod("HasData");
|
||||
var seedData = ((IEnumerable)hasDataMethod?.Invoke(instance, null))?.Cast<object>();
|
||||
if (seedData == null) continue;
|
||||
|
||||
List<object> recordsToRemove = new();
|
||||
foreach (var record in records)
|
||||
{
|
||||
object recordId = pkInfo.GetValue(record);
|
||||
if (seedData.Select(d1 => idPropertySeedData.GetValue(d1)).Any(dataId => recordId != null && dataId != null && recordId.Equals(dataId)))
|
||||
{
|
||||
recordsToRemove.Add(record);
|
||||
}
|
||||
}
|
||||
foreach (var itemToRemove in recordsToRemove)
|
||||
{
|
||||
records.Remove(itemToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查有没有 System.Text.Json.Serialization.JsonIgnore 的属性
|
||||
// var jsonIgnoreProperties = entityType.GetProperties().Where(p => (p.GetAttribute<System.Text.Json.Serialization.JsonIgnoreAttribute>() != null ||
|
||||
// p.GetAttribute<JsonIgnoreAttribute>() != null) && p.GetAttribute<SugarColumn>() != null).ToList();
|
||||
// var jsonIgnoreInfo = new List<List<JsonIgnoredPropertyData>>();
|
||||
// if (jsonIgnoreProperties.Count > 0)
|
||||
// {
|
||||
// int recordIndex = 0;
|
||||
// foreach (var r in (IEnumerable)records)
|
||||
// {
|
||||
// List<JsonIgnoredPropertyData> record = new();
|
||||
// foreach (var item in jsonIgnoreProperties)
|
||||
// {
|
||||
// object v = item.GetValue(r);
|
||||
// string strValue = "null";
|
||||
// if (v != null)
|
||||
// {
|
||||
// strValue = v.ToString();
|
||||
// if (v.GetType() == typeof(string))
|
||||
// strValue = "\"" + strValue + "\"";
|
||||
// else if (v.GetType() == typeof(DateTime))
|
||||
// strValue = "DateTime.Parse(\"" + ((DateTime)v).ToString("yyyy-MM-dd HH:mm:ss") + "\")";
|
||||
// }
|
||||
// record.Add(new JsonIgnoredPropertyData { RecordIndex = recordIndex, Name = item.Name, Value = strValue });
|
||||
// }
|
||||
// recordIndex++;
|
||||
// jsonIgnoreInfo.Add(record);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 获取所有字段信息
|
||||
var propertyList = entityType.GetProperties().Where(x => false == (x.GetCustomAttribute<SugarColumn>()?.IsIgnore ?? false)).ToList();
|
||||
for (var i = 0; i < propertyList.Count; i++)
|
||||
{
|
||||
if (propertyList[i].Name != nameof(EntityBaseId.Id) || !(propertyList[i].GetCustomAttribute<SugarColumn>()?.IsPrimaryKey ?? true)) continue;
|
||||
var temp = propertyList[i];
|
||||
for (var j = i; j > 0; j--) propertyList[j] = propertyList[j - 1];
|
||||
propertyList[0] = temp;
|
||||
}
|
||||
// 拼接数据
|
||||
var recordList = records.Select(obj => string.Join(", ", propertyList.Select(prop =>
|
||||
{
|
||||
var propType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
object value = prop.GetValue(obj);
|
||||
if (value == null) value = "null";
|
||||
else if (propType == typeof(string))
|
||||
{
|
||||
value = $"@\"{value}\"";
|
||||
}
|
||||
else if (propType.IsEnum)
|
||||
{
|
||||
value = $"{propType.Name}.{value}";
|
||||
}
|
||||
else if (propType == typeof(bool))
|
||||
{
|
||||
value = (bool)value ? "true" : "false";
|
||||
}
|
||||
else if (propType == typeof(DateTime))
|
||||
{
|
||||
value = $"DateTime.Parse(\"{((DateTime)value):yyyy-MM-dd HH:mm:ss.fff}\")";
|
||||
}
|
||||
return $"{prop.Name}={value}";
|
||||
}))).ToList();
|
||||
|
||||
var tContent = await File.ReadAllTextAsync(templatePath);
|
||||
var data = new
|
||||
{
|
||||
NameSpace = $"{input.Position}.SeedData",
|
||||
EntityNameSpace = entityType.Namespace,
|
||||
input.TableName,
|
||||
input.EntityName,
|
||||
input.SeedDataName,
|
||||
input.ConfigId,
|
||||
tableInfo.Description,
|
||||
// JsonIgnoreInfo = jsonIgnoreInfo,
|
||||
RecordList = recordList
|
||||
};
|
||||
var tResult = await _viewEngine.RunCompileAsync(tContent, data, builderAction: builder =>
|
||||
{
|
||||
builder.AddAssemblyReferenceByName("System.Linq");
|
||||
builder.AddAssemblyReferenceByName("System.Collections");
|
||||
builder.AddUsing("System.Collections.Generic");
|
||||
builder.AddUsing("System.Linq");
|
||||
});
|
||||
|
||||
var targetPath = GetSeedDataTargetPath(input);
|
||||
await File.WriteAllTextAsync(targetPath, tResult, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取库表信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task<IEnumerable<EntityInfo>> GetEntityInfos()
|
||||
{
|
||||
var entityInfos = new List<EntityInfo>();
|
||||
|
||||
var type = typeof(SugarTable);
|
||||
var types = new List<Type>();
|
||||
if (_codeGenOptions.EntityAssemblyNames != null)
|
||||
{
|
||||
foreach (var asm in _codeGenOptions.EntityAssemblyNames.Select(Assembly.Load))
|
||||
{
|
||||
types.AddRange(asm.GetExportedTypes().ToList());
|
||||
}
|
||||
}
|
||||
|
||||
Type[] cosType = types.Where(o => IsMyAttribute(Attribute.GetCustomAttributes(o, true))).ToArray();
|
||||
|
||||
foreach (var c in cosType)
|
||||
{
|
||||
var sugarAttribute = c.GetCustomAttributes(type, true)?.FirstOrDefault();
|
||||
|
||||
var des = c.GetCustomAttributes(typeof(DescriptionAttribute), true);
|
||||
var description = "";
|
||||
if (des.Length > 0)
|
||||
{
|
||||
description = ((DescriptionAttribute)des[0]).Description;
|
||||
}
|
||||
entityInfos.Add(new EntityInfo()
|
||||
{
|
||||
EntityName = c.Name,
|
||||
DbTableName = sugarAttribute == null ? c.Name : ((SugarTable)sugarAttribute).TableName,
|
||||
TableDescription = description,
|
||||
Type = c
|
||||
});
|
||||
}
|
||||
return await Task.FromResult(entityInfos);
|
||||
|
||||
bool IsMyAttribute(Attribute[] o)
|
||||
{
|
||||
return o.Any(a => a.GetType() == type);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实体模板文件路径
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static string GetEntityTemplatePath()
|
||||
{
|
||||
var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template");
|
||||
return Path.Combine(templatePath, "Entity.cs.vm");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取种子数据模板文件路径
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static string GetSeedDataTemplatePath()
|
||||
{
|
||||
var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template");
|
||||
return Path.Combine(templatePath, "SeedData.cs.vm");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置生成实体文件路径
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetEntityTargetPath(CreateEntityInput input)
|
||||
{
|
||||
var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, input.Position, "Entity");
|
||||
//if (!Directory.Exists(backendPath))
|
||||
//{
|
||||
// var pluginsPath = App.GetConfig<string[]>("AppSettings:ExternalAssemblies");
|
||||
// foreach (var pluginPath in pluginsPath)
|
||||
// {
|
||||
// backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, pluginPath, input.Position, "Entity");
|
||||
// if (Directory.Exists(backendPath))
|
||||
// break;
|
||||
// }
|
||||
//}
|
||||
if (!Directory.Exists(backendPath))
|
||||
Directory.CreateDirectory(backendPath);
|
||||
return Path.Combine(backendPath, input.EntityName + ".cs");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置生成种子数据文件路径
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetSeedDataTargetPath(CreateSeedDataInput input)
|
||||
{
|
||||
var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, input.Position, "SeedData");
|
||||
if (!Directory.Exists(backendPath))
|
||||
Directory.CreateDirectory(backendPath);
|
||||
return Path.Combine(backendPath, input.SeedDataName + ".cs");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 备份数据库(PostgreSQL)🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost, NonUnify]
|
||||
[DisplayName("备份数据库(PostgreSQL)")]
|
||||
public async Task<IActionResult> BackupDatabase()
|
||||
{
|
||||
if (_db.CurrentConnectionConfig.DbType != SqlSugar.DbType.PostgreSQL)
|
||||
throw Oops.Oh("只支持 PostgreSQL 数据库 😁");
|
||||
|
||||
var npgsqlConn = new NpgsqlConnectionStringBuilder(_db.CurrentConnectionConfig.ConnectionString);
|
||||
if (npgsqlConn == null || string.IsNullOrWhiteSpace(npgsqlConn.Host) || string.IsNullOrWhiteSpace(npgsqlConn.Username) || string.IsNullOrWhiteSpace(npgsqlConn.Password) || string.IsNullOrWhiteSpace(npgsqlConn.Database))
|
||||
throw Oops.Oh("PostgreSQL 数据库配置错误");
|
||||
|
||||
// 确保备份目录存在
|
||||
var backupDirectory = Path.Combine(Directory.GetCurrentDirectory(), "backups");
|
||||
Directory.CreateDirectory(backupDirectory);
|
||||
|
||||
// 构建备份文件名
|
||||
string backupFileName = $"backup_{DateTime.Now:yyyyMMddHHmmss}.sql";
|
||||
string backupFilePath = Path.Combine(backupDirectory, backupFileName);
|
||||
|
||||
// 启动pg_dump进程进行备份
|
||||
// 设置密码:export PGPASSWORD='xxxxxx'
|
||||
var bash = $"-U {npgsqlConn.Username} -h {npgsqlConn.Host} -p {npgsqlConn.Port} -E UTF8 -F c -b -v -f {backupFilePath} {npgsqlConn.Database}";
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "pg_dump",
|
||||
Arguments = bash,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
EnvironmentVariables =
|
||||
{
|
||||
["PGPASSWORD"] = npgsqlConn.Password
|
||||
}
|
||||
};
|
||||
|
||||
//_logger.LogInformation("备份数据库:pg_dump " + bash);
|
||||
|
||||
//try
|
||||
//{
|
||||
using (var backupProcess = Process.Start(startInfo))
|
||||
{
|
||||
await backupProcess.WaitForExitAsync();
|
||||
|
||||
//var output = await backupProcess.StandardOutput.ReadToEndAsync();
|
||||
//var error = await backupProcess.StandardError.ReadToEndAsync();
|
||||
|
||||
// 检查备份是否成功
|
||||
if (backupProcess.ExitCode != 0)
|
||||
{
|
||||
throw Oops.Oh($"备份失败:ExitCode({backupProcess.ExitCode})");
|
||||
}
|
||||
}
|
||||
|
||||
// _logger.LogInformation($"备份成功:{backupFilePath}");
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// _logger.LogError(ex, $"备份失败:");
|
||||
// throw;
|
||||
//}
|
||||
|
||||
// 若备份成功则提供下载链接
|
||||
return new FileStreamResult(new FileStream(backupFilePath, FileMode.Open), "application/octet-stream")
|
||||
{
|
||||
FileDownloadName = backupFileName
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class DictDataInput : BaseStatusInput
|
||||
{
|
||||
}
|
||||
|
||||
public class PageDictDataInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 字典类型Id
|
||||
/// </summary>
|
||||
public long DictTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文本
|
||||
/// </summary>
|
||||
public string Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
}
|
||||
|
||||
public class AddDictDataInput : SysDictData
|
||||
{
|
||||
}
|
||||
|
||||
public class UpdateDictDataInput : AddDictDataInput
|
||||
{
|
||||
}
|
||||
|
||||
public class DeleteDictDataInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
public class GetDataDictDataInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 字典类型Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "字典类型Id不能为空"), DataValidation(ValidationTypes.Numeric)]
|
||||
public long DictTypeId { get; set; }
|
||||
}
|
||||
|
||||
public class QueryDictDataInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 字典值
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "字典值不能为空")]
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class DictDataOutput
|
||||
{
|
||||
public long DictDataId { get; set; }
|
||||
public string TypeCode { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Code { get; set; }
|
||||
public string TagType { get; set; }
|
||||
public string StyleSetting { get; set; }
|
||||
public string ClassSetting { get; set; }
|
||||
public string ExtData { get; set; }
|
||||
public string Remark { get; set; }
|
||||
public int OrderNo { get; set; }
|
||||
public StatusEnum Status { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class DictTypeInput : BaseStatusInput
|
||||
{
|
||||
}
|
||||
|
||||
public class PageDictTypeInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
}
|
||||
|
||||
public class AddDictTypeInput : SysDictType
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否是租户字典(Y-是,N-否)
|
||||
/// </summary>
|
||||
public override YesNoEnum IsTenant { get; set; } = YesNoEnum.Y;
|
||||
|
||||
/// <summary>
|
||||
/// 是否是内置字典(Y-是,N-否)
|
||||
/// </summary>
|
||||
public override YesNoEnum SysFlag { get; set; } = YesNoEnum.N;
|
||||
}
|
||||
|
||||
public class UpdateDictTypeInput : AddDictTypeInput
|
||||
{
|
||||
}
|
||||
|
||||
public class DeleteDictTypeInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
public class GetDataDictTypeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 编码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "字典类型编码不能为空")]
|
||||
public string Code { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统字典值服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 420, Description = "系统字典值")]
|
||||
public class SysDictDataService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysDictData> _sysDictDataRep;
|
||||
public readonly ISugarQueryable<SysDictData> VSysDictData;
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly UserManager _userManager;
|
||||
private readonly SysLangTextCacheService _sysLangTextCacheService;
|
||||
|
||||
public SysDictDataService(SqlSugarRepository<SysDictData> sysDictDataRep,
|
||||
SysCacheService sysCacheService,
|
||||
UserManager userManager,
|
||||
SysLangTextCacheService sysLangTextCacheService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sysDictDataRep = sysDictDataRep;
|
||||
_sysCacheService = sysCacheService;
|
||||
_sysLangTextCacheService = sysLangTextCacheService;
|
||||
VSysDictData = _sysDictDataRep.Context.UnionAll(
|
||||
_sysDictDataRep.AsQueryable(),
|
||||
_sysDictDataRep.Change<SysDictDataTenant>().AsQueryable()
|
||||
//.WhereIF(_userManager.SuperAdmin, d => d.TenantId == _userManager.TenantId)
|
||||
.Select<SysDictData>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典值分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取字典值分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysDictData>> Page(PageDictDataInput input)
|
||||
{
|
||||
var langCode = _userManager.LangCode;
|
||||
var baseQuery = VSysDictData
|
||||
.Where(u => u.DictTypeId == input.DictTypeId)
|
||||
.WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code))
|
||||
.WhereIF(!string.IsNullOrEmpty(input.Label?.Trim()), u => u.Label.Contains(input.Label))
|
||||
.OrderBy(u => new { u.OrderNo, u.Code });
|
||||
var pageList = await baseQuery.ToPagedListAsync(input.Page, input.PageSize);
|
||||
var list = pageList.Items;
|
||||
var ids = list.Select(d => d.Id).Distinct().ToList();
|
||||
var translations = await _sysLangTextCacheService.GetTranslations(
|
||||
"SysDictData",
|
||||
"Label",
|
||||
ids,
|
||||
langCode);
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (translations.TryGetValue(item.Id, out var translatedLabel) && !string.IsNullOrEmpty(translatedLabel))
|
||||
{
|
||||
item.Label = translatedLabel;
|
||||
}
|
||||
}
|
||||
pageList.Items = list;
|
||||
return pageList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典值列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取字典值列表")]
|
||||
public async Task<List<SysDictData>> GetList([FromQuery] GetDataDictDataInput input)
|
||||
{
|
||||
var langCode = _userManager.LangCode;
|
||||
var list = await GetDictDataListByDictTypeId(input.DictTypeId);
|
||||
var ids = list.Select(d => d.Id).Distinct().ToList();
|
||||
var translations = await _sysLangTextCacheService.GetTranslations(
|
||||
"SysDictData",
|
||||
"Label",
|
||||
ids,
|
||||
langCode);
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (translations.TryGetValue(item.Id, out var translatedLabel) && !string.IsNullOrEmpty(translatedLabel))
|
||||
{
|
||||
item.Label = translatedLabel;
|
||||
}
|
||||
}
|
||||
return await GetDictDataListByDictTypeId(input.DictTypeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加字典值 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加字典值")]
|
||||
public async Task AddDictData(AddDictDataInput input)
|
||||
{
|
||||
var isExist = await VSysDictData.AnyAsync(u => u.Value == input.Value && u.DictTypeId == input.DictTypeId);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D3003);
|
||||
|
||||
var dictType = await _sysDictDataRep.Change<SysDictType>().GetByIdAsync(input.DictTypeId);
|
||||
if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008);
|
||||
|
||||
Remove(dictType);
|
||||
|
||||
dynamic dictData = dictType.IsTenant == YesNoEnum.Y ? input.Adapt<SysDictDataTenant>() : input.Adapt<SysDictData>();
|
||||
await _sysDictDataRep.Context.Insertable(dictData).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典值 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新字典值")]
|
||||
public async Task UpdateDictData(UpdateDictDataInput input)
|
||||
{
|
||||
var isExist = await VSysDictData.AnyAsync(u => u.Id == input.Id);
|
||||
if (!isExist) throw Oops.Oh(ErrorCodeEnum.D3004);
|
||||
|
||||
isExist = await VSysDictData.AnyAsync(u => u.Value == input.Value && u.DictTypeId == input.DictTypeId && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D3003);
|
||||
|
||||
var dictType = await _sysDictDataRep.Change<SysDictType>().GetByIdAsync(input.DictTypeId);
|
||||
if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
|
||||
|
||||
Remove(dictType);
|
||||
dynamic dictData = dictType.IsTenant == YesNoEnum.Y ? input.Adapt<SysDictDataTenant>() : input.Adapt<SysDictData>();
|
||||
await _sysDictDataRep.Context.Updateable(dictData).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典值 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除字典值")]
|
||||
public async Task DeleteDictData(DeleteDictDataInput input)
|
||||
{
|
||||
var dictData = await VSysDictData.FirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
|
||||
|
||||
var dictType = await _sysDictDataRep.Change<SysDictType>().GetByIdAsync(dictData.DictTypeId);
|
||||
if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
|
||||
|
||||
Remove(dictType);
|
||||
dynamic entity = dictType.IsTenant == YesNoEnum.Y ? input.Adapt<SysDictDataTenant>() : input.Adapt<SysDictData>();
|
||||
await _sysDictDataRep.Context.Deleteable(entity).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典值详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取字典值详情")]
|
||||
public async Task<SysDictData> GetDetail([FromQuery] DictDataInput input)
|
||||
{
|
||||
return (await VSysDictData.FirstAsync(u => u.Id == input.Id))?.Adapt<SysDictData>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改字典值状态 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[DisplayName("修改字典值状态")]
|
||||
public async Task SetStatus(DictDataInput input)
|
||||
{
|
||||
var dictData = await VSysDictData.FirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004);
|
||||
|
||||
var dictType = await _sysDictDataRep.Change<SysDictType>().GetByIdAsync(dictData.DictTypeId);
|
||||
if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
|
||||
|
||||
Remove(dictType);
|
||||
|
||||
dictData.Status = input.Status;
|
||||
dynamic entity = dictType.IsTenant == YesNoEnum.Y ? input.Adapt<SysDictDataTenant>() : input.Adapt<SysDictData>();
|
||||
await _sysDictDataRep.Context.Updateable(entity).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据字典类型Id获取字典值集合
|
||||
/// </summary>
|
||||
/// <param name="dictTypeId"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<SysDictData>> GetDictDataListByDictTypeId(long dictTypeId)
|
||||
{
|
||||
return await GetDataListByIdOrCode(dictTypeId, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据字典类型编码获取字典值集合 🔖
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据字典类型编码获取字典值集合")]
|
||||
public async Task<List<SysDictData>> GetDataList(string code)
|
||||
{
|
||||
return await GetDataListByIdOrCode(null, code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典值集合 🔖
|
||||
/// </summary>
|
||||
/// <param name="typeId"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<SysDictData>> GetDataListByIdOrCode(long? typeId, string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code) && typeId == null ||
|
||||
!string.IsNullOrWhiteSpace(code) && typeId != null)
|
||||
throw Oops.Oh(ErrorCodeEnum.D3011);
|
||||
|
||||
var dictType = await _sysDictDataRep.Change<SysDictType>().AsQueryable()
|
||||
.Where(u => u.Status == StatusEnum.Enable)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(code), u => u.Code == code)
|
||||
.WhereIF(typeId != null, u => u.Id == typeId)
|
||||
.FirstAsync();
|
||||
if (dictType == null) return null;
|
||||
|
||||
string dicKey = dictType.IsTenant == YesNoEnum.N ? $"{CacheConst.KeyDict}{dictType.Code}" : $"{CacheConst.KeyTenantDict}{_userManager}:{dictType?.Code}";
|
||||
var dictDataList = _sysCacheService.Get<List<SysDictData>>(dicKey);
|
||||
if (dictDataList == null)
|
||||
{
|
||||
//平台字典和租户字典分开缓存
|
||||
if (dictType.IsTenant == YesNoEnum.Y)
|
||||
{
|
||||
dictDataList = await _sysDictDataRep.Change<SysDictDataTenant>().AsQueryable()
|
||||
.Where(u => u.DictTypeId == dictType.Id)
|
||||
.Where(u => u.Status == StatusEnum.Enable)
|
||||
.WhereIF(_userManager.SuperAdmin, d => d.TenantId == _userManager.TenantId).Select<SysDictData>()
|
||||
.OrderBy(u => new { u.OrderNo, u.Value, u.Code })
|
||||
.ToListAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
dictDataList = await _sysDictDataRep.AsQueryable()
|
||||
.Where(u => u.DictTypeId == dictType.Id)
|
||||
.Where(u => u.Status == StatusEnum.Enable)
|
||||
.OrderBy(u => new { u.OrderNo, u.Value, u.Code })
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
_sysCacheService.Set(dicKey, dictDataList);
|
||||
}
|
||||
return dictDataList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据查询条件获取字典值集合 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据查询条件获取字典值集合")]
|
||||
public async Task<List<SysDictData>> GetDataList([FromQuery] QueryDictDataInput input)
|
||||
{
|
||||
var dataList = await GetDataList(input.Value);
|
||||
if (input.Status.HasValue) return dataList.Where(u => u.Status == (StatusEnum)input.Status.Value).ToList();
|
||||
return dataList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据字典类型Id删除字典值
|
||||
/// </summary>
|
||||
/// <param name="dictTypeId"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task DeleteDictData(long dictTypeId)
|
||||
{
|
||||
var dictType = await _sysDictDataRep.Change<SysDictType>().AsQueryable().Where(u => u.Id == dictTypeId).FirstAsync();
|
||||
Remove(dictType);
|
||||
|
||||
if (dictType?.IsTenant == YesNoEnum.Y)
|
||||
await _sysDictDataRep.Change<SysDictDataTenant>().DeleteAsync(u => u.DictTypeId == dictTypeId);
|
||||
else
|
||||
await _sysDictDataRep.DeleteAsync(u => u.DictTypeId == dictTypeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过字典数据Value查询显示文本Label
|
||||
/// 适用于列表中根据字典数据值找文本的子查询 _sysDictDataService.MapDictValueToLabel(() =>obj.Type, "org_type",obj);
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="mappingFiled"></param>
|
||||
/// <param name="dictTypeCode"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <returns></returns>
|
||||
public string MapDictValueToLabel<T>(Expression<Func<object>> mappingFiled, string dictTypeCode, T parameter)
|
||||
{
|
||||
return VSysDictData.InnerJoin<SysDictType>((d, dt) => d.DictTypeId.Equals(dt.Id) && dt.Code == dictTypeCode)
|
||||
.SetContext(d => d.Value, mappingFiled, parameter).FirstOrDefault()?.Label;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过字典数据显示文本Label查询Value
|
||||
/// 适用于列表数据导入根据字典数据文本找值的子查询 _sysDictDataService.MapDictLabelToValue(() => obj.Type, "org_type",obj);
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="mappingFiled"></param>
|
||||
/// <param name="dictTypeCode"></param>
|
||||
/// <param name="parameter"></param>
|
||||
/// <returns></returns>
|
||||
public string MapDictLabelToValue<T>(Expression<Func<object>> mappingFiled, string dictTypeCode, T parameter)
|
||||
{
|
||||
return VSysDictData.InnerJoin<SysDictType>((d, dt) => d.DictTypeId.Equals(dt.Id) && dt.Code == dictTypeCode)
|
||||
.SetContext(d => d.Label, mappingFiled, parameter).FirstOrDefault()?.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理字典数据缓存
|
||||
/// </summary>
|
||||
/// <param name="dictType"></param>
|
||||
private void Remove(SysDictType dictType)
|
||||
{
|
||||
_sysCacheService.Remove($"{CacheConst.KeyDict}{dictType?.Code}");
|
||||
_sysCacheService.Remove($"{CacheConst.KeyTenantDict}{_userManager}:{dictType?.Code}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统字典类型服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 430, Description = "系统字典类型")]
|
||||
public class SysDictTypeService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysDictType> _sysDictTypeRep;
|
||||
private readonly SysDictDataService _sysDictDataService;
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly UserManager _userManager;
|
||||
private readonly SysLangTextCacheService _sysLangTextCacheService;
|
||||
|
||||
public SysDictTypeService(SqlSugarRepository<SysDictType> sysDictTypeRep,
|
||||
SysDictDataService sysDictDataService,
|
||||
SysCacheService sysCacheService,
|
||||
UserManager userManager,
|
||||
SysLangTextCacheService sysLangTextCacheService)
|
||||
{
|
||||
_sysDictTypeRep = sysDictTypeRep;
|
||||
_sysDictDataService = sysDictDataService;
|
||||
_sysCacheService = sysCacheService;
|
||||
_userManager = userManager;
|
||||
_sysLangTextCacheService = sysLangTextCacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典类型分页列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取字典类型分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysDictType>> Page(PageDictTypeInput input)
|
||||
{
|
||||
var langCode = _userManager.LangCode;
|
||||
var baseQuery = _sysDictTypeRep.AsQueryable()
|
||||
.WhereIF(!_userManager.SuperAdmin, u => u.IsTenant == YesNoEnum.Y)
|
||||
.WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code))
|
||||
.WhereIF(!string.IsNullOrEmpty(input.Name?.Trim()), u => u.Name.Contains(input.Name));
|
||||
//.OrderBy(u => new { u.OrderNo, u.Code })
|
||||
var pageList = await baseQuery.ToPagedListAsync(input.Page, input.PageSize);
|
||||
var list = pageList.Items;
|
||||
var ids = list.Select(d => d.Id).Distinct().ToList();
|
||||
var translations = await _sysLangTextCacheService.GetTranslations(
|
||||
"SysDictType",
|
||||
"Name",
|
||||
ids,
|
||||
langCode);
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
|
||||
{
|
||||
item.Name = translatedName;
|
||||
}
|
||||
}
|
||||
pageList.Items = list;
|
||||
return pageList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典类型列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取字典类型列表")]
|
||||
public async Task<List<SysDictType>> GetList()
|
||||
{
|
||||
var langCode = _userManager.LangCode;
|
||||
var list = await _sysDictTypeRep.AsQueryable().OrderBy(u => new { u.OrderNo, u.Code }).ToListAsync();
|
||||
var ids = list.Select(d => d.Id).Distinct().ToList();
|
||||
var translations = await _sysLangTextCacheService.GetTranslations(
|
||||
"SysDictType",
|
||||
"Name",
|
||||
ids,
|
||||
langCode);
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
|
||||
{
|
||||
item.Name = translatedName;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典类型-值列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取字典类型-值列表")]
|
||||
public async Task<List<SysDictData>> GetDataList([FromQuery] GetDataDictTypeInput input)
|
||||
{
|
||||
var dictType = await _sysDictTypeRep.GetFirstAsync(u => u.Code == input.Code) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
|
||||
var langCode = _userManager.LangCode;
|
||||
var list = await _sysDictDataService.GetDictDataListByDictTypeId(dictType.Id);
|
||||
var ids = list.Select(d => d.Id).Distinct().ToList();
|
||||
var translations = await _sysLangTextCacheService.GetTranslations(
|
||||
"SysDictType",
|
||||
"Name",
|
||||
ids,
|
||||
langCode);
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
|
||||
{
|
||||
item.Name = translatedName;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加字典类型 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("添加字典类型")]
|
||||
public async Task AddDictType(AddDictTypeInput input)
|
||||
{
|
||||
if (input.Code.ToLower().EndsWith("enum")) throw Oops.Oh(ErrorCodeEnum.D3006);
|
||||
if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008);
|
||||
|
||||
var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001);
|
||||
|
||||
if (_userManager.SuperAdmin) input.IsTenant = YesNoEnum.N; // 超级管理员添加的字典类型默认非租户级
|
||||
|
||||
await _sysDictTypeRep.InsertAsync(input.Adapt<SysDictType>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典类型 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新字典类型")]
|
||||
public async Task UpdateDictType(UpdateDictTypeInput input)
|
||||
{
|
||||
var dict = await _sysDictTypeRep.GetFirstAsync(x => x.Id == input.Id);
|
||||
if (dict.IsTenant != input.IsTenant) throw Oops.Oh(ErrorCodeEnum.D3012);
|
||||
if (dict == null) throw Oops.Oh(ErrorCodeEnum.D3000);
|
||||
|
||||
if (dict.Code.ToLower().EndsWith("enum") && input.Code != dict.Code) throw Oops.Oh(ErrorCodeEnum.D3007);
|
||||
if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
|
||||
|
||||
var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001);
|
||||
|
||||
_sysCacheService.Remove($"{CacheConst.KeyDict}{input.Code}");
|
||||
await _sysDictTypeRep.UpdateAsync(input.Adapt<SysDictType>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典类型 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除字典类型")]
|
||||
public async Task DeleteDictType(DeleteDictTypeInput input)
|
||||
{
|
||||
var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
|
||||
if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
|
||||
|
||||
// 删除字典值
|
||||
await _sysDictTypeRep.DeleteAsync(dictType);
|
||||
await _sysDictDataService.DeleteDictData(input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典类型详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取字典类型详情")]
|
||||
public async Task<SysDictType> GetDetail([FromQuery] DictTypeInput input)
|
||||
{
|
||||
return await _sysDictTypeRep.GetByIdAsync(input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改字典类型状态 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[DisplayName("修改字典类型状态")]
|
||||
public async Task SetStatus(DictTypeInput input)
|
||||
{
|
||||
var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
|
||||
if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
|
||||
|
||||
_sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
|
||||
|
||||
dictType.Status = input.Status;
|
||||
await _sysDictTypeRep.AsUpdateable(dictType).UpdateColumns(u => new { u.Status }, true).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有字典集合 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取所有字典集合")]
|
||||
public async Task<dynamic> GetAllDictList()
|
||||
{
|
||||
var langCode = _userManager.LangCode;
|
||||
var ds = await _sysDictTypeRep.AsQueryable()
|
||||
.InnerJoin(_sysDictDataService.VSysDictData, (u, w) => u.Id == w.DictTypeId)
|
||||
.Select((u, w) => new DictDataOutput
|
||||
{
|
||||
DictDataId = w.Id, // 给翻译用
|
||||
TypeCode = u.Code,
|
||||
Label = w.Label,
|
||||
Value = w.Value,
|
||||
Code = w.Code,
|
||||
TagType = w.TagType,
|
||||
StyleSetting = w.StyleSetting,
|
||||
ClassSetting = w.ClassSetting,
|
||||
ExtData = w.ExtData,
|
||||
Remark = w.Remark,
|
||||
OrderNo = w.OrderNo,
|
||||
Status = w.Status == StatusEnum.Enable && u.Status == StatusEnum.Enable ? StatusEnum.Enable : StatusEnum.Disable
|
||||
})
|
||||
.ToListAsync();
|
||||
var ids = ds.Select(x => x.DictDataId).Distinct().ToList();
|
||||
|
||||
Dictionary<long, string> translations = new();
|
||||
if (ids.Any())
|
||||
{
|
||||
translations = await _sysLangTextCacheService.GetTranslations(
|
||||
"SysDictData",
|
||||
"Label",
|
||||
ids,
|
||||
langCode
|
||||
);
|
||||
}
|
||||
foreach (var item in ds)
|
||||
{
|
||||
if (translations.TryGetValue(item.DictDataId, out var translated) && !string.IsNullOrEmpty(translated))
|
||||
{
|
||||
item.Label = translated;
|
||||
}
|
||||
}
|
||||
|
||||
var result = ds
|
||||
.OrderBy(u => u.OrderNo)
|
||||
.GroupBy(u => u.TypeCode)
|
||||
.ToDictionary(u => u.Key, u => u.ToList());
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举输入参数
|
||||
/// </summary>
|
||||
public class EnumInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 枚举类型名称
|
||||
/// </summary>
|
||||
/// <example>AccountTypeEnum</example>
|
||||
[Required(ErrorMessage = "枚举类型不能为空")]
|
||||
public string EnumName { get; set; }
|
||||
}
|
||||
|
||||
public class QueryEnumDataInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 实体名称
|
||||
/// </summary>
|
||||
/// <example>SysUser</example>
|
||||
[Required(ErrorMessage = "实体名称不能为空")]
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名称
|
||||
/// </summary>
|
||||
/// <example>AccountType</example>
|
||||
[Required(ErrorMessage = "字段名称不能为空")]
|
||||
public string FieldName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举类型输出参数
|
||||
/// </summary>
|
||||
public class EnumTypeOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 枚举类型描述
|
||||
/// </summary>
|
||||
public string TypeDescribe { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 枚举类型名称
|
||||
/// </summary>
|
||||
public string TypeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 枚举类型全名称
|
||||
/// </summary>
|
||||
public string TypeFullName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 枚举类型备注
|
||||
/// </summary>
|
||||
public string TypeRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 枚举实体
|
||||
/// </summary>
|
||||
public List<EnumEntity> EnumEntities { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统枚举服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 275)]
|
||||
public class SysEnumService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly EnumOptions _enumOptions;
|
||||
|
||||
public SysEnumService(IOptions<EnumOptions> enumOptions)
|
||||
{
|
||||
_enumOptions = enumOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有枚举类型 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取所有枚举类型")]
|
||||
public List<EnumTypeOutput> GetEnumTypeList()
|
||||
{
|
||||
var enumTypeList = App.EffectiveTypes.Where(t => t.IsEnum)
|
||||
.Where(t => _enumOptions.EntityAssemblyNames.Contains(t.Assembly.GetName().Name) || _enumOptions.EntityAssemblyNames.Any(name => t.Assembly.GetName().Name!.Contains(name)))
|
||||
.Where(t => t.GetCustomAttributes(typeof(IgnoreEnumToDictAttribute), false).Length == 0) // 排除有忽略转字典特性类型
|
||||
.Where(t => t.GetCustomAttributes(typeof(ErrorCodeTypeAttribute), false).Length == 0) // 排除错误代码类型
|
||||
.OrderBy(u => u.Name).ThenBy(u => u.FullName)
|
||||
.ToList();
|
||||
|
||||
// 如果存在同名枚举类,则依次增加 "_序号" 后缀
|
||||
var list = enumTypeList.Select(GetEnumDescription).ToList();
|
||||
foreach (var enumType in list.GroupBy(u => u.TypeName).Where(g => g.Count() > 1))
|
||||
{
|
||||
int i = 1;
|
||||
foreach (var item in list.Where(u => u.TypeName == enumType.Key).Skip(1)) item.TypeName = $"{item.TypeName}_{i++}";
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典描述
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
private static EnumTypeOutput GetEnumDescription(Type type)
|
||||
{
|
||||
string description = type.Name;
|
||||
var attrs = type.GetCustomAttributes(typeof(DescriptionAttribute), false);
|
||||
if (attrs.Length != 0)
|
||||
{
|
||||
var att = ((DescriptionAttribute[])attrs)[0];
|
||||
description = att.Description;
|
||||
}
|
||||
var enumType = App.EffectiveTypes.FirstOrDefault(t => t.IsEnum && t.Name == type.Name);
|
||||
return new EnumTypeOutput
|
||||
{
|
||||
TypeDescribe = description,
|
||||
TypeName = type.Name,
|
||||
TypeRemark = description,
|
||||
TypeFullName = type.FullName,
|
||||
EnumEntities = (enumType ?? type).EnumToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过枚举类型获取枚举值集合 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("通过枚举类型获取枚举值集合")]
|
||||
public List<EnumEntity> GetEnumDataList([FromQuery] EnumInput input)
|
||||
{
|
||||
var enumType = App.EffectiveTypes.FirstOrDefault(u => u.IsEnum && u.Name == input.EnumName);
|
||||
if (enumType is not { IsEnum: true }) throw Oops.Oh(ErrorCodeEnum.D1503);
|
||||
|
||||
return enumType.EnumToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过实体的字段名获取相关枚举值集合(目前仅支持枚举类型) 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("通过实体的字段名获取相关枚举值集合")]
|
||||
public static List<EnumEntity> GetEnumDataListByField([FromQuery] QueryEnumDataInput input)
|
||||
{
|
||||
// 获取实体类型属性
|
||||
Type entityType = App.EffectiveTypes.FirstOrDefault(u => u.Name == input.EntityName) ?? throw Oops.Oh(ErrorCodeEnum.D1504);
|
||||
|
||||
// 获取字段类型
|
||||
var fieldType = entityType.GetProperties().FirstOrDefault(u => u.Name == input.FieldName)?.PropertyType;
|
||||
if (fieldType is not { IsEnum: true }) throw Oops.Oh(ErrorCodeEnum.D1503);
|
||||
|
||||
return fieldType.EnumToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
/*
|
||||
*━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
* 文件名称:BaiDuTranslationService
|
||||
* 创建时间:2025年03月25日 星期二 20:54:04
|
||||
* 创 建 者:莫闻啼
|
||||
*━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
* 功能描述:
|
||||
* 调用百度翻译Api接口在线翻译,在DeBug模式下生成前端i18n Ts翻译key value,需要先维护对应目录下的zh-CN.ts,对比对应语言包下不存在的key,将value进行翻译并新增到对应语言包文件中
|
||||
*
|
||||
*━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
*/
|
||||
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 百度翻译
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("Extend", Module = "Extend", Order = 200)]
|
||||
public class BaiDuTranslationService : IDynamicApiController, ITransient
|
||||
{
|
||||
// http远程请求
|
||||
private readonly IHttpRemoteService _httpRemoteService;
|
||||
|
||||
/// <summary>
|
||||
/// 百度翻译appId
|
||||
/// </summary>
|
||||
private static readonly string _appId = "xxxxxxxxxxx";
|
||||
|
||||
/// <summary>
|
||||
/// 百度翻译appKey
|
||||
/// </summary>
|
||||
private static readonly string _appKey = "xxxxxxxxxxx";
|
||||
|
||||
/// <summary>
|
||||
/// 百度翻译api地址
|
||||
/// </summary>
|
||||
private static readonly string _baseUrl = "https://fanyi-api.baidu.com/api/trans/vip/translate?";
|
||||
|
||||
// 语言映射字典
|
||||
private static readonly Dictionary<string, string> langMap = new Dictionary<string, string>
|
||||
{
|
||||
["en"] = "en",
|
||||
["de"] = "de",
|
||||
["fi"] = "fin",
|
||||
["es"] = "spa",
|
||||
["fr"] = "fra",
|
||||
["it"] = "it",
|
||||
["ja"] = "jp",
|
||||
["ko"] = "kor",
|
||||
["no"] = "nor",
|
||||
["pl"] = "pl",
|
||||
["pt"] = "pt",
|
||||
["ru"] = "ru",
|
||||
["th"] = "th",
|
||||
["id"] = "id",
|
||||
["ms"] = "may",
|
||||
["vi"] = "vie",
|
||||
["zh-HK"] = "yue",
|
||||
["zh-TW"] = "cht"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一个<see cref="BaiDuTranslationService"/>类型的新实例.
|
||||
/// </summary>
|
||||
/// <param name="httpRemoteService"></param>
|
||||
public BaiDuTranslationService(IHttpRemoteService httpRemoteService)
|
||||
{
|
||||
_httpRemoteService = httpRemoteService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 百度在线翻译
|
||||
/// </summary>
|
||||
/// <param name="from">翻译源语种</param>
|
||||
/// <param name="to">翻译目标语种</param>
|
||||
/// <param name="content">文本内容</param>
|
||||
///<remarks>
|
||||
///源语种和目标语种支持:
|
||||
///zh:简体中文
|
||||
///cht:繁體中文(台灣)
|
||||
///yue:繁體中文(香港)
|
||||
///en:英语
|
||||
///de:德语
|
||||
///spa:西班牙语
|
||||
///fin:芬兰语
|
||||
///fra:法语
|
||||
///it:意大利语
|
||||
///jp:日语
|
||||
///kor:韩语
|
||||
///nor:挪威语
|
||||
///pl:波兰语
|
||||
///pt:葡萄牙语
|
||||
///ru:俄语
|
||||
///th:泰语
|
||||
///id:印度尼西亚语
|
||||
///may:马来西亚
|
||||
///vie:越南语
|
||||
///
|
||||
///更多语种请查看:https://api.fanyi.baidu.com/doc/21
|
||||
/// </remarks>
|
||||
/// <returns>翻译后的文本内容</returns>
|
||||
[DisplayName("百度在线翻译")]
|
||||
[HttpGet]
|
||||
public async Task<BaiDuTranslationResult> Translation([FromQuery][Required] string from, [FromQuery][Required] string to, [FromQuery][Required] string content)
|
||||
{
|
||||
// 标准版API授权只能翻译基础18语言,201种需要企业尊享版支持见百度api 文档
|
||||
Random rd = new Random();
|
||||
string salt = rd.Next(100000).ToString();
|
||||
// 改成您的密钥
|
||||
string secretKey = _appKey;
|
||||
string sign = EncryptString(_appId + content + salt + secretKey);
|
||||
string url = $"{_baseUrl}q={HttpUtility.UrlEncode(content)}&from={from}&to={to}&appid={_appId}&salt={salt}&sign={sign}";
|
||||
var res = await _httpRemoteService.GetAsAsync<BaiDuTranslationResult>(url);
|
||||
|
||||
if (!res.error_code.Equals("0"))
|
||||
{
|
||||
throw Oops.Bah($"翻译失败,错误码:{res.error_code},错误信息:{res.error_msg}");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
/// <summary>
|
||||
/// 生成前端页面i18n文件
|
||||
/// </summary>
|
||||
[DisplayName("生成前端页面i18n文件")]
|
||||
[HttpPost]
|
||||
public async Task GeneratePageI18nFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取基础路径
|
||||
var i18nPath = AppContext.BaseDirectory;
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
i18nPath = Directory.GetParent(i18nPath).FullName;
|
||||
}
|
||||
|
||||
i18nPath = Path.Combine(i18nPath, "Web", "src", "i18n", "pages", "systemMenu");
|
||||
|
||||
// 读取基础语言文件
|
||||
var dic = await ReadBaseLanguageFile(i18nPath);
|
||||
|
||||
if (dic.Count == 0)
|
||||
{
|
||||
throw Oops.Bah("未查询到属性定义,不能生成");
|
||||
}
|
||||
|
||||
// 并行处理所有语言文件
|
||||
var files = Directory.GetFiles(i18nPath, "*.ts").Where(f => !f.EndsWith("zh-CN.ts")).ToList();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var langCode = Path.GetFileNameWithoutExtension(file);
|
||||
var langDic = await ReadLanguageFile(file);
|
||||
|
||||
// 查询出没有生成的键值对
|
||||
|
||||
// Linq查询
|
||||
// var notGen = dic.Where(kv => !langDic.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
// 转换为 HashSet 提升性能
|
||||
var langDicKey = new HashSet<string>(langDic.Keys);
|
||||
var notGen = dic.Where(kv => !langDicKey.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
// 没有未生成的跳出
|
||||
if (notGen.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"{langCode,-6} 语言包:{langDic.Count}/共:{dic.Count} 已全部生成,无需再次生成");
|
||||
continue;
|
||||
}
|
||||
|
||||
var str = string.Empty;
|
||||
Console.WriteLine($"{langCode,-6}开始生成语言包,未生成:{notGen.Count}/已生成:{langDic.Count}/共{dic.Count}");
|
||||
foreach (var gen in notGen)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!langMap.TryGetValue(langCode, out var targetLang))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = await Translation("zh", targetLang, $"{gen.Value}");
|
||||
|
||||
if (!result.error_code.Equals("0"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var translationValue = result.trans_result[0].Dst;
|
||||
LogTranslationProgress(gen.Key, gen.Value, translationValue, ConsoleColor.DarkMagenta);
|
||||
|
||||
// 如果翻译结果为空字符串不追加
|
||||
if (string.IsNullOrEmpty(translationValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果翻译结果包含"'" 法语意大利语常出现 在"'"前加转义符
|
||||
if (translationValue.Contains("'"))
|
||||
{
|
||||
translationValue = translationValue.Replace("'", "\\'");
|
||||
}
|
||||
|
||||
str += ($" {gen.Key}: '{translationValue}',{Environment.NewLine}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogError(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (str.Length > 0)
|
||||
{
|
||||
str = str.TrimStart();
|
||||
await FileHelper.InsertsStringAtSpecifiedLocationInFile(file, str, '}', 2, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw Oops.Bah(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成前端菜单i18n文件
|
||||
/// </summary>
|
||||
[DisplayName("生成前端菜单i18n文件")]
|
||||
[HttpPost]
|
||||
public async Task GenerateMenuI18nFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取基础路径
|
||||
var i18nPath = AppContext.BaseDirectory;
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
i18nPath = Directory.GetParent(i18nPath).FullName;
|
||||
}
|
||||
|
||||
i18nPath = Path.Combine(i18nPath, "Web", "src", "i18n", "menu");
|
||||
|
||||
// 读取基础语言文件
|
||||
var dic = await ReadBaseLanguageFile(i18nPath);
|
||||
|
||||
if (dic.Count == 0)
|
||||
{
|
||||
throw Oops.Bah("未查询到属性定义,不能生成");
|
||||
}
|
||||
|
||||
// 并行处理所有语言文件
|
||||
var files = Directory.GetFiles(i18nPath, "*.ts").Where(f => !f.EndsWith("zh-CN.ts")).ToList();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var langCode = Path.GetFileNameWithoutExtension(file);
|
||||
var langDic = await ReadLanguageFile(file);
|
||||
|
||||
// 查询出没有生成的键值对
|
||||
|
||||
// Linq查询
|
||||
// var notGen = dic.Where(kv => !langDic.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
// 转换为 HashSet 提升性能
|
||||
var langDicKey = new HashSet<string>(langDic.Keys);
|
||||
var notGen = dic.Where(kv => !langDicKey.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
// 没有未生成的跳出
|
||||
if (notGen.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"{langCode,-6} 语言包:{langDic.Count}/共:{dic.Count} 已全部生成,无需再次生成");
|
||||
continue;
|
||||
}
|
||||
|
||||
var str = string.Empty;
|
||||
Console.WriteLine($"{langCode,-6}开始生成语言包,未生成:{notGen.Count}/已生成:{langDic.Count}/共{dic.Count}");
|
||||
foreach (var gen in notGen)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!langMap.TryGetValue(langCode, out var targetLang))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = await Translation("zh", targetLang, $"{gen.Value}");
|
||||
|
||||
if (!result.error_code.Equals("0"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var translationValue = result.trans_result[0].Dst;
|
||||
LogTranslationProgress(gen.Key, gen.Value, translationValue, ConsoleColor.DarkMagenta);
|
||||
|
||||
// 如果翻译结果为空字符串不追加
|
||||
if (string.IsNullOrEmpty(translationValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果翻译结果包含"'" 法语意大利语常出现 在"'"前加转义符
|
||||
if (translationValue.Contains("'"))
|
||||
{
|
||||
translationValue = translationValue.Replace("'", "\\'");
|
||||
}
|
||||
|
||||
str += ($" {gen.Key}: '{translationValue}',{Environment.NewLine}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogError(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (str.Length > 0)
|
||||
{
|
||||
str = str.TrimStart();
|
||||
await FileHelper.InsertsStringAtSpecifiedLocationInFile(file, str, '}', 2, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw Oops.Bah(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#region 辅助方法
|
||||
|
||||
private static async Task<Dictionary<string, string>> ReadBaseLanguageFile(string i18nPath)
|
||||
{
|
||||
var baseFile = Path.Combine(i18nPath, "zh-CN.ts");
|
||||
if (!File.Exists(baseFile))
|
||||
{
|
||||
throw Oops.Bah("【zh-CN.ts】文件未找到");
|
||||
}
|
||||
|
||||
var dic = new Dictionary<string, string>();
|
||||
using var reader = new StreamReader(baseFile, Encoding.UTF8);
|
||||
|
||||
while (await reader.ReadLineAsync() is { } line)
|
||||
{
|
||||
if (line.Contains('{') || line.Contains('}')) continue;
|
||||
|
||||
var cleanLine = line.Trim().TrimEnd(',').Replace("'", "");
|
||||
var parts = cleanLine.Split(new[] { ':' }, 2);
|
||||
if (parts.Length == 2) dic[parts[0].Trim()] = parts[1].Trim();
|
||||
}
|
||||
|
||||
reader.Close();
|
||||
return dic;
|
||||
}
|
||||
|
||||
private static async Task<Dictionary<string, string>> ReadLanguageFile(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw Oops.Bah($"【{filePath.Split('/').Last()}】文件未找到");
|
||||
}
|
||||
|
||||
var dic = new Dictionary<string, string>();
|
||||
using var reader = new StreamReader(filePath, Encoding.UTF8);
|
||||
|
||||
while (await reader.ReadLineAsync() is { } line)
|
||||
{
|
||||
if (line.Contains('{') || line.Contains('}')) continue;
|
||||
|
||||
var cleanLine = line.Trim().TrimEnd(',').Replace("'", "");
|
||||
var parts = cleanLine.Split(new[] { ':' }, 2);
|
||||
if (parts.Length == 2) dic[parts[0].Trim()] = parts[1].Trim();
|
||||
}
|
||||
|
||||
reader.Close();
|
||||
return dic;
|
||||
}
|
||||
|
||||
private static void LogTranslationProgress(string key, string value, string res, ConsoleColor color)
|
||||
{
|
||||
Console.ForegroundColor = color;
|
||||
Console.WriteLine($"翻译属性: {key,-32}值: {value,-64}结果: {res}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
private static void LogError(Exception e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkRed;
|
||||
Console.WriteLine($"{e.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
#endregion 辅助方法
|
||||
|
||||
#endif
|
||||
|
||||
// 计算MD5值
|
||||
[NonAction]
|
||||
private static string EncryptString(string str)
|
||||
{
|
||||
MD5 md5 = MD5.Create();
|
||||
// 将字符串转换成字节数组
|
||||
byte[] byteOld = Encoding.UTF8.GetBytes(str);
|
||||
// 调用加密方法
|
||||
byte[] byteNew = md5.ComputeHash(byteOld);
|
||||
// 将加密结果转换为字符串
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (byte b in byteNew)
|
||||
{
|
||||
// 将字节转换成16进制表示的字符串,
|
||||
sb.Append(b.ToString("x2"));
|
||||
}
|
||||
|
||||
// 返回加密的字符串
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 百度翻译结果
|
||||
/// </summary>
|
||||
public class BaiDuTranslationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 源语种
|
||||
/// </summary>
|
||||
public string From { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标语种
|
||||
/// </summary>
|
||||
public string To { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译结果
|
||||
/// </summary>
|
||||
public List<TransResult> trans_result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误码 正常为0
|
||||
/// </summary>
|
||||
public string error_code { get; set; } = "0";
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
public string error_msg { get; set; } = String.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译结果
|
||||
/// </summary>
|
||||
public class TransResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 源字符
|
||||
/// </summary>
|
||||
public string Src { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标字符
|
||||
/// </summary>
|
||||
public string Dst { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 文件分页查询
|
||||
/// </summary>
|
||||
public class PageFileInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名称
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件路径
|
||||
/// </summary>
|
||||
public string FilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件后缀
|
||||
/// </summary>
|
||||
public string? Suffix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
public class UploadFileInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件
|
||||
/// </summary>
|
||||
[Required]
|
||||
public IFormFile File { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类别
|
||||
/// </summary>
|
||||
public string FileType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否公开
|
||||
/// </summary>
|
||||
public bool IsPublic { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 允许格式:.jpeg.jpg.png.bmp.gif.tif
|
||||
/// </summary>
|
||||
public string AllowSuffix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定存储桶名称
|
||||
/// </summary>
|
||||
public string? BucketName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定存储提供者ID
|
||||
/// </summary>
|
||||
public long? ProviderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务数据Id
|
||||
/// </summary>
|
||||
public long? DataId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件Base64
|
||||
/// </summary>
|
||||
public class UploadFileFromBase64Input
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件内容
|
||||
/// </summary>
|
||||
public string FileDataBase64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型( "image/jpeg",)
|
||||
/// </summary>
|
||||
public string ContentType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询关联查询输入
|
||||
/// </summary>
|
||||
public class RelationQueryInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 关联对象名称
|
||||
/// </summary>
|
||||
public string RelationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联对象Id
|
||||
/// </summary>
|
||||
public long? RelationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型:多个以","分割
|
||||
/// </summary>
|
||||
public string FileTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属Id
|
||||
/// </summary>
|
||||
public long? BelongId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型分割
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string[] GetFileTypeBS()
|
||||
{
|
||||
return FileTypes.Split(',');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 文件存储提供者分页查询输入参数
|
||||
/// </summary>
|
||||
public class PageFileProviderInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储提供者
|
||||
/// </summary>
|
||||
public string? Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储桶名称
|
||||
/// </summary>
|
||||
public string? BucketName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool? IsEnable { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加文件存储提供者输入参数
|
||||
/// </summary>
|
||||
public class AddFileProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储提供者
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "存储提供者不能为空")]
|
||||
public string Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储桶名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "存储桶名称不能为空")]
|
||||
public string BucketName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问密钥ID(所有云服务商统一使用此字段)
|
||||
/// </summary>
|
||||
public string? AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密钥
|
||||
/// </summary>
|
||||
public string? SecretKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地域
|
||||
/// </summary>
|
||||
public string? Region { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 端点地址
|
||||
/// </summary>
|
||||
public string? Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用HTTPS
|
||||
/// </summary>
|
||||
public bool? IsEnableHttps { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用缓存
|
||||
/// </summary>
|
||||
public bool? IsEnableCache { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool? IsEnable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否默认提供者
|
||||
/// </summary>
|
||||
public bool? IsDefault { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义域名
|
||||
/// </summary>
|
||||
public string? SinceDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序号
|
||||
/// </summary>
|
||||
public int? OrderNo { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支持的业务类型(JSON格式)
|
||||
/// </summary>
|
||||
public string? BusinessTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优先级
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 100;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新文件存储提供者输入参数
|
||||
/// </summary>
|
||||
public class UpdateFileProviderInput : AddFileProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "主键Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件存储提供者输入参数
|
||||
/// </summary>
|
||||
public class DeleteFileProviderInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询文件存储提供者输入参数
|
||||
/// </summary>
|
||||
public class QueryFileProviderInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试连接输入参数
|
||||
/// </summary>
|
||||
public class TestConnectionInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认存储提供者输入参数
|
||||
/// </summary>
|
||||
public class SetDefaultProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储提供者ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "存储提供者ID不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件上传选择存储提供者输入参数
|
||||
/// </summary>
|
||||
public class SelectProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件类型
|
||||
/// </summary>
|
||||
public string? FileType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务类型
|
||||
/// </summary>
|
||||
public string? BusinessType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定提供者ID
|
||||
/// </summary>
|
||||
public long? ProviderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定存储桶名称
|
||||
/// </summary>
|
||||
public string? BucketName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class DefaultFileProvider : ICustomFileProvider, ITransient
|
||||
{
|
||||
/// <summary>
|
||||
/// 构建文件的完整物理路径
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <returns></returns>
|
||||
private string BuildFullFilePath(SysFile sysFile)
|
||||
{
|
||||
return Path.Combine(App.WebHostEnvironment.WebRootPath, sysFile.FilePath ?? "", $"{sysFile.Id}{sysFile.Suffix}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建目录的完整物理路径
|
||||
/// </summary>
|
||||
/// <param name="relativePath"></param>
|
||||
/// <returns></returns>
|
||||
private string BuildFullDirectoryPath(string relativePath)
|
||||
{
|
||||
return Path.Combine(App.WebHostEnvironment.WebRootPath, relativePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保目录存在
|
||||
/// </summary>
|
||||
/// <param name="directoryPath"></param>
|
||||
private void EnsureDirectoryExists(string directoryPath)
|
||||
{
|
||||
if (!Directory.Exists(directoryPath))
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
|
||||
public Task DeleteFileAsync(SysFile sysFile)
|
||||
{
|
||||
var filePath = BuildFullFilePath(sysFile);
|
||||
if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFileBase64Async(SysFile sysFile)
|
||||
{
|
||||
var realFile = BuildFullFilePath(sysFile);
|
||||
if (!File.Exists(realFile))
|
||||
{
|
||||
Log.Error($"DownloadFileBase64:文件[{realFile}]不存在");
|
||||
throw Oops.Oh($"文件[{sysFile.FilePath}]不存在");
|
||||
}
|
||||
|
||||
byte[] fileBytes = await File.ReadAllBytesAsync(realFile);
|
||||
return Convert.ToBase64String(fileBytes);
|
||||
}
|
||||
|
||||
public Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName)
|
||||
{
|
||||
var fullPath = BuildFullFilePath(sysFile);
|
||||
return Task.FromResult(new FileStreamResult(new FileStream(fullPath, FileMode.Open), "application/octet-stream")
|
||||
{
|
||||
FileDownloadName = fileName + sysFile.Suffix
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<SysFile> UploadFileAsync(IFormFile file, SysFile newFile, string path, string finalName)
|
||||
{
|
||||
newFile.Provider = ""; // 本地存储 Provider 显示为空
|
||||
|
||||
var directoryPath = BuildFullDirectoryPath(path);
|
||||
EnsureDirectoryExists(directoryPath);
|
||||
|
||||
var realFile = Path.Combine(directoryPath, finalName);
|
||||
await using var stream = File.Create(realFile);
|
||||
await file.CopyToAsync(stream);
|
||||
|
||||
newFile.Url = $"{newFile.FilePath}/{newFile.Id + newFile.Suffix}";
|
||||
return newFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义文件提供器接口
|
||||
/// </summary>
|
||||
public interface ICustomFileProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <returns></returns>
|
||||
public Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName);
|
||||
|
||||
/// <summary>
|
||||
/// 下载指定文件Base64格式
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <returns></returns>
|
||||
public Task<string> DownloadFileBase64Async(SysFile sysFile);
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <returns></returns>
|
||||
public Task DeleteFileAsync(SysFile sysFile);
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <param name="path">文件存储位置</param>
|
||||
/// <param name="finalName">文件最终名称</param>
|
||||
/// <returns></returns>
|
||||
public Task<SysFile> UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 多OSS文件提供者
|
||||
/// </summary>
|
||||
public class MultiOSSFileProvider : ICustomFileProvider, ITransient
|
||||
{
|
||||
private readonly SysFileProviderService _fileProviderService;
|
||||
private readonly IOSSServiceManager _ossServiceManager;
|
||||
private readonly OSSProviderOptions _ossProviderOptions;
|
||||
|
||||
public MultiOSSFileProvider(SysFileProviderService fileProviderService,
|
||||
IOSSServiceManager ossServiceManager,
|
||||
IOptions<OSSProviderOptions> ossProviderOptions)
|
||||
{
|
||||
_fileProviderService = fileProviderService;
|
||||
_ossServiceManager = ossServiceManager;
|
||||
_ossProviderOptions = ossProviderOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <param name="path">文件存储位置</param>
|
||||
/// <param name="finalName">文件最终名称</param>
|
||||
/// <returns></returns>
|
||||
public async Task<SysFile> UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName)
|
||||
{
|
||||
// 获取OSS配置(传入文件信息用于策略选择)
|
||||
var provider = await GetFileProvider(sysFile, file) ?? throw Oops.Oh("未找到可用的存储提供者配置");
|
||||
|
||||
// 获取OSS服务
|
||||
var ossService = await _ossServiceManager.GetOSSServiceAsync(provider);
|
||||
|
||||
// 设置文件信息
|
||||
sysFile.Provider = provider.Provider;
|
||||
sysFile.BucketName = provider.BucketName; // 保存原始存储桶名称
|
||||
|
||||
var filePath = string.Concat(path, "/", finalName);
|
||||
|
||||
// 上传文件
|
||||
await ossService.PutObjectAsync(provider.BucketName, filePath, file.OpenReadStream());
|
||||
|
||||
// 生成外链地址
|
||||
sysFile.Url = GenerateFileUrl(provider, provider.BucketName, filePath);
|
||||
|
||||
return sysFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <returns></returns>
|
||||
public async Task DeleteFileAsync(SysFile sysFile)
|
||||
{
|
||||
// 获取OSS配置(统一方法)
|
||||
var provider = await GetFileProvider(sysFile) ?? throw Oops.Oh($"未找到存储提供者配置: {sysFile.Provider}-{sysFile.BucketName}");
|
||||
var ossService = await _ossServiceManager.GetOSSServiceAsync(provider);
|
||||
var filePath = string.Concat(sysFile.FilePath, "/", $"{sysFile.Id}{sysFile.Suffix}");
|
||||
|
||||
await ossService.RemoveObjectAsync(provider.BucketName, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <returns></returns>
|
||||
public async Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName)
|
||||
{
|
||||
// 获取OSS配置(统一方法)
|
||||
var provider = await GetFileProvider(sysFile) ?? throw Oops.Oh($"未找到存储提供者配置: {sysFile.Provider}-{sysFile.BucketName}");
|
||||
var ossService = await _ossServiceManager.GetOSSServiceAsync(provider);
|
||||
var filePath = Path.Combine(sysFile.FilePath ?? "", sysFile.Id + sysFile.Suffix);
|
||||
|
||||
var httpRemoteService = App.GetRequiredService<IHttpRemoteService>();
|
||||
var stream = await httpRemoteService.GetAsStreamAsync(await ossService.PresignedGetObjectAsync(provider.BucketName, filePath, 5));
|
||||
|
||||
return new FileStreamResult(stream, "application/octet-stream") { FileDownloadName = fileName + sysFile.Suffix };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件Base64格式
|
||||
/// </summary>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> DownloadFileBase64Async(SysFile sysFile)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
HttpResponseMessage response = await httpClient.GetAsync(sysFile.Url);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
byte[] fileBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
return Convert.ToBase64String(fileBytes);
|
||||
}
|
||||
throw Oops.Oh($"下载文件失败,状态码: {response.StatusCode}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件提供者配置(统一方法,支持上传、删除、下载场景)
|
||||
/// </summary>
|
||||
/// <param name="sysFile">系统文件信息</param>
|
||||
/// <param name="file">上传的文件(可选,仅上传时传入)</param>
|
||||
/// <returns></returns>
|
||||
private async Task<SysFileProvider?> GetFileProvider(SysFile sysFile, IFormFile? file = null)
|
||||
{
|
||||
// 1. 如果已指定存储桶,直接使用
|
||||
if (!string.IsNullOrEmpty(sysFile.BucketName))
|
||||
{
|
||||
var provider = await _fileProviderService.GetFileProviderByBucket(sysFile.Provider, sysFile.BucketName);
|
||||
if (provider != null) return provider;
|
||||
|
||||
// 如果数据库中找不到,尝试配置文件兜底
|
||||
if (_ossProviderOptions.Enabled && _ossProviderOptions.Bucket == sysFile.BucketName)
|
||||
{
|
||||
return await CreateProviderFromConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果有上传文件信息,使用策略选择(仅上传场景)
|
||||
if (file != null)
|
||||
{
|
||||
var uploadInput = new UploadFileInput
|
||||
{
|
||||
File = file,
|
||||
BucketName = sysFile.BucketName,
|
||||
FileType = sysFile.FileType
|
||||
};
|
||||
|
||||
return await SelectProviderAsync(file, uploadInput);
|
||||
}
|
||||
|
||||
// 3. 最后兜底:使用默认存储提供者
|
||||
return await _fileProviderService.GetDefaultProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选择合适的OSS存储提供者(内联版本)
|
||||
/// </summary>
|
||||
/// <param name="file">上传的文件</param>
|
||||
/// <param name="input">上传输入参数</param>
|
||||
/// <returns></returns>
|
||||
private async Task<SysFileProvider?> SelectProviderAsync(IFormFile file, UploadFileInput input)
|
||||
{
|
||||
// 1. 优先使用指定的提供者ID
|
||||
if (input.ProviderId.HasValue)
|
||||
{
|
||||
var provider = await _fileProviderService.GetFileProviderById(input.ProviderId.Value);
|
||||
if (provider != null) return provider;
|
||||
}
|
||||
|
||||
// 2. 其次使用指定的存储桶名称
|
||||
if (!string.IsNullOrEmpty(input.BucketName))
|
||||
{
|
||||
var providers = await _fileProviderService.GetCachedFileProviders();
|
||||
var provider = providers.FirstOrDefault(p => p.BucketName == input.BucketName);
|
||||
if (provider != null) return provider;
|
||||
}
|
||||
|
||||
// 3. 使用默认提供者
|
||||
var defaultProvider = await _fileProviderService.GetDefaultProvider();
|
||||
if (defaultProvider != null) return defaultProvider;
|
||||
|
||||
// 4. 兜底:如果数据库中没有配置,尝试从配置文件创建默认提供者
|
||||
return await CreateProviderFromConfiguration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成文件URL(内联版本)
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <param name="bucketName">存储桶名称</param>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <returns></returns>
|
||||
private static string GenerateFileUrl(SysFileProvider provider, string bucketName, string filePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(bucketName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
|
||||
|
||||
var protocol = provider.IsEnableHttps == true ? "https" : "http";
|
||||
|
||||
// 如果有自定义域名,直接使用
|
||||
if (!string.IsNullOrWhiteSpace(provider.SinceDomain))
|
||||
{
|
||||
return $"{provider.SinceDomain.TrimEnd('/')}/{filePath.TrimStart('/')}";
|
||||
}
|
||||
|
||||
// 根据不同提供者生成URL
|
||||
return provider.Provider.ToUpper() switch
|
||||
{
|
||||
"ALIYUN" => $"{protocol}://{bucketName}.{provider.Endpoint}/{filePath.TrimStart('/')}",
|
||||
"QCLOUD" => $"{protocol}://{bucketName}-{provider.Endpoint}.cos.{provider.Region}.myqcloud.com/{filePath.TrimStart('/')}",
|
||||
"MINIO" => $"{protocol}://{provider.Endpoint}/{bucketName}/{filePath.TrimStart('/')}",
|
||||
_ => throw Oops.Oh($"不支持的OSS提供者: {provider.Provider}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从配置文件创建默认提供者(兜底机制)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Task<SysFileProvider?> CreateProviderFromConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查是否启用了OSS配置
|
||||
if (!_ossProviderOptions.Enabled && !App.Configuration["MultiOSS:Enabled"].ToBoolean())
|
||||
return Task.FromResult<SysFileProvider?>(null);
|
||||
|
||||
// 验证必要配置
|
||||
if (string.IsNullOrWhiteSpace(_ossProviderOptions.AccessKey) ||
|
||||
string.IsNullOrWhiteSpace(_ossProviderOptions.SecretKey) ||
|
||||
string.IsNullOrWhiteSpace(_ossProviderOptions.Bucket))
|
||||
{
|
||||
return Task.FromResult<SysFileProvider?>(null);
|
||||
}
|
||||
|
||||
// 使用现有的OSSProviderOptions创建临时提供者配置(不保存到数据库)
|
||||
var provider = new SysFileProvider
|
||||
{
|
||||
Id = 0, // 临时ID
|
||||
Provider = Enum.GetName(_ossProviderOptions.Provider),
|
||||
BucketName = _ossProviderOptions.Bucket,
|
||||
AccessKey = _ossProviderOptions.AccessKey,
|
||||
SecretKey = _ossProviderOptions.SecretKey,
|
||||
Endpoint = _ossProviderOptions.Endpoint,
|
||||
Region = _ossProviderOptions.Region,
|
||||
IsEnableHttps = _ossProviderOptions.IsEnableHttps,
|
||||
IsEnableCache = _ossProviderOptions.IsEnableCache,
|
||||
IsEnable = true,
|
||||
IsDefault = true,
|
||||
SinceDomain = _ossProviderOptions.CustomHost,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
|
||||
return Task.FromResult<SysFileProvider?>(provider);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 配置读取失败,返回null
|
||||
return Task.FromResult<SysFileProvider?>(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using OnceMi.AspNetCore.OSS;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class OSSFileProvider : ICustomFileProvider, ITransient
|
||||
{
|
||||
private readonly IOSSService _OSSService;
|
||||
private readonly OSSProviderOptions _OSSProviderOptions;
|
||||
|
||||
public OSSFileProvider(IOptions<OSSProviderOptions> oSSProviderOptions, IOSSServiceFactory ossServiceFactory)
|
||||
{
|
||||
_OSSProviderOptions = oSSProviderOptions.Value;
|
||||
if (_OSSProviderOptions.Enabled)
|
||||
_OSSService = ossServiceFactory.Create(Enum.GetName(_OSSProviderOptions.Provider));
|
||||
}
|
||||
|
||||
public async Task DeleteFileAsync(SysFile file)
|
||||
{
|
||||
await _OSSService.RemoveObjectAsync(file.BucketName, string.Concat(file.FilePath, "/", $"{file.Id}{file.Suffix}"));
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFileBase64Async(SysFile file)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
HttpResponseMessage response = await httpClient.GetAsync(file.Url);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// 读取文件内容并将其转换为 Base64 字符串
|
||||
byte[] fileBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
return Convert.ToBase64String(fileBytes);
|
||||
}
|
||||
throw new HttpRequestException($"Request failed with status code: {response.StatusCode}");
|
||||
}
|
||||
|
||||
public async Task<FileStreamResult> GetFileStreamResultAsync(SysFile file, string fileName)
|
||||
{
|
||||
var filePath = Path.Combine(file.FilePath ?? "", file.Id + file.Suffix);
|
||||
var httpRemoteService = App.GetRequiredService<IHttpRemoteService>();
|
||||
var stream = await httpRemoteService.GetAsStreamAsync(await _OSSService.PresignedGetObjectAsync(file.BucketName, filePath, 5));
|
||||
return new FileStreamResult(stream, "application/octet-stream") { FileDownloadName = fileName + file.Suffix };
|
||||
}
|
||||
|
||||
public async Task<SysFile> UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName)
|
||||
{
|
||||
sysFile.Provider = Enum.GetName(_OSSProviderOptions.Provider);
|
||||
var filePath = string.Concat(path, "/", finalName);
|
||||
await _OSSService.PutObjectAsync(sysFile.BucketName, filePath, file.OpenReadStream());
|
||||
// http://<你的bucket名字>.oss.aliyuncs.com/<你的object名字>
|
||||
// 生成外链地址 方便前端预览
|
||||
switch (_OSSProviderOptions.Provider)
|
||||
{
|
||||
case OSSProvider.Aliyun:
|
||||
sysFile.Url = $"{(_OSSProviderOptions.IsEnableHttps ? "https" : "http")}://{sysFile.BucketName}.{_OSSProviderOptions.Endpoint}/{filePath}";
|
||||
break;
|
||||
|
||||
case OSSProvider.QCloud:
|
||||
var protocol = _OSSProviderOptions.IsEnableHttps ? "https" : "http";
|
||||
sysFile.Url = !string.IsNullOrWhiteSpace(_OSSProviderOptions.CustomHost)
|
||||
? $"{protocol}://{_OSSProviderOptions.CustomHost}/{filePath}"
|
||||
: $"{protocol}://{sysFile.BucketName}-{_OSSProviderOptions.Endpoint}.cos.{_OSSProviderOptions.Region}.myqcloud.com/{filePath}";
|
||||
break;
|
||||
|
||||
case OSSProvider.Minio:
|
||||
// 获取Minio文件的下载或者预览地址
|
||||
// newFile.Url = await GetMinioPreviewFileUrl(newFile.BucketName, filePath);// 这种方法生成的Url是有7天有效期的,不能这样使用
|
||||
// 需要在MinIO中的Buckets开通对 Anonymous 的readonly权限
|
||||
var customHost = _OSSProviderOptions.CustomHost;
|
||||
if (string.IsNullOrWhiteSpace(customHost))
|
||||
customHost = _OSSProviderOptions.Endpoint;
|
||||
sysFile.Url = $"{(_OSSProviderOptions.IsEnableHttps ? "https" : "http")}://{customHost}/{sysFile.BucketName}/{filePath}";
|
||||
break;
|
||||
}
|
||||
return sysFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class SSHFileProvider : ICustomFileProvider, ITransient
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建SSH连接助手
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private SSHHelper CreateSSHHelper()
|
||||
{
|
||||
return new SSHHelper(
|
||||
App.Configuration["SSHProvider:Host"],
|
||||
App.Configuration["SSHProvider:Port"].ToInt(),
|
||||
App.Configuration["SSHProvider:Username"],
|
||||
App.Configuration["SSHProvider:Password"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建文件完整路径
|
||||
/// </summary>
|
||||
/// <param name="sysFile"></param>
|
||||
/// <returns></returns>
|
||||
private string BuildFilePath(SysFile sysFile)
|
||||
{
|
||||
return string.Concat(sysFile.FilePath, "/", sysFile.Id + sysFile.Suffix);
|
||||
}
|
||||
|
||||
public Task DeleteFileAsync(SysFile sysFile)
|
||||
{
|
||||
var fullPath = BuildFilePath(sysFile);
|
||||
using var helper = CreateSSHHelper();
|
||||
helper.DeleteFile(fullPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> DownloadFileBase64Async(SysFile sysFile)
|
||||
{
|
||||
using var helper = CreateSSHHelper();
|
||||
return Task.FromResult(Convert.ToBase64String(helper.ReadAllBytes(sysFile.FilePath)));
|
||||
}
|
||||
|
||||
public Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName)
|
||||
{
|
||||
var filePath = BuildFilePath(sysFile);
|
||||
using var helper = CreateSSHHelper();
|
||||
return Task.FromResult(new FileStreamResult(helper.OpenRead(filePath), "application/octet-stream")
|
||||
{
|
||||
FileDownloadName = fileName + sysFile.Suffix
|
||||
});
|
||||
}
|
||||
|
||||
public Task<SysFile> UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName)
|
||||
{
|
||||
var fullPath = string.Concat(path.StartsWith('/') ? path : "/" + path, "/", finalName);
|
||||
using var helper = CreateSSHHelper();
|
||||
helper.UploadFile(file.OpenReadStream(), fullPath);
|
||||
return Task.FromResult(sysFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using OnceMi.AspNetCore.OSS;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// OSS服务管理器接口
|
||||
/// </summary>
|
||||
public interface IOSSServiceManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取OSS服务实例
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
Task<IOSSService> GetOSSServiceAsync(SysFileProvider provider);
|
||||
|
||||
/// <summary>
|
||||
/// 清除缓存
|
||||
/// </summary>
|
||||
void ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OSS服务管理器实现
|
||||
/// </summary>
|
||||
public class OSSServiceManager : IOSSServiceManager, ITransient
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ConcurrentDictionary<string, IOSSService> _ossServiceCache;
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _disposed = false;
|
||||
|
||||
public OSSServiceManager(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_ossServiceCache = new ConcurrentDictionary<string, IOSSService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取OSS服务实例(带缓存)
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IOSSService> GetOSSServiceAsync(SysFileProvider provider)
|
||||
{
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
|
||||
var cacheKey = provider.ConfigKey;
|
||||
|
||||
// 尝试从缓存获取
|
||||
if (_ossServiceCache.TryGetValue(cacheKey, out var cachedService))
|
||||
{
|
||||
return cachedService;
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
if (!await ValidateConfigurationAsync(provider))
|
||||
{
|
||||
throw new InvalidOperationException($"OSS提供者配置无效: {provider.DisplayName}");
|
||||
}
|
||||
|
||||
// 线程安全地创建新服务
|
||||
lock (_lockObject)
|
||||
{
|
||||
// 双重检查锁定模式
|
||||
if (_ossServiceCache.TryGetValue(cacheKey, out cachedService))
|
||||
{
|
||||
return cachedService;
|
||||
}
|
||||
|
||||
// 转换配置并创建服务
|
||||
var ossOptions = ConvertToOSSOptions(provider);
|
||||
var ossService = CreateOSSService(ossOptions);
|
||||
|
||||
// 添加到缓存
|
||||
_ossServiceCache.TryAdd(cacheKey, ossService);
|
||||
|
||||
return ossService;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建OSS服务实例
|
||||
/// </summary>
|
||||
/// <param name="options">OSS配置选项</param>
|
||||
/// <returns></returns>
|
||||
private IOSSService CreateOSSService(OSSOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
try
|
||||
{
|
||||
// 使用现有的IOSSServiceFactory,但需要先注册配置
|
||||
var providerName = Enum.GetName(options.Provider);
|
||||
var configSectionName = $"TempOSS_{Guid.NewGuid():N}";
|
||||
|
||||
// 创建临时配置
|
||||
var configData = new Dictionary<string, string>
|
||||
{
|
||||
[$"{configSectionName}:Provider"] = providerName,
|
||||
[$"{configSectionName}:Endpoint"] = options.Endpoint ?? "",
|
||||
[$"{configSectionName}:AccessKey"] = options.AccessKey ?? "",
|
||||
[$"{configSectionName}:SecretKey"] = options.SecretKey ?? "",
|
||||
[$"{configSectionName}:Region"] = options.Region ?? "",
|
||||
[$"{configSectionName}:IsEnableHttps"] = options.IsEnableHttps.ToString(),
|
||||
[$"{configSectionName}:IsEnableCache"] = options.IsEnableCache.ToString()
|
||||
};
|
||||
|
||||
var tempConfig = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configData)
|
||||
.Build();
|
||||
|
||||
// 创建临时服务集合,但不立即释放
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(tempConfig);
|
||||
services.AddLogging();
|
||||
services.AddOSSService(providerName, configSectionName);
|
||||
|
||||
// 构建服务提供者并创建OSS服务
|
||||
var tempServiceProvider = services.BuildServiceProvider();
|
||||
var ossServiceFactory = tempServiceProvider.GetRequiredService<IOSSServiceFactory>();
|
||||
var ossService = ossServiceFactory.Create(providerName);
|
||||
|
||||
// 注意:不要释放tempServiceProvider,因为ossService可能依赖它
|
||||
// 这里我们接受这个内存开销,因为缓存会减少创建频率
|
||||
|
||||
return ossService;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw Oops.Oh($"创建OSS服务失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证配置
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
private Task<bool> ValidateConfigurationAsync(SysFileProvider provider)
|
||||
{
|
||||
if (provider == null) return Task.FromResult(false);
|
||||
|
||||
// 基本字段验证
|
||||
var isValid = !string.IsNullOrWhiteSpace(provider.Provider) &&
|
||||
!string.IsNullOrWhiteSpace(provider.BucketName) &&
|
||||
!string.IsNullOrWhiteSpace(provider.AccessKey) &&
|
||||
!string.IsNullOrWhiteSpace(provider.SecretKey);
|
||||
|
||||
// Minio额外需要Endpoint
|
||||
if (provider.Provider.ToUpper() == "MINIO")
|
||||
{
|
||||
isValid = isValid && !string.IsNullOrWhiteSpace(provider.Endpoint);
|
||||
}
|
||||
|
||||
return Task.FromResult(isValid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将SysFileProvider转换为OSSOptions
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <returns></returns>
|
||||
private OSSOptions ConvertToOSSOptions(SysFileProvider provider)
|
||||
{
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
|
||||
var ossOptions = new OSSOptions
|
||||
{
|
||||
Provider = Enum.Parse<OSSProvider>(provider.Provider),
|
||||
Endpoint = provider.Endpoint,
|
||||
Region = provider.Region,
|
||||
IsEnableHttps = provider.IsEnableHttps ?? true,
|
||||
IsEnableCache = provider.IsEnableCache ?? true
|
||||
};
|
||||
|
||||
// 设置认证信息(所有提供者现在都使用统一的字段)
|
||||
ossOptions.AccessKey = provider.AccessKey;
|
||||
ossOptions.SecretKey = provider.SecretKey;
|
||||
|
||||
return ossOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除缓存
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_ossServiceCache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_ossServiceCache.Clear();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 文件存储提供者管理控制器 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 412, Description = "文件存储提供者管理")]
|
||||
public class SysFileProviderController : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SysFileProviderService _fileProviderService;
|
||||
|
||||
public SysFileProviderController(SysFileProviderService fileProviderService)
|
||||
{
|
||||
_fileProviderService = fileProviderService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储提供者列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储提供者列表")]
|
||||
public async Task<List<SysFileProvider>> GetProviderList()
|
||||
{
|
||||
return await _fileProviderService.GetFileProviderList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储提供者分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储提供者分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysFileProvider>> GetProviderPage(PageFileProviderInput input)
|
||||
{
|
||||
return await _fileProviderService.GetFileProviderPage(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储提供者详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储提供者详情")]
|
||||
public async Task<SysFileProvider> GetProvider([FromQuery] QueryFileProviderInput input)
|
||||
{
|
||||
return await _fileProviderService.GetFileProvider(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("添加存储提供者")]
|
||||
public async Task AddProvider(AddFileProviderInput input)
|
||||
{
|
||||
await _fileProviderService.AddFileProvider(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新存储提供者")]
|
||||
public async Task UpdateProvider(UpdateFileProviderInput input)
|
||||
{
|
||||
await _fileProviderService.UpdateFileProvider(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除存储提供者")]
|
||||
public async Task DeleteProvider(DeleteFileProviderInput input)
|
||||
{
|
||||
await _fileProviderService.DeleteFileProvider(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据存储桶名称获取存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="bucketName">存储桶名称</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据存储桶名称获取存储提供者")]
|
||||
public async Task<SysFileProvider?> GetProviderByBucketName(string bucketName)
|
||||
{
|
||||
return await _fileProviderService.GetProviderByBucketName(bucketName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除存储提供者缓存 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("清除存储提供者缓存")]
|
||||
public async Task ClearCache()
|
||||
{
|
||||
await _fileProviderService.ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量启用/禁用存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "BatchEnable"), HttpPost]
|
||||
[DisplayName("批量启用/禁用存储提供者")]
|
||||
public async Task BatchEnableProvider(BatchEnableProviderInput input)
|
||||
{
|
||||
foreach (var id in input.Ids)
|
||||
{
|
||||
var provider = await _fileProviderService.GetFileProviderById(id);
|
||||
if (provider != null)
|
||||
{
|
||||
var updateInput = new UpdateFileProviderInput
|
||||
{
|
||||
Id = id,
|
||||
Provider = provider.Provider,
|
||||
BucketName = provider.BucketName,
|
||||
IsEnable = input.IsEnable
|
||||
};
|
||||
await _fileProviderService.UpdateFileProvider(updateInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储提供者统计信息 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储提供者统计信息")]
|
||||
public async Task<object> GetProviderStatistics()
|
||||
{
|
||||
var providers = await _fileProviderService.GetCachedFileProviders();
|
||||
|
||||
var statistics = new
|
||||
{
|
||||
Total = providers.Count,
|
||||
Enabled = providers.Count(p => p.IsEnable == true),
|
||||
Disabled = providers.Count(p => p.IsEnable != true),
|
||||
ByProvider = providers.GroupBy(p => p.Provider)
|
||||
.Select(g => new { Provider = g.Key, Count = g.Count() })
|
||||
.ToList(),
|
||||
ByRegion = providers.Where(p => !string.IsNullOrEmpty(p.Region))
|
||||
.GroupBy(p => p.Region)
|
||||
.Select(g => new { Region = g.Key, Count = g.Count() })
|
||||
.ToList()
|
||||
};
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有可用的存储桶列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取所有可用的存储桶列表")]
|
||||
public async Task<List<string>> GetAvailableBuckets()
|
||||
{
|
||||
return await _fileProviderService.GetAvailableBuckets();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储桶和提供者的映射关系 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取存储桶和提供者的映射关系")]
|
||||
public async Task<Dictionary<string, List<SysFileProvider>>> GetBucketProviderMapping()
|
||||
{
|
||||
return await _fileProviderService.GetBucketProviderMapping();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量启用/禁用存储提供者输入参数
|
||||
/// </summary>
|
||||
public class BatchEnableProviderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储提供者ID列表
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<long> Ids { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool IsEnable { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using OnceMi.AspNetCore.OSS;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统文件存储提供者服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 411, Description = "文件存储提供者")]
|
||||
public class SysFileProviderService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly UserManager _userManager;
|
||||
private readonly SqlSugarRepository<SysFileProvider> _sysFileProviderRep;
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly IOSSServiceFactory _ossServiceFactory;
|
||||
private readonly IOSSServiceManager _ossServiceManager;
|
||||
private static readonly string CacheKey = "sys_file_provider";
|
||||
|
||||
public SysFileProviderService(UserManager userManager,
|
||||
SqlSugarRepository<SysFileProvider> sysFileProviderRep,
|
||||
SysCacheService sysCacheService,
|
||||
IOSSServiceFactory ossServiceFactory,
|
||||
IOSSServiceManager ossServiceManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sysFileProviderRep = sysFileProviderRep;
|
||||
_sysCacheService = sysCacheService;
|
||||
_ossServiceFactory = ossServiceFactory;
|
||||
_ossServiceManager = ossServiceManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件存储提供者分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件存储提供者分页列表")]
|
||||
[NonAction]
|
||||
public async Task<SqlSugarPagedList<SysFileProvider>> GetFileProviderPage([FromQuery] PageFileProviderInput input)
|
||||
{
|
||||
return await _sysFileProviderRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Provider), u => u.Provider.Contains(input.Provider!))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.BucketName), u => u.BucketName.Contains(input.BucketName!))
|
||||
.WhereIF(input.IsEnable.HasValue, u => u.IsEnable == input.IsEnable)
|
||||
.OrderBy(u => u.OrderNo)
|
||||
.OrderBy(u => u.Id)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件存储提供者列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件存储提供者列表")]
|
||||
[NonAction]
|
||||
public async Task<List<SysFileProvider>> GetFileProviderList()
|
||||
{
|
||||
return await _sysFileProviderRep.AsQueryable()
|
||||
.Where(u => u.IsEnable == true)
|
||||
.OrderBy(u => u.OrderNo)
|
||||
.OrderBy(u => u.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加文件存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加文件存储提供者")]
|
||||
[NonAction]
|
||||
public async Task AddFileProvider(AddFileProviderInput input)
|
||||
{
|
||||
// 验证输入参数
|
||||
if (input == null)
|
||||
throw Oops.Oh("输入参数不能为空").StatusCode(400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.Provider))
|
||||
throw Oops.Oh("存储提供者不能为空").StatusCode(400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.BucketName))
|
||||
throw Oops.Oh("存储桶名称不能为空").StatusCode(400);
|
||||
|
||||
// 验证提供者类型
|
||||
if (!Enum.TryParse<OSSProvider>(input.Provider, true, out _))
|
||||
throw Oops.Oh($"不支持的存储提供者类型: {input.Provider}").StatusCode(400);
|
||||
|
||||
var isExist = await _sysFileProviderRep.AsQueryable()
|
||||
.AnyAsync(u => u.Provider == input.Provider && u.BucketName == input.BucketName);
|
||||
if (isExist)
|
||||
throw Oops.Oh(ErrorCodeEnum.D1006).StatusCode(400);
|
||||
|
||||
var fileProvider = input.Adapt<SysFileProvider>();
|
||||
|
||||
// 验证配置完整性
|
||||
await ValidateProviderConfiguration(fileProvider);
|
||||
|
||||
// 处理默认提供者逻辑
|
||||
await HandleDefaultProviderLogic(fileProvider);
|
||||
|
||||
await _sysFileProviderRep.InsertAsync(fileProvider);
|
||||
|
||||
// 清除缓存
|
||||
await ClearCache();
|
||||
|
||||
// 清除OSS服务缓存
|
||||
_ossServiceManager?.ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新文件存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新文件存储提供者")]
|
||||
[NonAction]
|
||||
public async Task UpdateFileProvider(UpdateFileProviderInput input)
|
||||
{
|
||||
// 验证输入参数
|
||||
if (input == null)
|
||||
throw Oops.Oh("输入参数不能为空").StatusCode(400);
|
||||
|
||||
var isExist = await _sysFileProviderRep.AsQueryable()
|
||||
.AnyAsync(u => u.Provider == input.Provider && u.BucketName == input.BucketName && u.Id != input.Id);
|
||||
if (isExist)
|
||||
throw Oops.Oh(ErrorCodeEnum.D1006).StatusCode(400);
|
||||
|
||||
var fileProvider = input.Adapt<SysFileProvider>();
|
||||
|
||||
// 验证配置完整性
|
||||
await ValidateProviderConfiguration(fileProvider);
|
||||
|
||||
// 处理默认提供者逻辑
|
||||
await HandleDefaultProviderLogic(fileProvider);
|
||||
|
||||
await _sysFileProviderRep.AsUpdateable(fileProvider).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
|
||||
|
||||
// 清除缓存
|
||||
await ClearCache();
|
||||
|
||||
// 清除OSS服务缓存
|
||||
_ossServiceManager?.ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除文件存储提供者")]
|
||||
[NonAction]
|
||||
public async Task DeleteFileProvider(DeleteFileProviderInput input)
|
||||
{
|
||||
// 检查是否为默认提供者
|
||||
var provider = await _sysFileProviderRep.GetByIdAsync(input.Id) ?? throw Oops.Oh("存储提供者不存在").StatusCode(400);
|
||||
var isDefault = provider.IsDefault == true;
|
||||
|
||||
await _sysFileProviderRep.DeleteByIdAsync(input.Id);
|
||||
|
||||
// 如果删除的是默认提供者,自动设置第一个启用的提供者为默认
|
||||
if (isDefault)
|
||||
{
|
||||
var firstEnabledProvider = await _sysFileProviderRep.AsQueryable()
|
||||
.Where(p => p.IsEnable == true)
|
||||
.OrderBy(p => p.OrderNo)
|
||||
.OrderBy(p => p.Id)
|
||||
.FirstAsync();
|
||||
|
||||
if (firstEnabledProvider != null)
|
||||
{
|
||||
await _sysFileProviderRep.AsUpdateable()
|
||||
.SetColumns(p => p.IsDefault == true)
|
||||
.Where(p => p.Id == firstEnabledProvider.Id)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
Debug.WriteLine($"自动设置新的默认提供者: {firstEnabledProvider.DisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
await ClearCache();
|
||||
|
||||
// 清除OSS服务缓存
|
||||
_ossServiceManager?.ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件存储提供者详情 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件存储提供者详情")]
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider> GetFileProvider([FromQuery] QueryFileProviderInput input)
|
||||
{
|
||||
return await _sysFileProviderRep.GetFirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据提供者和存储桶获取配置
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="bucketName"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetFileProviderByBucket(string provider, string bucketName)
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
return providers.FirstOrDefault(x => x.Provider == provider && x.BucketName == bucketName && x.IsEnable == true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取配置
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetFileProviderById(long id)
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
return providers.FirstOrDefault(x => x.Id == id && x.IsEnable == true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据存储桶名称获取存储提供者
|
||||
/// </summary>
|
||||
/// <param name="bucketName">存储桶名称</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetProviderByBucketName(string bucketName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bucketName))
|
||||
return null;
|
||||
|
||||
var providers = await GetCachedFileProviders();
|
||||
return providers.FirstOrDefault(p => p.BucketName == bucketName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认存储提供者
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetDefaultProvider()
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
|
||||
// 优先返回标记为默认的提供者
|
||||
var defaultProvider = providers.FirstOrDefault(p => p.IsDefault == true);
|
||||
if (defaultProvider != null)
|
||||
return defaultProvider;
|
||||
|
||||
// 如果没有标记为默认的,返回第一个启用的提供者(兼容旧逻辑)
|
||||
return providers.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认存储提供者信息 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取默认存储提供者信息")]
|
||||
[NonAction]
|
||||
public async Task<SysFileProvider?> GetDefaultProviderInfo()
|
||||
{
|
||||
return await GetDefaultProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认存储提供者 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "SetDefault"), HttpPost]
|
||||
[DisplayName("设置默认存储提供者")]
|
||||
[NonAction]
|
||||
public async Task SetDefaultProvider(SetDefaultProviderInput input)
|
||||
{
|
||||
// 验证提供者是否存在且启用
|
||||
var provider = await _sysFileProviderRep.GetByIdAsync(input.Id) ?? throw Oops.Oh("存储提供者不存在").StatusCode(400);
|
||||
if (provider.IsEnable != true)
|
||||
throw Oops.Oh("只能设置启用状态的存储提供者为默认").StatusCode(400);
|
||||
|
||||
// 开启事务,确保数据一致性
|
||||
await _sysFileProviderRep.AsTenant().BeginTranAsync();
|
||||
try
|
||||
{
|
||||
// 先将所有提供者的默认标识设为false
|
||||
await _sysFileProviderRep.AsUpdateable()
|
||||
.SetColumns(p => p.IsDefault == false)
|
||||
.Where(p => p.IsDefault == true)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 设置指定提供者为默认
|
||||
await _sysFileProviderRep.AsUpdateable()
|
||||
.SetColumns(p => p.IsDefault == true)
|
||||
.Where(p => p.Id == input.Id)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
await _sysFileProviderRep.AsTenant().CommitTranAsync();
|
||||
|
||||
// 清除缓存
|
||||
await ClearCache();
|
||||
|
||||
// 清除OSS服务缓存
|
||||
_ossServiceManager?.ClearCache();
|
||||
|
||||
Debug.WriteLine($"已设置默认存储提供者: {provider.DisplayName}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _sysFileProviderRep.AsTenant().RollbackTranAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存的文件提供者列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<SysFileProvider>> GetCachedFileProviders()
|
||||
{
|
||||
return await _sysCacheService.AdGetAsync(CacheKey, async () =>
|
||||
{
|
||||
return await _sysFileProviderRep.AsQueryable()
|
||||
.Where(u => u.IsEnable == true)
|
||||
.OrderBy(u => u.OrderNo)
|
||||
.OrderBy(u => u.Id)
|
||||
.ToListAsync();
|
||||
}, TimeSpan.FromMinutes(30));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除缓存
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task ClearCache()
|
||||
{
|
||||
_sysCacheService.Remove(CacheKey);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有可用的存储桶列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<string>> GetAvailableBuckets()
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
return providers.Select(p => p.BucketName).Distinct().OrderBy(b => b).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储桶和提供者的映射关系
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<Dictionary<string, List<SysFileProvider>>> GetBucketProviderMapping()
|
||||
{
|
||||
var providers = await GetCachedFileProviders();
|
||||
var mapping = new Dictionary<string, List<SysFileProvider>>();
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
if (!mapping.TryGetValue(provider.BucketName, out List<SysFileProvider> value))
|
||||
{
|
||||
value = new List<SysFileProvider>();
|
||||
mapping[provider.BucketName] = value;
|
||||
}
|
||||
|
||||
value.Add(provider);
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证存储提供者配置
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
private async Task ValidateProviderConfiguration(SysFileProvider provider)
|
||||
{
|
||||
if (provider == null)
|
||||
throw Oops.Oh("存储提供者配置不能为空").StatusCode(400);
|
||||
|
||||
// 基础字段验证
|
||||
if (string.IsNullOrWhiteSpace(provider.Provider))
|
||||
throw Oops.Oh("存储提供者类型不能为空").StatusCode(400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider.BucketName))
|
||||
throw Oops.Oh("存储桶名称不能为空").StatusCode(400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider.Endpoint))
|
||||
throw Oops.Oh("端点地址不能为空").StatusCode(400);
|
||||
|
||||
// 所有提供者都需要AccessKey和SecretKey
|
||||
if (string.IsNullOrWhiteSpace(provider.AccessKey))
|
||||
throw Oops.Oh($"{provider.Provider} AccessKey不能为空").StatusCode(400);
|
||||
if (string.IsNullOrWhiteSpace(provider.SecretKey))
|
||||
throw Oops.Oh($"{provider.Provider} SecretKey不能为空").StatusCode(400);
|
||||
|
||||
// 根据不同提供者验证特定字段
|
||||
switch (provider.Provider.ToUpper())
|
||||
{
|
||||
case "ALIYUN":
|
||||
if (string.IsNullOrWhiteSpace(provider.Region))
|
||||
throw Oops.Oh("阿里云Region不能为空").StatusCode(400);
|
||||
break;
|
||||
|
||||
case "QCLOUD":
|
||||
if (string.IsNullOrWhiteSpace(provider.Endpoint))
|
||||
throw Oops.Oh("腾讯云Endpoint(AppId)不能为空").StatusCode(400);
|
||||
if (string.IsNullOrWhiteSpace(provider.Region))
|
||||
throw Oops.Oh("腾讯云Region不能为空").StatusCode(400);
|
||||
break;
|
||||
|
||||
case "MINIO":
|
||||
// Minio只需要AccessKey和SecretKey,已在上面验证
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Oops.Oh($"不支持的存储提供者类型: {provider.Provider}").StatusCode(400);
|
||||
}
|
||||
|
||||
// 验证存储桶名称格式
|
||||
await ValidateBucketName(provider.Provider, provider.BucketName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证存储桶名称格式
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者类型</param>
|
||||
/// <param name="bucketName">存储桶名称</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
private async Task ValidateBucketName(string provider, string bucketName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bucketName))
|
||||
return;
|
||||
|
||||
switch (provider.ToUpper())
|
||||
{
|
||||
case "ALIYUN":
|
||||
// 阿里云存储桶命名规则
|
||||
if (bucketName.Length < 3 || bucketName.Length > 63)
|
||||
throw Oops.Oh("阿里云存储桶名称长度必须在3-63字符之间").StatusCode(400);
|
||||
|
||||
if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-]*[a-z0-9]$"))
|
||||
throw Oops.Oh("阿里云存储桶名称只能包含小写字母、数字和短横线,且必须以字母或数字开头和结尾").StatusCode(400);
|
||||
break;
|
||||
|
||||
case "QCLOUD":
|
||||
// 腾讯云存储桶命名规则
|
||||
if (bucketName.Length < 1 || bucketName.Length > 40)
|
||||
throw Oops.Oh("腾讯云存储桶名称长度必须在1-40字符之间").StatusCode(400);
|
||||
|
||||
if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-]*[a-z0-9]$"))
|
||||
throw Oops.Oh("腾讯云存储桶名称只能包含小写字母、数字和短横线,且必须以字母或数字开头和结尾").StatusCode(400);
|
||||
break;
|
||||
|
||||
case "MINIO":
|
||||
// Minio存储桶命名规则
|
||||
if (bucketName.Length < 3 || bucketName.Length > 63)
|
||||
throw Oops.Oh("Minio存储桶名称长度必须在3-63字符之间").StatusCode(400);
|
||||
|
||||
if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-\.]*[a-z0-9]$"))
|
||||
throw Oops.Oh("Minio存储桶名称只能包含小写字母、数字、短横线和点,且必须以字母或数字开头和结尾").StatusCode(400);
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理默认提供者逻辑
|
||||
/// </summary>
|
||||
/// <param name="provider">存储提供者配置</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
private async Task HandleDefaultProviderLogic(SysFileProvider provider)
|
||||
{
|
||||
// 如果设置为默认提供者
|
||||
if (provider.IsDefault == true)
|
||||
{
|
||||
// 确保只有一个默认提供者,将其他提供者的默认标识设为false
|
||||
await _sysFileProviderRep.AsUpdateable()
|
||||
.SetColumns(p => p.IsDefault == false)
|
||||
.Where(p => p.IsDefault == true && p.Id != provider.Id)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
// 如果没有设置IsDefault值,默认为false
|
||||
{
|
||||
provider.IsDefault ??= false;
|
||||
}
|
||||
|
||||
// 检查是否还有其他默认提供者,如果没有且当前提供者启用,则设为默认
|
||||
var hasDefaultProvider = await _sysFileProviderRep.AsQueryable()
|
||||
.Where(p => p.IsDefault == true && p.IsEnable == true && p.Id != provider.Id)
|
||||
.AnyAsync();
|
||||
|
||||
if (!hasDefaultProvider && provider.IsEnable == true && provider.IsDefault != true)
|
||||
{
|
||||
// 如果没有其他默认提供者且当前提供者启用,则设为默认
|
||||
provider.IsDefault = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Aliyun.OSS.Util;
|
||||
using Furion.AspNetCore;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统文件服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 410, Description = "系统文件")]
|
||||
public class SysFileService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly UserManager _userManager;
|
||||
private readonly SqlSugarRepository<SysFile> _sysFileRep;
|
||||
private readonly OSSProviderOptions _OSSProviderOptions;
|
||||
private readonly UploadOptions _uploadOptions;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif";
|
||||
private readonly INamedServiceProvider<ICustomFileProvider> _namedServiceProvider;
|
||||
private readonly ICustomFileProvider _customFileProvider;
|
||||
|
||||
public SysFileService(UserManager userManager,
|
||||
SqlSugarRepository<SysFile> sysFileRep,
|
||||
IOptions<OSSProviderOptions> oSSProviderOptions,
|
||||
IOptions<UploadOptions> uploadOptions,
|
||||
INamedServiceProvider<ICustomFileProvider> namedServiceProvider,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_namedServiceProvider = namedServiceProvider;
|
||||
_userManager = userManager;
|
||||
_sysFileRep = sysFileRep;
|
||||
_OSSProviderOptions = oSSProviderOptions.Value;
|
||||
_uploadOptions = uploadOptions.Value;
|
||||
_configuration = configuration;
|
||||
|
||||
// 简化提供者选择逻辑
|
||||
if (_OSSProviderOptions.Enabled || _configuration["MultiOSS:Enabled"].ToBoolean())
|
||||
{
|
||||
// 统一使用MultiOSSFileProvider处理所有OSS情况
|
||||
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(MultiOSSFileProvider));
|
||||
}
|
||||
else if (_configuration["SSHProvider:Enabled"].ToBoolean())
|
||||
{
|
||||
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(SSHFileProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(DefaultFileProvider));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件分页列表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysFile>> Page(PageFileInput input)
|
||||
{
|
||||
// 获取所有公开附件
|
||||
var publicList = _sysFileRep.AsQueryable().ClearFilter().Where(u => u.IsPublic == true);
|
||||
// 获取私有附件
|
||||
var privateList = _sysFileRep.AsQueryable().Where(u => u.IsPublic == false);
|
||||
// 合并公开和私有附件并分页
|
||||
return await _sysFileRep.Context.UnionAll(publicList, privateList)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.FileName), u => u.FileName.Contains(input.FileName.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.FilePath), u => u.FilePath.Contains(input.FilePath.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()),
|
||||
u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime)
|
||||
.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件Base64 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传文件Base64")]
|
||||
public async Task<SysFile> UploadFileFromBase64(UploadFileFromBase64Input input)
|
||||
{
|
||||
var pattern = @"data:(?<type>.+?);base64,(?<data>[^""]+)";
|
||||
var regex = new Regex(pattern, RegexOptions.Compiled);
|
||||
var match = regex.Match(input.FileDataBase64);
|
||||
|
||||
byte[] fileData = Convert.FromBase64String(match.Groups["data"].Value);
|
||||
var contentType = match.Groups["type"].Value;
|
||||
if (string.IsNullOrEmpty(input.FileName))
|
||||
input.FileName = $"{YitIdHelper.NextId()}.{contentType.AsSpan(contentType.LastIndexOf('/') + 1)}";
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
ms.Write(fileData);
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
IFormFile formFile = new FormFile(ms, 0, fileData.Length, "file", input.FileName)
|
||||
{
|
||||
Headers = new HeaderDictionary(),
|
||||
ContentType = contentType
|
||||
};
|
||||
var uploadFileInput = input.Adapt<UploadFileInput>();
|
||||
uploadFileInput.File = formFile;
|
||||
return await UploadFile(uploadFileInput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传多文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="files"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传多文件")]
|
||||
public async Task<List<SysFile>> UploadFiles([Required] List<IFormFile> files)
|
||||
{
|
||||
var fileList = new List<SysFile>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
var uploadedFile = await UploadFile(new UploadFileInput { File = file });
|
||||
fileList.Add(uploadedFile);
|
||||
}
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件Id或Url下载 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据文件Id或Url下载")]
|
||||
public async Task<IActionResult> DownloadFile(SysFile input)
|
||||
{
|
||||
var file = input.Id > 0 ? await GetFile(input.Id) : await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == input.Url);
|
||||
var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8"));
|
||||
return await GetFileStreamResult(file, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件预览 🔖
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("文件预览")]
|
||||
public async Task<IActionResult> GetPreview([FromRoute] long id)
|
||||
{
|
||||
var file = await GetFile(id);
|
||||
//var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8"));
|
||||
return await GetFileStreamResult(file, file.Id + "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<IActionResult> GetFileStreamResult(SysFile file, string fileName)
|
||||
{
|
||||
return await _customFileProvider.GetFileStreamResultAsync(file, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public async Task<Stream> GetFileStream(SysFile file)
|
||||
{
|
||||
var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8"));
|
||||
var result = await _customFileProvider.GetFileStreamResultAsync(file, fileName);
|
||||
return result.FileStream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载指定文件Base64格式 🔖
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("下载指定文件Base64格式")]
|
||||
public async Task<string> DownloadFileBase64([FromBody] string url)
|
||||
{
|
||||
var sysFile = await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在");
|
||||
return await _customFileProvider.DownloadFileBase64Async(sysFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除文件")]
|
||||
public async Task DeleteFile(BaseIdInput input)
|
||||
{
|
||||
var file = await _sysFileRep.GetByIdAsync(input.Id) ?? throw Oops.Oh($"文件不存在");
|
||||
await _sysFileRep.DeleteAsync(file);
|
||||
await _customFileProvider.DeleteFileAsync(file);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新文件")]
|
||||
public async Task UpdateFile(SysFile input)
|
||||
{
|
||||
var isExist = await _sysFileRep.IsAnyAsync(u => u.Id == input.Id);
|
||||
if (!isExist) throw Oops.Oh(ErrorCodeEnum.D8000);
|
||||
|
||||
await _sysFileRep.UpdateAsync(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件")]
|
||||
public async Task<SysFile> GetFile([FromQuery] long id)
|
||||
{
|
||||
var file = await _sysFileRep.CopyNew().GetByIdAsync(id);
|
||||
return file ?? throw Oops.Oh(ErrorCodeEnum.D8000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件Id集合获取文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据文件Id集合获取文件")]
|
||||
public async Task<List<SysFile>> GetFileByIds([FromQuery][FlexibleArray<long>] List<long> ids)
|
||||
{
|
||||
return await _sysFileRep.AsQueryable().Where(u => ids.Contains(u.Id)).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件路径 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取文件路径")]
|
||||
public async Task<List<TreeNode>> GetFolder()
|
||||
{
|
||||
// 优化:直接在数据库层面获取不重复的文件路径
|
||||
var folders = await _sysFileRep.AsQueryable()
|
||||
.Select(u => u.FilePath)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var pathTreeBuilder = new PathTreeBuilder();
|
||||
var tree = pathTreeBuilder.BuildTree(folders);
|
||||
return tree.Children;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="targetPath">存储目标路径</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传文件")]
|
||||
public async Task<SysFile> UploadFile([FromForm] UploadFileInput input, [BindNever] string targetPath = "")
|
||||
{
|
||||
if (input.File == null || input.File.Length <= 0) throw Oops.Oh(ErrorCodeEnum.D8000);
|
||||
|
||||
if (input.File.FileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) throw Oops.Oh(ErrorCodeEnum.D8005);
|
||||
|
||||
// 判断是否重复上传的文件
|
||||
var sizeKb = input.File.Length / 1024; // 大小KB
|
||||
var fileMd5 = string.Empty;
|
||||
if (_uploadOptions.EnableMd5)
|
||||
{
|
||||
await using (var fileStream = input.File.OpenReadStream())
|
||||
{
|
||||
fileMd5 = OssUtils.ComputeContentMd5(fileStream, fileStream.Length);
|
||||
}
|
||||
// Mysql8 中如果使用了 utf8mb4_general_ci 之外的编码会出错,尽量避免在条件里使用.ToString()
|
||||
// 因为 Squsugar 并不是把变量转换为字符串来构造SQL语句,而是构造了CAST(123 AS CHAR)这样的语句,这样这个返回值是utf8mb4_general_ci,所以容易出错。
|
||||
var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileMd5 == fileMd5 && u.SizeKb == sizeKb);
|
||||
if (sysFile != null) return sysFile;
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
if (!_uploadOptions.ContentType.Contains(input.File.ContentType)) throw Oops.Oh($"{ErrorCodeEnum.D8001}:{input.File.ContentType}");
|
||||
|
||||
// 验证文件大小
|
||||
if (sizeKb > _uploadOptions.MaxSize) throw Oops.Oh($"{ErrorCodeEnum.D8002},允许最大:{_uploadOptions.MaxSize}KB");
|
||||
|
||||
// 获取文件后缀
|
||||
var suffix = Path.GetExtension(input.File.FileName).ToLower(); // 后缀
|
||||
if (string.IsNullOrWhiteSpace(suffix))
|
||||
suffix = string.Concat(".", input.File.ContentType.AsSpan(input.File.ContentType.LastIndexOf('/') + 1));
|
||||
if (!string.IsNullOrWhiteSpace(suffix))
|
||||
{
|
||||
//var contentTypeProvider = FS.GetFileExtensionContentTypeProvider();
|
||||
//suffix = contentTypeProvider.Mappings.FirstOrDefault(u => u.Value == file.ContentType).Key;
|
||||
// 修改 image/jpeg 类型返回的 .jpeg、jpe 后缀
|
||||
if (suffix == ".jpeg" || suffix == ".jpe")
|
||||
suffix = ".jpg";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003);
|
||||
|
||||
// 防止客户端伪造文件类型
|
||||
if (!string.IsNullOrWhiteSpace(input.AllowSuffix) && !input.AllowSuffix.Contains(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003);
|
||||
//if (!VerifyFileExtensionName.IsSameType(file.OpenReadStream(), suffix)) throw Oops.Oh(ErrorCodeEnum.D8001);
|
||||
|
||||
// 文件存储位置
|
||||
var path = string.IsNullOrWhiteSpace(targetPath) ? _uploadOptions.Path : targetPath;
|
||||
path = path.ParseToDateTimeForRep();
|
||||
|
||||
var newFile = input.Adapt<SysFile>();
|
||||
newFile.Id = YitIdHelper.NextId();
|
||||
|
||||
// 优先使用用户指定的存储桶名称,如果没有指定则使用默认配置
|
||||
if (!string.IsNullOrEmpty(input.BucketName))
|
||||
{
|
||||
newFile.BucketName = input.BucketName;
|
||||
}
|
||||
else
|
||||
{
|
||||
// MultiOSSFileProvider会自动使用默认配置
|
||||
newFile.BucketName = _OSSProviderOptions.Enabled ? _OSSProviderOptions.Bucket : "Local";
|
||||
}
|
||||
|
||||
newFile.FileName = Path.GetFileNameWithoutExtension(input.File.FileName);
|
||||
newFile.Suffix = suffix;
|
||||
newFile.SizeKb = sizeKb;
|
||||
newFile.FilePath = path;
|
||||
newFile.FileMd5 = fileMd5;
|
||||
newFile.DataId = input.DataId;
|
||||
|
||||
var finalName = newFile.Id + suffix; // 文件最终名称
|
||||
|
||||
newFile = await _customFileProvider.UploadFileAsync(input.File, newFile, path, finalName);
|
||||
await _sysFileRep.AsInsertable(newFile).ExecuteCommandAsync();
|
||||
return newFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传头像 🔖
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传头像")]
|
||||
public async Task<SysFile> UploadAvatar([Required] IFormFile file)
|
||||
{
|
||||
var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/avatar");
|
||||
|
||||
var sysUserRep = _sysFileRep.ChangeRepository<SqlSugarRepository<SysUser>>();
|
||||
var user = await sysUserRep.GetByIdAsync(_userManager.UserId);
|
||||
await sysUserRep.UpdateAsync(u => new SysUser() { Avatar = sysFile.Url }, u => u.Id == user.Id);
|
||||
// 删除已有头像文件
|
||||
if (!string.IsNullOrWhiteSpace(user.Avatar))
|
||||
{
|
||||
var fileId = Path.GetFileNameWithoutExtension(user.Avatar);
|
||||
if (long.TryParse(fileId, out var id))
|
||||
{
|
||||
try
|
||||
{
|
||||
await DeleteFile(new BaseIdInput { Id = id });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略删除旧头像文件的错误,不影响新头像上传
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sysFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传电子签名 🔖
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("上传电子签名")]
|
||||
public async Task<SysFile> UploadSignature([Required] IFormFile file)
|
||||
{
|
||||
var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/signature");
|
||||
|
||||
var sysUserRep = _sysFileRep.ChangeRepository<SqlSugarRepository<SysUser>>();
|
||||
var user = await sysUserRep.GetByIdAsync(_userManager.UserId);
|
||||
// 删除已有电子签名文件
|
||||
if (!string.IsNullOrWhiteSpace(user.Signature) && user.Signature.EndsWith(".png"))
|
||||
{
|
||||
var fileId = Path.GetFileNameWithoutExtension(user.Signature);
|
||||
if (long.TryParse(fileId, out var id))
|
||||
{
|
||||
try
|
||||
{
|
||||
await DeleteFile(new BaseIdInput { Id = id });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略删除旧签名文件的错误,不影响新签名上传
|
||||
}
|
||||
}
|
||||
}
|
||||
await sysUserRep.UpdateAsync(u => new SysUser() { Signature = sysFile.Url }, u => u.Id == user.Id);
|
||||
return sysFile;
|
||||
}
|
||||
|
||||
#region 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可
|
||||
|
||||
/// <summary>
|
||||
/// 更新文件的业务数据Id
|
||||
/// </summary>
|
||||
/// <param name="dataId"></param>
|
||||
/// <param name="sysFiles"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task UpdateFileByDataId(long dataId, List<SysFile> sysFiles)
|
||||
{
|
||||
var newFileIds = sysFiles.Select(u => u.Id).ToList();
|
||||
|
||||
// 求文件Id差集并删除(无效文件)
|
||||
var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId);
|
||||
var tmpFileIds = tmpFiles.Select(u => u.Id).ToList();
|
||||
var deleteFileIds = tmpFileIds.Except(newFileIds);
|
||||
foreach (var fileId in deleteFileIds)
|
||||
await DeleteFile(new BaseIdInput() { Id = fileId });
|
||||
|
||||
await _sysFileRep.UpdateAsync(u => new SysFile() { DataId = dataId }, u => newFileIds.Contains(u.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除业务数据对应的文件
|
||||
/// </summary>
|
||||
/// <param name="dataId"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task DeleteFileByDataId(long dataId)
|
||||
{
|
||||
// 删除冗余无效的物理文件
|
||||
var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId);
|
||||
foreach (var file in tmpFiles)
|
||||
await _customFileProvider.DeleteFileAsync(file);
|
||||
await _sysFileRep.AsDeleteable().Where(u => u.DataId == dataId).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
#endregion 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 作业持久化(数据库)
|
||||
/// </summary>
|
||||
public class DbJobPersistence : IJobPersistence
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public DbJobPersistence(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作业调度服务启动时
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
public async Task<IEnumerable<SchedulerBuilder>> PreloadAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
|
||||
var dynamicJobCompiler = scope.ServiceProvider.GetRequiredService<DynamicJobCompiler>();
|
||||
|
||||
// 获取所有定义的作业
|
||||
var allJobs = App.EffectiveTypes.ScanToBuilders().ToList();
|
||||
// 若数据库不存在任何作业,则直接返回
|
||||
if (!await db.Queryable<SysJobDetail>().AnyAsync(u => true, stoppingToken)) return allJobs;
|
||||
|
||||
// 遍历所有定义的作业
|
||||
foreach (var schedulerBuilder in allJobs)
|
||||
{
|
||||
// 获取作业信息构建器
|
||||
var jobBuilder = schedulerBuilder.GetJobBuilder();
|
||||
|
||||
// 加载数据库数据
|
||||
var dbDetail = await db.Queryable<SysJobDetail>().FirstAsync(u => u.JobId == jobBuilder.JobId, stoppingToken);
|
||||
if (dbDetail == null) continue;
|
||||
|
||||
// 同步数据库数据
|
||||
jobBuilder.LoadFrom(dbDetail);
|
||||
|
||||
// 获取作业的所有数据库的触发器
|
||||
var dbTriggers = await db.Queryable<SysJobTrigger>().Where(u => u.JobId == jobBuilder.JobId).ToListAsync(stoppingToken);
|
||||
// 遍历所有作业触发器
|
||||
foreach (var (_, triggerBuilder) in schedulerBuilder.GetEnumerable())
|
||||
{
|
||||
// 加载数据库数据
|
||||
var dbTrigger = dbTriggers.FirstOrDefault(u => u.JobId == jobBuilder.JobId && u.TriggerId == triggerBuilder.TriggerId);
|
||||
if (dbTrigger == null) continue;
|
||||
|
||||
triggerBuilder.LoadFrom(dbTrigger).Updated(); // 标记更新
|
||||
}
|
||||
// 遍历所有非编译时定义的触发器加入到作业中
|
||||
foreach (var dbTrigger in dbTriggers)
|
||||
{
|
||||
if (schedulerBuilder.GetTriggerBuilder(dbTrigger.TriggerId)?.JobId == jobBuilder.JobId) continue;
|
||||
var triggerBuilder = TriggerBuilder.Create(dbTrigger.TriggerId).LoadFrom(dbTrigger);
|
||||
schedulerBuilder.AddTriggerBuilder(triggerBuilder); // 先添加
|
||||
triggerBuilder.Updated(); // 再标记更新
|
||||
}
|
||||
|
||||
// 标记更新
|
||||
schedulerBuilder.Updated();
|
||||
}
|
||||
|
||||
// 获取数据库所有通过脚本创建的作业
|
||||
var allDbScriptJobs = await db.Queryable<SysJobDetail>().Where(u => u.CreateType != JobCreateTypeEnum.BuiltIn).ToListAsync(stoppingToken);
|
||||
foreach (var dbDetail in allDbScriptJobs)
|
||||
{
|
||||
// 动态创建作业
|
||||
Type jobType = dbDetail.CreateType switch
|
||||
{
|
||||
JobCreateTypeEnum.Script => dynamicJobCompiler.BuildJob(dbDetail.ScriptCode),
|
||||
JobCreateTypeEnum.Http => typeof(HttpJob),
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
// 动态构建的 jobType 的程序集名称为随机名称,需重新设置
|
||||
dbDetail.AssemblyName = jobType.Assembly.FullName!.Split(',')[0];
|
||||
var jobBuilder = JobBuilder.Create(jobType).LoadFrom(dbDetail);
|
||||
|
||||
// 强行设置为不扫描 IJob 实现类 [Trigger] 特性触发器,否则 SchedulerBuilder.Create 会再次扫描,导致重复添加同名触发器
|
||||
jobBuilder.SetIncludeAnnotations(false);
|
||||
|
||||
// 获取作业的所有数据库的触发器加入到作业中
|
||||
var dbTriggers = await db.Queryable<SysJobTrigger>().Where(u => u.JobId == jobBuilder.JobId).ToListAsync();
|
||||
var triggerBuilders = dbTriggers.Select(u => TriggerBuilder.Create(u.TriggerId).LoadFrom(u).Updated());
|
||||
var schedulerBuilder = SchedulerBuilder.Create(jobBuilder, triggerBuilders.ToArray());
|
||||
|
||||
// 标记更新
|
||||
schedulerBuilder.Updated();
|
||||
|
||||
allJobs.Add(schedulerBuilder);
|
||||
}
|
||||
|
||||
return allJobs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作业计划初始化通知
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <param name="stoppingToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task<SchedulerBuilder> OnLoadingAsync(SchedulerBuilder builder, CancellationToken stoppingToken)
|
||||
{
|
||||
return Task.FromResult(builder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作业计划Scheduler的JobDetail变化时
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public async Task OnChangedAsync(PersistenceContext context)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
|
||||
|
||||
var jobDetail = context.JobDetail.Adapt<SysJobDetail>();
|
||||
switch (context.Behavior)
|
||||
{
|
||||
case PersistenceBehavior.Appended:
|
||||
await db.Insertable(jobDetail).ExecuteCommandAsync();
|
||||
break;
|
||||
|
||||
case PersistenceBehavior.Updated:
|
||||
await db.Updateable(jobDetail).WhereColumns(u => new { u.JobId }).IgnoreColumns(u => new { u.Id, u.CreateType, u.ScriptCode }).ExecuteCommandAsync();
|
||||
break;
|
||||
|
||||
case PersistenceBehavior.Removed:
|
||||
await db.Deleteable<SysJobDetail>().Where(u => u.JobId == jobDetail.JobId).ExecuteCommandAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作业计划Scheduler的触发器Trigger变化时
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public async Task OnTriggerChangedAsync(PersistenceTriggerContext context)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
|
||||
|
||||
var jobTrigger = context.Trigger.Adapt<SysJobTrigger>();
|
||||
switch (context.Behavior)
|
||||
{
|
||||
case PersistenceBehavior.Appended:
|
||||
await db.Insertable(jobTrigger).ExecuteCommandAsync();
|
||||
break;
|
||||
|
||||
case PersistenceBehavior.Updated:
|
||||
await db.Updateable(jobTrigger).WhereColumns(u => new { u.TriggerId, u.JobId }).IgnoreColumns(u => new { u.Id }).ExecuteCommandAsync();
|
||||
break;
|
||||
|
||||
case PersistenceBehavior.Removed:
|
||||
await db.Deleteable<SysJobTrigger>().Where(u => u.TriggerId == jobTrigger.TriggerId && u.JobId == jobTrigger.JobId).ExecuteCommandAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作业触发器运行记录
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public async Task OnExecutionRecordAsync(PersistenceExecutionRecordContext context)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
|
||||
|
||||
var jobTriggerRecord = context.Timeline.Adapt<SysJobTriggerRecord>();
|
||||
await db.Insertable(jobTriggerRecord).ExecuteCommandAsync();
|
||||
|
||||
await scope.ServiceProvider.GetRequiredService<SysJobService>().ClearExpireJobTriggerRecord(jobTriggerRecord);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class JobDetailInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 作业Id
|
||||
/// </summary>
|
||||
public string JobId { get; set; }
|
||||
}
|
||||
|
||||
public class PageJobDetailInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 作业Id
|
||||
/// </summary>
|
||||
public string JobId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 组名称
|
||||
/// </summary>
|
||||
public string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 描述信息
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public class AddJobDetailInput : SysJobDetail
|
||||
{
|
||||
/// <summary>
|
||||
/// 作业Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "作业Id不能为空"), MinLength(2, ErrorMessage = "作业Id不能少于2个字符")]
|
||||
public override string JobId { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateJobDetailInput : AddJobDetailInput
|
||||
{
|
||||
}
|
||||
|
||||
public class DeleteJobDetailInput : JobDetailInput
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class JobDetailOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 作业信息
|
||||
/// </summary>
|
||||
public SysJobDetail JobDetail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发器集合
|
||||
/// </summary>
|
||||
public List<SysJobTrigger> JobTriggers { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class JobTriggerInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 作业Id
|
||||
/// </summary>
|
||||
public string JobId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发器Id
|
||||
/// </summary>
|
||||
public string TriggerId { get; set; }
|
||||
}
|
||||
|
||||
public class AddJobTriggerInput : SysJobTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// 作业Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "作业Id不能为空"), MinLength(2, ErrorMessage = "作业Id不能少于2个字符")]
|
||||
public override string JobId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发器Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "触发器Id不能为空"), MinLength(2, ErrorMessage = "触发器Id不能少于2个字符")]
|
||||
public override string TriggerId { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateJobTriggerInput : AddJobTriggerInput
|
||||
{
|
||||
}
|
||||
|
||||
public class DeleteJobTriggerInput : JobTriggerInput
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class PageJobTriggerRecordInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 作业Id
|
||||
/// </summary>
|
||||
public string JobId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发器Id
|
||||
/// </summary>
|
||||
public string TriggerId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 作业集群控制
|
||||
/// </summary>
|
||||
public class JobClusterServer : IJobClusterServer
|
||||
{
|
||||
private static readonly SqlSugarRepository<SysJobCluster> _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
|
||||
private readonly Random _random = new(DateTime.Now.Millisecond);
|
||||
|
||||
/// <summary>
|
||||
/// 当前作业调度器启动通知
|
||||
/// </summary>
|
||||
/// <param name="context">作业集群服务上下文</param>
|
||||
public async void Start(JobClusterContext context)
|
||||
{
|
||||
// 在作业集群表中,如果 clusterId 不存在,则新增一条(否则更新一条),并设置 status 为 ClusterStatus.Waiting
|
||||
if (await _sysJobClusterRep.IsAnyAsync(u => u.ClusterId == context.ClusterId))
|
||||
{
|
||||
await _sysJobClusterRep.AsUpdateable().SetColumns(u => u.Status == ClusterStatus.Waiting).Where(u => u.ClusterId == context.ClusterId).ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _sysJobClusterRep.AsInsertable(new SysJobCluster { ClusterId = context.ClusterId, Status = ClusterStatus.Waiting }).ExecuteCommandAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待被唤醒
|
||||
/// </summary>
|
||||
/// <param name="context">作业集群服务上下文</param>
|
||||
/// <returns><see cref="Task"/></returns>
|
||||
public async Task WaitingForAsync(JobClusterContext context)
|
||||
{
|
||||
var clusterId = context.ClusterId;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// 控制集群心跳频率(放在头部为了防止 IsAnyAsync continue 没sleep占用大量IO和CPU)
|
||||
await Task.Delay(3000 + _random.Next(500, 1000)); // 错开集群同时启动
|
||||
|
||||
try
|
||||
{
|
||||
ICache cache = App.GetRequiredService<ICacheProvider>().Cache;
|
||||
// 使用分布式锁
|
||||
using (cache.AcquireLock("lock:JobClusterServer:WaitingForAsync", 1000))
|
||||
{
|
||||
// 在这里查询数据库,根据以下两种情况处理
|
||||
// 1) 如果作业集群表已有 status 为 ClusterStatus.Working 则继续循环
|
||||
// 2) 如果作业集群表中还没有其他服务或只有自己,则插入一条集群服务或调用 await WorkNowAsync(clusterId); 之后 return;
|
||||
// 3) 如果作业集群表中没有 status 为 ClusterStatus.Working 的,调用 await WorkNowAsync(clusterId); 之后 return;
|
||||
if (await _sysJobClusterRep.IsAnyAsync(u => u.Status == ClusterStatus.Working)) continue;
|
||||
|
||||
await WorkNowAsync(clusterId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前作业调度器停止通知
|
||||
/// </summary>
|
||||
/// <param name="context">作业集群服务上下文</param>
|
||||
public async void Stop(JobClusterContext context)
|
||||
{
|
||||
// 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
|
||||
await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前作业调度器宕机
|
||||
/// </summary>
|
||||
/// <param name="context">作业集群服务上下文</param>
|
||||
public async void Crash(JobClusterContext context)
|
||||
{
|
||||
// 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
|
||||
await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指示集群可以工作
|
||||
/// </summary>
|
||||
/// <param name="clusterId">集群 Id</param>
|
||||
/// <returns></returns>
|
||||
private static async Task WorkNowAsync(string clusterId)
|
||||
{
|
||||
// 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working
|
||||
await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Working }, u => u.ClusterId == clusterId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 作业执行监视器
|
||||
/// </summary>
|
||||
public class JobMonitor : IJobMonitor
|
||||
{
|
||||
private readonly SysConfigService _sysConfigService;
|
||||
private readonly IEventPublisher _eventPublisher;
|
||||
private readonly ILogger<JobMonitor> _logger;
|
||||
|
||||
public JobMonitor(IServiceScopeFactory serviceScopeFactory, IEventPublisher eventPublisher, ILogger<JobMonitor> logger)
|
||||
{
|
||||
var serviceScope = serviceScopeFactory.CreateScope();
|
||||
_sysConfigService = serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
|
||||
_eventPublisher = eventPublisher;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task OnExecutingAsync(JobExecutingContext context, CancellationToken stoppingToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task OnExecutedAsync(JobExecutedContext context, CancellationToken stoppingToken)
|
||||
{
|
||||
if (context.Exception == null) return;
|
||||
|
||||
var exception = $"定时任务【{context.Trigger.Description}】错误:{context.Exception}";
|
||||
// 将作业异常信息记录到本地
|
||||
_logger.LogError(exception);
|
||||
|
||||
if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysErrorMail))
|
||||
{
|
||||
// 将作业异常信息发送到邮件
|
||||
await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, exception, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统作业任务服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 320, Description = "作业任务")]
|
||||
public class SysJobService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysJobDetail> _sysJobDetailRep;
|
||||
private readonly SqlSugarRepository<SysJobTrigger> _sysJobTriggerRep;
|
||||
private readonly SqlSugarRepository<SysJobTriggerRecord> _sysJobTriggerRecordRep;
|
||||
private readonly SqlSugarRepository<SysJobCluster> _sysJobClusterRep;
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
private readonly DynamicJobCompiler _dynamicJobCompiler;
|
||||
|
||||
public SysJobService(SqlSugarRepository<SysJobDetail> sysJobDetailRep,
|
||||
SqlSugarRepository<SysJobTrigger> sysJobTriggerRep,
|
||||
SqlSugarRepository<SysJobTriggerRecord> sysJobTriggerRecordRep,
|
||||
SqlSugarRepository<SysJobCluster> sysJobClusterRep,
|
||||
ISchedulerFactory schedulerFactory,
|
||||
DynamicJobCompiler dynamicJobCompiler)
|
||||
{
|
||||
_sysJobDetailRep = sysJobDetailRep;
|
||||
_sysJobTriggerRep = sysJobTriggerRep;
|
||||
_sysJobTriggerRecordRep = sysJobTriggerRecordRep;
|
||||
_sysJobClusterRep = sysJobClusterRep;
|
||||
_schedulerFactory = schedulerFactory;
|
||||
_dynamicJobCompiler = dynamicJobCompiler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取作业分页列表 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("获取作业分页列表")]
|
||||
public async Task<SqlSugarPagedList<JobDetailOutput>> PageJobDetail(PageJobDetailInput input)
|
||||
{
|
||||
var jobDetails = await _sysJobDetailRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId.Contains(input.JobId.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), u => u.GroupName.Contains(input.GroupName.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Description), u => u.Description.Contains(input.Description.Trim()))
|
||||
.Select(d => new JobDetailOutput
|
||||
{
|
||||
JobDetail = d,
|
||||
}).ToPagedListAsync(input.Page, input.PageSize);
|
||||
await _sysJobDetailRep.AsSugarClient().ThenMapperAsync(jobDetails.Items, async u =>
|
||||
{
|
||||
u.JobTriggers = await _sysJobTriggerRep.GetListAsync(t => t.JobId == u.JobDetail.JobId);
|
||||
});
|
||||
|
||||
// 提取中括号里面的参数值
|
||||
var rgx = new Regex(@"(?i)(?<=\[)(.*)(?=\])");
|
||||
foreach (var job in jobDetails.Items)
|
||||
{
|
||||
foreach (var jobTrigger in job.JobTriggers)
|
||||
{
|
||||
jobTrigger.Args = rgx.Match(jobTrigger.Args ?? "").Value;
|
||||
}
|
||||
}
|
||||
return jobDetails;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取作业组名称集合 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("获取作业组名称集合")]
|
||||
public async Task<List<string>> ListJobGroup()
|
||||
{
|
||||
return await _sysJobDetailRep.AsQueryable().Distinct().Select(e => e.GroupName).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加作业 ⏰
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "AddJobDetail"), HttpPost]
|
||||
[DisplayName("添加作业")]
|
||||
public async Task AddJobDetail(AddJobDetailInput input)
|
||||
{
|
||||
var isExist = await _sysJobDetailRep.IsAnyAsync(u => u.JobId == input.JobId && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006);
|
||||
|
||||
// 动态创建作业
|
||||
Type jobType;
|
||||
switch (input.CreateType)
|
||||
{
|
||||
case JobCreateTypeEnum.Script when string.IsNullOrEmpty(input.ScriptCode):
|
||||
throw Oops.Oh(ErrorCodeEnum.D1701);
|
||||
case JobCreateTypeEnum.Script:
|
||||
{
|
||||
jobType = _dynamicJobCompiler.BuildJob(input.ScriptCode);
|
||||
|
||||
if (jobType.GetCustomAttributes(typeof(JobDetailAttribute)).FirstOrDefault() is not JobDetailAttribute jobDetailAttribute)
|
||||
throw Oops.Oh(ErrorCodeEnum.D1702);
|
||||
if (jobDetailAttribute.JobId != input.JobId)
|
||||
throw Oops.Oh(ErrorCodeEnum.D1703);
|
||||
break;
|
||||
}
|
||||
case JobCreateTypeEnum.Http:
|
||||
jobType = typeof(HttpJob);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
_schedulerFactory.AddJob(JobBuilder.Create(jobType).LoadFrom(input.Adapt<SysJobDetail>()).SetJobType(jobType));
|
||||
|
||||
// 延迟一下等待持久化写入,再执行其他字段的更新
|
||||
await Task.Delay(500);
|
||||
await _sysJobDetailRep.AsUpdateable()
|
||||
.SetColumns(u => new SysJobDetail { CreateType = input.CreateType, ScriptCode = input.ScriptCode })
|
||||
.Where(u => u.JobId == input.JobId).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新作业 ⏰
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "UpdateJobDetail"), HttpPost]
|
||||
[DisplayName("更新作业")]
|
||||
public async Task UpdateJobDetail(UpdateJobDetailInput input)
|
||||
{
|
||||
var isExist = await _sysJobDetailRep.IsAnyAsync(u => u.JobId == input.JobId && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006);
|
||||
|
||||
var sysJobDetail = await _sysJobDetailRep.GetFirstAsync(u => u.Id == input.Id);
|
||||
if (sysJobDetail.JobId != input.JobId) throw Oops.Oh(ErrorCodeEnum.D1704);
|
||||
|
||||
var scheduler = _schedulerFactory.GetJob(sysJobDetail.JobId);
|
||||
var oldScriptCode = sysJobDetail.ScriptCode; // 旧脚本代码
|
||||
input.Adapt(sysJobDetail);
|
||||
|
||||
if (input.CreateType == JobCreateTypeEnum.Script)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input.ScriptCode)) throw Oops.Oh(ErrorCodeEnum.D1701);
|
||||
|
||||
if (input.ScriptCode != oldScriptCode)
|
||||
{
|
||||
// 动态创建作业
|
||||
var jobType = _dynamicJobCompiler.BuildJob(input.ScriptCode);
|
||||
|
||||
if (jobType.GetCustomAttributes(typeof(JobDetailAttribute)).FirstOrDefault() is not JobDetailAttribute jobDetailAttribute)
|
||||
throw Oops.Oh(ErrorCodeEnum.D1702);
|
||||
if (jobDetailAttribute.JobId != input.JobId) throw Oops.Oh(ErrorCodeEnum.D1703);
|
||||
|
||||
scheduler?.UpdateDetail(JobBuilder.Create(jobType).LoadFrom(sysJobDetail).SetJobType(jobType));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
scheduler?.UpdateDetail(scheduler.GetJobBuilder().LoadFrom(sysJobDetail));
|
||||
}
|
||||
|
||||
// Tip: 假如这次更新有变更了 JobId,变更 JobId 后触发的持久化更新执行,会由于找不到 JobId 而更新不到数据
|
||||
// 延迟一下等待持久化写入,再执行其他字段的更新
|
||||
await Task.Delay(500);
|
||||
await _sysJobDetailRep.UpdateAsync(sysJobDetail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除作业 ⏰
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "DeleteJobDetail"), HttpPost]
|
||||
[DisplayName("删除作业")]
|
||||
public async Task DeleteJobDetail(DeleteJobDetailInput input)
|
||||
{
|
||||
_schedulerFactory.RemoveJob(input.JobId);
|
||||
|
||||
// 如果 _schedulerFactory 中不存在 JodId,则无法触发持久化,下面的代码确保作业和触发器能被删除
|
||||
await _sysJobDetailRep.DeleteAsync(u => u.JobId == input.JobId);
|
||||
await _sysJobTriggerRep.DeleteAsync(u => u.JobId == input.JobId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取触发器列表 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("获取触发器列表")]
|
||||
public async Task<List<SysJobTrigger>> GetJobTriggerList([FromQuery] JobDetailInput input)
|
||||
{
|
||||
return await _sysJobTriggerRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId.Contains(input.JobId))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加触发器 ⏰
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "AddJobTrigger"), HttpPost]
|
||||
[DisplayName("添加触发器")]
|
||||
public async Task AddJobTrigger(AddJobTriggerInput input)
|
||||
{
|
||||
var isExist = await _sysJobTriggerRep.IsAnyAsync(u => u.TriggerId == input.TriggerId && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006);
|
||||
|
||||
var jobTrigger = input.Adapt<SysJobTrigger>();
|
||||
jobTrigger.Args = "[" + jobTrigger.Args + "]";
|
||||
|
||||
var scheduler = _schedulerFactory.GetJob(input.JobId);
|
||||
scheduler?.AddTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新触发器 ⏰
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "UpdateJobTrigger"), HttpPost]
|
||||
[DisplayName("更新触发器")]
|
||||
public async Task UpdateJobTrigger(UpdateJobTriggerInput input)
|
||||
{
|
||||
var isExist = await _sysJobTriggerRep.IsAnyAsync(u => u.TriggerId == input.TriggerId && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006);
|
||||
|
||||
var jobTrigger = input.Adapt<SysJobTrigger>();
|
||||
if (jobTrigger.EndTime.HasValue && jobTrigger.EndTime.Value.Year < 1901)
|
||||
{
|
||||
jobTrigger.EndTime = null;
|
||||
}
|
||||
if (jobTrigger.StartTime.HasValue && jobTrigger.StartTime.Value.Year < 1901)
|
||||
{
|
||||
jobTrigger.StartTime = null;
|
||||
}
|
||||
jobTrigger.Args = "[" + jobTrigger.Args + "]";
|
||||
|
||||
var scheduler = _schedulerFactory.GetJob(input.JobId);
|
||||
scheduler?.UpdateTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除触发器 ⏰
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "DeleteJobTrigger"), HttpPost]
|
||||
[DisplayName("删除触发器")]
|
||||
public async Task DeleteJobTrigger(DeleteJobTriggerInput input)
|
||||
{
|
||||
var scheduler = _schedulerFactory.GetJob(input.JobId);
|
||||
scheduler?.RemoveTrigger(input.TriggerId);
|
||||
|
||||
// 如果 _schedulerFactory 中不存在 JodId,则无法触发持久化,下行代码确保触发器能被删除
|
||||
await _sysJobTriggerRep.DeleteAsync(u => u.JobId == input.JobId && u.TriggerId == input.TriggerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停所有作业 ⏰
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("暂停所有作业")]
|
||||
public void PauseAllJob()
|
||||
{
|
||||
_schedulerFactory.PauseAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动所有作业 ⏰
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("启动所有作业")]
|
||||
public void StartAllJob()
|
||||
{
|
||||
_schedulerFactory.StartAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停作业 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("暂停作业")]
|
||||
public void PauseJob(JobDetailInput input)
|
||||
{
|
||||
_schedulerFactory.TryPauseJob(input.JobId, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动作业 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("启动作业")]
|
||||
public void StartJob(JobDetailInput input)
|
||||
{
|
||||
_schedulerFactory.TryStartJob(input.JobId, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消作业 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("取消作业")]
|
||||
public void CancelJob(JobDetailInput input)
|
||||
{
|
||||
_schedulerFactory.TryCancelJob(input.JobId, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行作业 ⏰
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
[DisplayName("执行作业")]
|
||||
public void RunJob(JobDetailInput input)
|
||||
{
|
||||
if (_schedulerFactory.TryRunJob(input.JobId, out _) != ScheduleResult.Succeed) throw Oops.Oh(ErrorCodeEnum.D1705);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停触发器 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("暂停触发器")]
|
||||
public void PauseTrigger(JobTriggerInput input)
|
||||
{
|
||||
var scheduler = _schedulerFactory.GetJob(input.JobId);
|
||||
scheduler?.PauseTrigger(input.TriggerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动触发器 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("启动触发器")]
|
||||
public void StartTrigger(JobTriggerInput input)
|
||||
{
|
||||
var scheduler = _schedulerFactory.GetJob(input.JobId);
|
||||
scheduler?.StartTrigger(input.TriggerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制唤醒作业调度器 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("强制唤醒作业调度器")]
|
||||
public void CancelSleep()
|
||||
{
|
||||
_schedulerFactory.CancelSleep();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制触发所有作业持久化 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("强制触发所有作业持久化")]
|
||||
public void PersistAll()
|
||||
{
|
||||
_schedulerFactory.PersistAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取集群列表 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("获取集群列表")]
|
||||
public async Task<List<SysJobCluster>> GetJobClusterList()
|
||||
{
|
||||
return await _sysJobClusterRep.GetListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取作业触发器运行记录分页列表 ⏰
|
||||
/// </summary>
|
||||
[DisplayName("获取作业触发器运行记录分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysJobTriggerRecord>> PageJobTriggerRecord(PageJobTriggerRecordInput input)
|
||||
{
|
||||
return await _sysJobTriggerRecordRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId == input.JobId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.TriggerId), u => u.TriggerId == input.TriggerId)
|
||||
.OrderByDescending(u => u.Id)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空作业触发器运行记录 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "ClearJobTriggerRecord"), HttpPost]
|
||||
[DisplayName("清空作业触发器运行记录")]
|
||||
public void ClearJobTriggerRecord()
|
||||
{
|
||||
_sysJobTriggerRecordRep.AsSugarClient().DbMaintenance.TruncateTable<SysJobTriggerRecord>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空不保留的作业触发器运行记录 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
[DisplayName("清空过期的作业触发器运行记录")]
|
||||
public async Task ClearExpireJobTriggerRecord(SysJobTriggerRecord input)
|
||||
{
|
||||
int keepRecords = 30;//保留记录条数
|
||||
// 使用CopyNew()创建新的数据库连接实例,避免连接冲突
|
||||
var db = _sysJobTriggerRecordRep.AsSugarClient().CopyNew();
|
||||
await db.Deleteable<SysJobTriggerRecord>().In(it => it.Id,
|
||||
db.Queryable<SysJobTriggerRecord>()
|
||||
.Skip(keepRecords)
|
||||
.OrderByDescending(it => it.LastRunTime)
|
||||
.Where(u => u.JobId == input.JobId && u.TriggerId == input.TriggerId)
|
||||
.Select(it => it.Id) //注意Select不要ToList(), ToList就2次查询了
|
||||
).ExecuteCommandAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 语言输出参数
|
||||
/// </summary>
|
||||
public class SysLangDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 语言代码
|
||||
/// </summary>
|
||||
public string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL 语言代码
|
||||
/// </summary>
|
||||
public string UrlCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 书写方向
|
||||
/// </summary>
|
||||
public DirectionEnum Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日期格式
|
||||
/// </summary>
|
||||
public string DateFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间格式
|
||||
/// </summary>
|
||||
public string TimeFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每周起始日
|
||||
/// </summary>
|
||||
public WeekEnum WeekStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组符号
|
||||
/// </summary>
|
||||
public string Grouping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小数点符号
|
||||
/// </summary>
|
||||
public string DecimalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 千分位分隔符
|
||||
/// </summary>
|
||||
public string? ThousandsSep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool Active { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime? CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者Id
|
||||
/// </summary>
|
||||
public long? CreateUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者姓名
|
||||
/// </summary>
|
||||
public string? CreateUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者Id
|
||||
/// </summary>
|
||||
public long? UpdateUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者姓名
|
||||
/// </summary>
|
||||
public string? UpdateUserName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 语言基础输入参数
|
||||
/// </summary>
|
||||
public class SysLangBaseInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
public virtual long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言名称不能为空")]
|
||||
public virtual string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言代码不能为空")]
|
||||
public virtual string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "ISO 语言代码不能为空")]
|
||||
public virtual string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "URL 语言代码不能为空")]
|
||||
public virtual string UrlCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 书写方向
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "书写方向不能为空")]
|
||||
public virtual DirectionEnum Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日期格式
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "日期格式不能为空")]
|
||||
public virtual string DateFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间格式
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "时间格式不能为空")]
|
||||
public virtual string TimeFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每周起始日
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "每周起始日不能为空")]
|
||||
public virtual WeekEnum? WeekStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组符号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "分组符号不能为空")]
|
||||
public virtual string Grouping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小数点符号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "小数点符号不能为空")]
|
||||
public virtual string DecimalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 千分位分隔符
|
||||
/// </summary>
|
||||
public virtual string? ThousandsSep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "是否启用不能为空")]
|
||||
public virtual bool? Active { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多语言分页查询输入参数
|
||||
/// </summary>
|
||||
public class PageSysLangInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 语言代码
|
||||
/// </summary>
|
||||
public string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL 语言代码
|
||||
/// </summary>
|
||||
public string UrlCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool? Active { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 选中主键列表
|
||||
/// </summary>
|
||||
public List<long> SelectKeyList { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多语言增加输入参数
|
||||
/// </summary>
|
||||
public class AddSysLangInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言名称不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "语言名称字符长度不能超过255")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言代码不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")]
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "ISO 语言代码不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "ISO 语言代码字符长度不能超过255")]
|
||||
public string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "URL 语言代码不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "URL 语言代码字符长度不能超过255")]
|
||||
public string UrlCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 书写方向
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "书写方向不能为空")]
|
||||
public DirectionEnum Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日期格式
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "日期格式不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "日期格式字符长度不能超过255")]
|
||||
public string DateFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间格式
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "时间格式不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "时间格式字符长度不能超过255")]
|
||||
public string TimeFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每周起始日
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "每周起始日不能为空")]
|
||||
public WeekEnum? WeekStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组符号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "分组符号不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "分组符号字符长度不能超过255")]
|
||||
public string Grouping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小数点符号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "小数点符号不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "小数点符号字符长度不能超过255")]
|
||||
public string DecimalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 千分位分隔符
|
||||
/// </summary>
|
||||
[MaxLength(255, ErrorMessage = "千分位分隔符字符长度不能超过255")]
|
||||
public string? ThousandsSep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "是否启用不能为空")]
|
||||
public bool? Active { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多语言删除输入参数
|
||||
/// </summary>
|
||||
public class DeleteSysLangInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "主键Id不能为空")]
|
||||
public long? Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多语言更新输入参数
|
||||
/// </summary>
|
||||
public class UpdateSysLangInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "主键Id不能为空")]
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言名称不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "语言名称字符长度不能超过255")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言代码不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")]
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "ISO 语言代码不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "ISO 语言代码字符长度不能超过255")]
|
||||
public string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "URL 语言代码不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "URL 语言代码字符长度不能超过255")]
|
||||
public string UrlCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 书写方向
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "书写方向不能为空")]
|
||||
public DirectionEnum Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日期格式
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "日期格式不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "日期格式字符长度不能超过255")]
|
||||
public string DateFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间格式
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "时间格式不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "时间格式字符长度不能超过255")]
|
||||
public string TimeFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每周起始日
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "每周起始日不能为空")]
|
||||
public WeekEnum? WeekStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组符号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "分组符号不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "分组符号字符长度不能超过255")]
|
||||
public string Grouping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小数点符号
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "小数点符号不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "小数点符号字符长度不能超过255")]
|
||||
public string DecimalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 千分位分隔符
|
||||
/// </summary>
|
||||
[MaxLength(255, ErrorMessage = "千分位分隔符字符长度不能超过255")]
|
||||
public string? ThousandsSep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "是否启用不能为空")]
|
||||
public bool? Active { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多语言主键查询输入参数
|
||||
/// </summary>
|
||||
public class QueryByIdSysLangInput : DeleteSysLangInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多语言数据导入实体
|
||||
/// </summary>
|
||||
[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)]
|
||||
public class ImportSysLangInput : BaseImportInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言名称
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*语言名称")]
|
||||
[ExporterHeader("*语言名称", Format = "", Width = 25, IsBold = true)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*语言代码")]
|
||||
[ExporterHeader("*语言代码", Format = "", Width = 25, IsBold = true)]
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 语言代码
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*ISO 语言代码")]
|
||||
[ExporterHeader("*ISO 语言代码", Format = "", Width = 25, IsBold = true)]
|
||||
public string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL 语言代码
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*URL 语言代码")]
|
||||
[ExporterHeader("*URL 语言代码", Format = "", Width = 25, IsBold = true)]
|
||||
public string UrlCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 书写方向
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*书写方向")]
|
||||
[ExporterHeader("*书写方向", Format = "", Width = 25, IsBold = true)]
|
||||
public DirectionEnum Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日期格式
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*日期格式")]
|
||||
[ExporterHeader("*日期格式", Format = "", Width = 25, IsBold = true)]
|
||||
public string DateFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间格式
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*时间格式")]
|
||||
[ExporterHeader("*时间格式", Format = "", Width = 25, IsBold = true)]
|
||||
public string TimeFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每周起始日
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*每周起始日")]
|
||||
[ExporterHeader("*每周起始日", Format = "", Width = 25, IsBold = true)]
|
||||
public WeekEnum? WeekStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组符号
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*分组符号")]
|
||||
[ExporterHeader("*分组符号", Format = "", Width = 25, IsBold = true)]
|
||||
public string Grouping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小数点符号
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*小数点符号")]
|
||||
[ExporterHeader("*小数点符号", Format = "", Width = 25, IsBold = true)]
|
||||
public string DecimalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 千分位分隔符
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "千分位分隔符")]
|
||||
[ExporterHeader("千分位分隔符", Format = "", Width = 25, IsBold = true)]
|
||||
public string? ThousandsSep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*是否启用")]
|
||||
[ExporterHeader("*是否启用", Format = "", Width = 25, IsBold = true)]
|
||||
public bool? Active { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 语言输出参数
|
||||
/// </summary>
|
||||
public class SysLangOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 语言代码
|
||||
/// </summary>
|
||||
public string IsoCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL 语言代码
|
||||
/// </summary>
|
||||
public string UrlCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 书写方向
|
||||
/// </summary>
|
||||
public DirectionEnum Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日期格式
|
||||
/// </summary>
|
||||
public string DateFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间格式
|
||||
/// </summary>
|
||||
public string TimeFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每周起始日
|
||||
/// </summary>
|
||||
public WeekEnum WeekStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组符号
|
||||
/// </summary>
|
||||
public string Grouping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小数点符号
|
||||
/// </summary>
|
||||
public string DecimalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 千分位分隔符
|
||||
/// </summary>
|
||||
public string? ThousandsSep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool Active { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime? CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者Id
|
||||
/// </summary>
|
||||
public long? CreateUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者姓名
|
||||
/// </summary>
|
||||
public string? CreateUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者Id
|
||||
/// </summary>
|
||||
public long? UpdateUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者姓名
|
||||
/// </summary>
|
||||
public string? UpdateUserName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多语言数据导入模板实体
|
||||
/// </summary>
|
||||
public class ExportSysLangOutput : ImportSysLangInput
|
||||
{
|
||||
[ImporterHeader(IsIgnore = true)]
|
||||
[ExporterHeader(IsIgnore = true)]
|
||||
public override string Error { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 语言服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 100, Description = "语言服务")]
|
||||
public partial class SysLangService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysLang> _sysLangRep;
|
||||
|
||||
public SysLangService(SqlSugarRepository<SysLang> sysLangRep)
|
||||
{
|
||||
_sysLangRep = sysLangRep;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询语言 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("分页查询语言")]
|
||||
[ApiDescriptionSettings(Name = "Page"), HttpPost]
|
||||
public async Task<SqlSugarPagedList<SysLangOutput>> Page(PageSysLangInput input)
|
||||
{
|
||||
input.Keyword = input.Keyword?.Trim();
|
||||
var query = _sysLangRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Name.Contains(input.Keyword) || u.Code.Contains(input.Keyword) || u.IsoCode.Contains(input.Keyword) || u.UrlCode.Contains(input.Keyword))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.IsoCode), u => u.IsoCode.Contains(input.IsoCode.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.UrlCode), u => u.UrlCode.Contains(input.UrlCode.Trim()))
|
||||
.Select<SysLangOutput>();
|
||||
return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取语言详情 ℹ️
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取语言详情")]
|
||||
[ApiDescriptionSettings(Name = "Detail"), HttpGet]
|
||||
public async Task<SysLang> Detail([FromQuery] QueryByIdSysLangInput input)
|
||||
{
|
||||
return await _sysLangRep.GetFirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加语言 ➕
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("增加语言")]
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
public async Task<long> Add(AddSysLangInput input)
|
||||
{
|
||||
var entity = input.Adapt<SysLang>();
|
||||
return await _sysLangRep.InsertAsync(entity) ? entity.Id : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新语言 ✏️
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("更新语言")]
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
public async Task Update(UpdateSysLangInput input)
|
||||
{
|
||||
var entity = input.Adapt<SysLang>();
|
||||
await _sysLangRep.AsUpdateable(entity)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除语言 ❌
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("删除语言")]
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
public async Task Delete(DeleteSysLangInput input)
|
||||
{
|
||||
var entity = await _sysLangRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
await _sysLangRep.DeleteAsync(entity); //真删除
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取下拉列表数据 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[DisplayName("获取下拉列表数据")]
|
||||
[ApiDescriptionSettings(Name = "DropdownData"), HttpPost]
|
||||
public async Task<dynamic> DropdownData()
|
||||
{
|
||||
var data = await _sysLangRep.Context.Queryable<SysLang>()
|
||||
.Where(m => m.Active == true)
|
||||
.Select(u => new
|
||||
{
|
||||
Code = u.Code,
|
||||
Value = u.UrlCode,
|
||||
Label = $"{u.Name}"
|
||||
}).ToListAsync();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表输出参数
|
||||
/// </summary>
|
||||
public class SysLangTextDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体名
|
||||
/// </summary>
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体ID
|
||||
/// </summary>
|
||||
public long EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
public string LangCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译内容
|
||||
/// </summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime? CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者Id
|
||||
/// </summary>
|
||||
public long? CreateUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者姓名
|
||||
/// </summary>
|
||||
public string? CreateUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者Id
|
||||
/// </summary>
|
||||
public long? UpdateUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者姓名
|
||||
/// </summary>
|
||||
public string? UpdateUserName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表基础输入参数
|
||||
/// </summary>
|
||||
public class SysLangTextBaseInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
public virtual long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "所属实体名不能为空")]
|
||||
public virtual string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "所属实体ID不能为空")]
|
||||
public virtual long? EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "字段名不能为空")]
|
||||
public virtual string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言代码不能为空")]
|
||||
public virtual string LangCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译内容
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "翻译内容不能为空")]
|
||||
public virtual string Content { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表分页查询输入参数
|
||||
/// </summary>
|
||||
public class PageSysLangTextInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属实体名
|
||||
/// </summary>
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体ID
|
||||
/// </summary>
|
||||
public long? EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
public string LangCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译内容
|
||||
/// </summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 选中主键列表
|
||||
/// </summary>
|
||||
public List<long> SelectKeyList { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表增加输入参数
|
||||
/// </summary>
|
||||
public class AddSysLangTextInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属实体名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "所属实体名不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "所属实体名字符长度不能超过255")]
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "所属实体ID不能为空")]
|
||||
public long? EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "字段名不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "字段名字符长度不能超过255")]
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言代码不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")]
|
||||
public string LangCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译内容
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "翻译内容不能为空")]
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表输入参数
|
||||
/// </summary>
|
||||
public class ListSysLangTextInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属实体名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "所属实体名不能为空")]
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "所属实体ID不能为空")]
|
||||
public long? EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "字段名不能为空")]
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
public string LangCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表删除输入参数
|
||||
/// </summary>
|
||||
public class DeleteSysLangTextInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "主键Id不能为空")]
|
||||
public long? Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表更新输入参数
|
||||
/// </summary>
|
||||
public class UpdateSysLangTextInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "主键Id不能为空")]
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "所属实体名不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "所属实体名字符长度不能超过255")]
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "所属实体ID不能为空")]
|
||||
public long? EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "字段名不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "字段名字符长度不能超过255")]
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "语言代码不能为空")]
|
||||
[MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")]
|
||||
public string LangCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译内容
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "翻译内容不能为空")]
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表主键查询输入参数
|
||||
/// </summary>
|
||||
public class QueryByIdSysLangTextInput : DeleteSysLangTextInput
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表数据导入实体
|
||||
/// </summary>
|
||||
[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)]
|
||||
public class ImportSysLangTextInput : BaseImportInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属实体名
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*所属实体名")]
|
||||
[ExporterHeader("*所属实体名", Format = "", Width = 25, IsBold = true)]
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体ID
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*所属实体ID")]
|
||||
[ExporterHeader("*所属实体ID", Format = "", Width = 25, IsBold = true)]
|
||||
public long? EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*字段名")]
|
||||
[ExporterHeader("*字段名", Format = "", Width = 25, IsBold = true)]
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*语言代码")]
|
||||
[ExporterHeader("*语言代码", Format = "", Width = 25, IsBold = true)]
|
||||
public string LangCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译内容
|
||||
/// </summary>
|
||||
[ImporterHeader(Name = "*翻译内容")]
|
||||
[ExporterHeader("*翻译内容", Format = "", Width = 25, IsBold = true)]
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class AiTranslateTextInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 原文
|
||||
/// </summary>
|
||||
public string OriginalText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标语言
|
||||
/// </summary>
|
||||
public string TargetLang { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表输出参数
|
||||
/// </summary>
|
||||
public class SysLangTextOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体名
|
||||
/// </summary>
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属实体ID
|
||||
/// </summary>
|
||||
public long EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字段名
|
||||
/// </summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
public string LangCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译内容
|
||||
/// </summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime? CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者Id
|
||||
/// </summary>
|
||||
public long? CreateUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者姓名
|
||||
/// </summary>
|
||||
public string? CreateUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者Id
|
||||
/// </summary>
|
||||
public long? UpdateUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者姓名
|
||||
/// </summary>
|
||||
public string? UpdateUserName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译表数据导入模板实体
|
||||
/// </summary>
|
||||
public class ExportSysLangTextOutput : ImportSysLangTextInput
|
||||
{
|
||||
[ImporterHeader(IsIgnore = true)]
|
||||
[ExporterHeader(IsIgnore = true)]
|
||||
public override string Error { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class LangFieldMap<TEntity>
|
||||
{
|
||||
/// <summary>实体名,如 Product</summary>
|
||||
public string EntityName { get; set; }
|
||||
|
||||
/// <summary>字段名,如 Name/Description</summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>如何取主键ID</summary>
|
||||
public Func<TEntity, long> IdSelector { get; set; }
|
||||
|
||||
/// <summary>如何写回翻译值</summary>
|
||||
public Action<TEntity, string> SetTranslatedValue { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻译缓存服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 100, Description = "翻译缓存服务")]
|
||||
public class SysLangTextCacheService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly SqlSugarRepository<SysLangText> _sysLangTextRep;
|
||||
private TimeSpan expireSeconds = TimeSpan.FromHours(1);
|
||||
|
||||
public SysLangTextCacheService(
|
||||
SysCacheService sysCacheService,
|
||||
SqlSugarRepository<SysLangText> sysLangTextRep)
|
||||
{
|
||||
_sysCacheService = sysCacheService;
|
||||
_sysLangTextRep = sysLangTextRep;
|
||||
}
|
||||
|
||||
private string BuildKey(string entityName, string fieldName, long entityId, string langCode)
|
||||
{
|
||||
return $"LangCache_{entityName}_{fieldName}_{entityId}_{langCode}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【单条翻译获取】
|
||||
/// 根据实体类型、字段、主键ID 和语言编码获取翻译内容。<br/>
|
||||
/// 适用于:小表(如菜单、字典),可设置较长缓存时间。<br/>
|
||||
/// <br/>
|
||||
/// 【示例】<br/>
|
||||
/// var content = await _sysLangTextCacheService.GetTranslation("Product", "Name", 123, "en-US");
|
||||
/// </summary>
|
||||
/// <param name="entityName">实体名称,如 "Product"</param>
|
||||
/// <param name="fieldName">字段名称,如 "Name"</param>
|
||||
/// <param name="entityId">实体主键ID</param>
|
||||
/// <param name="langCode">语言编码,如 "zh-CN"</param>
|
||||
/// <returns>翻译后的内容(若无则返回 null 或空)</returns>
|
||||
[NonAction]
|
||||
public async Task<string> GetTranslation(string entityName, string fieldName, long entityId, string langCode)
|
||||
{
|
||||
var key = BuildKey(entityName, fieldName, entityId, langCode);
|
||||
var value = _sysCacheService.Get<string>(key);
|
||||
if (!string.IsNullOrEmpty(value)) return value;
|
||||
|
||||
value = await _sysLangTextRep.AsQueryable()
|
||||
.Where(u => u.EntityName == entityName && u.FieldName == fieldName && u.EntityId == entityId && u.LangCode == langCode)
|
||||
.Select(u => u.Content)
|
||||
.FirstAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
_sysCacheService.Set(key, value, expireSeconds); // 设置过期
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据实体类型、字段、主键ID 和语言编码获取翻译实体
|
||||
/// </summary>
|
||||
/// <param name="entityName">实体名称</param>
|
||||
/// <param name="fieldName">字段名称</param>
|
||||
/// <param name="entityId">实体主键ID</param>
|
||||
/// <param name="langCode">语言编码</param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<SysLangText> GetTranslationEntity(string entityName, string fieldName, long entityId, string langCode)
|
||||
{
|
||||
var key = BuildKey(entityName, fieldName, entityId, langCode) + "_entity";
|
||||
var value = _sysCacheService.Get<SysLangText>(key);
|
||||
if (!value.IsNullOrEmpty()) return value;
|
||||
|
||||
value = await _sysLangTextRep.AsQueryable()
|
||||
.Where(u => u.EntityName == entityName && u.FieldName == fieldName && u.EntityId == entityId && u.LangCode == langCode)
|
||||
.FirstAsync();
|
||||
|
||||
if (!value.IsNullOrEmpty())
|
||||
{
|
||||
_sysCacheService.Set(key, value, expireSeconds); // 设置过期
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【批量翻译获取】<br/>
|
||||
/// 根据实体、字段和一批主键ID获取对应翻译内容,自动从缓存或数据库获取。<br/>
|
||||
/// 适用于:SKU、多商品、批量字典等需要高效批量获取的场景。<br/>
|
||||
///
|
||||
/// 【示例】<br/>
|
||||
/// var dict = await _sysLangTextCacheService.GetTranslations("SKU", "Name", skuIds, "en_US");
|
||||
/// </summary>
|
||||
/// <param name="entityName">实体名称</param>
|
||||
/// <param name="fieldName">字段名称</param>
|
||||
/// <param name="entityIds">主键ID集合</param>
|
||||
/// <param name="langCode">语言编码</param>
|
||||
/// <returns>主键ID到翻译内容的字典</returns>
|
||||
[NonAction]
|
||||
public async Task<Dictionary<long, string>> GetTranslations(string entityName, string fieldName, List<long> entityIds, string langCode)
|
||||
{
|
||||
var result = new Dictionary<long, string>();
|
||||
var missingIds = new HashSet<long>(); // 用 HashSet 提高后面 Contains 的性能
|
||||
|
||||
foreach (var id in entityIds.Distinct()) // 先去重,防止重复缓存 Key
|
||||
{
|
||||
var key = BuildKey(entityName, fieldName, id, langCode);
|
||||
var value = _sysCacheService.Get<string>(key);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
result[id] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
missingIds.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingIds.Any())
|
||||
{
|
||||
var list = await _sysLangTextRep.AsQueryable()
|
||||
.Where(u => u.EntityName == entityName &&
|
||||
u.FieldName == fieldName &&
|
||||
missingIds.Contains(u.EntityId) &&
|
||||
u.LangCode == langCode)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Content)) continue; // 跳过脏数据
|
||||
|
||||
var key = BuildKey(item.EntityName, item.FieldName, item.EntityId, item.LangCode);
|
||||
_sysCacheService.Set(key, item.Content, expireSeconds);
|
||||
|
||||
// 用 TryAdd 防止异常
|
||||
result[item.EntityId] = item.Content;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【列表翻译】<br/>
|
||||
/// 按配置把同一字段的翻译写回到实体列表中。内部会调用批量翻译接口。<br/>
|
||||
/// <br/>
|
||||
/// 【示例】<br/>
|
||||
/// await _sysLangTextCacheService.TranslateList(products, "Product", "Name", p => p.Id, (p, val) => p.Name = val, "zh-CN");
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="list">待翻译的实体列表</param>
|
||||
/// <param name="entityName">实体名称</param>
|
||||
/// <param name="fieldName">字段名称</param>
|
||||
/// <param name="idSelector">用于取出主键ID的表达式</param>
|
||||
/// <param name="setTranslatedValue">写回翻译值的委托</param>
|
||||
/// <param name="langCode">语言编码</param>
|
||||
/// <returns>翻译后的实体列表(引用传递)</returns>
|
||||
[NonAction]
|
||||
public async Task<List<TEntity>> TranslateList<TEntity>(List<TEntity> list, string entityName, string fieldName, Func<TEntity, long> idSelector, Action<TEntity, string> setTranslatedValue, string langCode)
|
||||
{
|
||||
var ids = list.Select(idSelector).Distinct().ToList();
|
||||
var dict = await GetTranslations(entityName, fieldName, ids, langCode);
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
var id = idSelector(item);
|
||||
if (dict.TryGetValue(id, out var value))
|
||||
{
|
||||
setTranslatedValue(item, value);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【多字段批量翻译】
|
||||
/// 对列表中的实体对象,按配置的字段映射进行多字段翻译处理。<br/>
|
||||
/// 常用于:菜单多语言、商品多语言、SKU多语言等需要多字段翻译的场景。<br/><br/>
|
||||
/// ✅ 特点:<br/>
|
||||
/// 1️⃣ 可同时翻译同一实体的多个字段(如 Name、Description、Title 等)<br/>
|
||||
/// 2️⃣ 内部先尝试从缓存读取,如缓存未命中则批量查询数据库,并自动写回缓存<br/>
|
||||
/// 3️⃣ 引用传递,直接对原实体对象赋值,无需额外返回<br/><br/>
|
||||
/// 【使用示例】:<br/>
|
||||
/// <code>
|
||||
/// var fields = new List<LangFieldMap<Product>>
|
||||
/// {
|
||||
/// new LangFieldMap<Product> {
|
||||
/// EntityName = "Product",
|
||||
/// FieldName = "Name",
|
||||
/// IdSelector = p => p.Id,
|
||||
/// SetTranslatedValue = (p, val) => p.Name = val
|
||||
/// },
|
||||
/// new LangFieldMap<Product> {
|
||||
/// EntityName = "Product",
|
||||
/// FieldName = "Description",
|
||||
/// IdSelector = p => p.Id,
|
||||
/// SetTranslatedValue = (p, val) => p.Description = val
|
||||
/// }
|
||||
/// };
|
||||
/// await _sysLangTextCacheService.TranslateMultiFields(products, fields, "zh-CN");
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">要翻译的实体类型,如 Product/Menu/SKU 等</typeparam>
|
||||
/// <param name="list">需要翻译的实体对象列表</param>
|
||||
/// <param name="fields">需要翻译的字段映射集合,支持多个字段</param>
|
||||
/// <param name="langCode">语言编码,如 "zh-CN"、"en-US"、"it-IT" 等</param>
|
||||
/// <returns>翻译后的实体列表(引用传递,原对象已直接赋值)</returns>
|
||||
[NonAction]
|
||||
public async Task<List<TEntity>> TranslateMultiFields<TEntity>(
|
||||
List<TEntity> list,
|
||||
List<LangFieldMap<TEntity>> fields,
|
||||
string langCode)
|
||||
{
|
||||
var keyToField = new Dictionary<string, (TEntity Entity, LangFieldMap<TEntity> FieldMap)>();
|
||||
var missingKeys = new List<string>();
|
||||
|
||||
// 先尝试从缓存读取
|
||||
foreach (var item in list)
|
||||
{
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var id = field.IdSelector(item);
|
||||
var key = BuildKey(field.EntityName, field.FieldName, id, langCode);
|
||||
var cached = _sysCacheService.Get<string>(key);
|
||||
if (!string.IsNullOrEmpty(cached))
|
||||
{
|
||||
// 命中缓存,直接赋值
|
||||
field.SetTranslatedValue(item, cached);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 缓存未命中,加入待查表
|
||||
keyToField[key] = (item, field);
|
||||
missingKeys.Add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingKeys.Any())
|
||||
{
|
||||
// 把缺失的 keys 拆解成组合实体
|
||||
var missingTuples = missingKeys
|
||||
.Select(key =>
|
||||
{
|
||||
var parts = key.Split('_');
|
||||
return new
|
||||
{
|
||||
EntityName = parts[1],
|
||||
FieldName = parts[2],
|
||||
EntityId = long.Parse(parts[3])
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 按 EntityName + FieldName 分组
|
||||
var grouped = missingTuples
|
||||
.GroupBy(x => new { x.EntityName, x.FieldName })
|
||||
.ToList();
|
||||
|
||||
var result = new List<SysLangText>();
|
||||
|
||||
// 分批查询,每组单独查询
|
||||
const int chunkSize = 500;
|
||||
foreach (var g in grouped)
|
||||
{
|
||||
var allIds = g.Select(x => x.EntityId).Distinct().ToList();
|
||||
for (int i = 0; i < allIds.Count; i += chunkSize)
|
||||
{
|
||||
var chunk = allIds.Skip(i).Take(chunkSize).ToList();
|
||||
var temp = await _sysLangTextRep.AsQueryable()
|
||||
.Where(u => u.LangCode == langCode
|
||||
&& u.EntityName == g.Key.EntityName
|
||||
&& u.FieldName == g.Key.FieldName
|
||||
&& chunk.Contains(u.EntityId))
|
||||
.ToListAsync();
|
||||
result.AddRange(temp);
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历查询结果,写回实体和缓存
|
||||
foreach (var item in result)
|
||||
{
|
||||
var key = BuildKey(item.EntityName, item.FieldName, item.EntityId, item.LangCode);
|
||||
if (keyToField.TryGetValue(key, out var tuple))
|
||||
{
|
||||
tuple.FieldMap.SetTranslatedValue(tuple.Entity, item.Content);
|
||||
_sysCacheService.Set(key, item.Content, expireSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除缓存
|
||||
/// </summary>
|
||||
/// <param name="entityName"></param>
|
||||
/// <param name="fieldName"></param>
|
||||
/// <param name="entityId"></param>
|
||||
/// <param name="langCode"></param>
|
||||
public void DeleteCache(string entityName, string fieldName, long entityId, string langCode)
|
||||
{
|
||||
var key = BuildKey(entityName, fieldName, entityId, langCode);
|
||||
_sysCacheService.Remove(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新缓存
|
||||
/// </summary>
|
||||
/// <param name="entityName"></param>
|
||||
/// <param name="fieldName"></param>
|
||||
/// <param name="entityId"></param>
|
||||
/// <param name="langCode"></param>
|
||||
/// <param name="newValue"></param>
|
||||
public void UpdateCache(string entityName, string fieldName, long entityId, string langCode, string newValue)
|
||||
{
|
||||
var key = BuildKey(entityName, fieldName, entityId, langCode);
|
||||
_sysCacheService.Set(key, newValue, expireSeconds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 翻译服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 100, Description = "翻译服务")]
|
||||
public partial class SysLangTextService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysLangText> _sysLangTextRep;
|
||||
private readonly ISqlSugarClient _sqlSugarClient;
|
||||
private readonly SysLangTextCacheService _sysLangTextCacheService;
|
||||
|
||||
public SysLangTextService(
|
||||
SqlSugarRepository<SysLangText> sysLangTextRep,
|
||||
SysLangTextCacheService sysLangTextCacheService,
|
||||
ISqlSugarClient sqlSugarClient)
|
||||
{
|
||||
_sysLangTextRep = sysLangTextRep;
|
||||
_sqlSugarClient = sqlSugarClient;
|
||||
_sysLangTextCacheService = sysLangTextCacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询翻译表 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("分页查询翻译表")]
|
||||
[ApiDescriptionSettings(Name = "Page"), HttpPost]
|
||||
public async Task<SqlSugarPagedList<SysLangTextOutput>> Page(PageSysLangTextInput input)
|
||||
{
|
||||
input.Keyword = input.Keyword?.Trim();
|
||||
var query = _sysLangTextRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.EntityName.Contains(input.Keyword) || u.FieldName.Contains(input.Keyword) || u.LangCode.Contains(input.Keyword) || u.Content.Contains(input.Keyword))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.EntityName), u => u.EntityName.Contains(input.EntityName.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.FieldName), u => u.FieldName.Contains(input.FieldName.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.LangCode), u => u.LangCode.Contains(input.LangCode.Trim()))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Content), u => u.Content.Contains(input.Content.Trim()))
|
||||
.WhereIF(input.EntityId != null, u => u.EntityId == input.EntityId)
|
||||
.Select<SysLangTextOutput>();
|
||||
return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
[DisplayName("获取翻译表")]
|
||||
[ApiDescriptionSettings(Name = "List"), HttpPost]
|
||||
public async Task<List<SysLangTextOutput>> List(ListSysLangTextInput input)
|
||||
{
|
||||
var query = _sysLangTextRep.AsQueryable()
|
||||
.Where(u => u.EntityName == input.EntityName.Trim() && u.FieldName == input.FieldName.Trim() && u.EntityId == input.EntityId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.LangCode), u => u.LangCode == input.LangCode.Trim())
|
||||
.Select<SysLangTextOutput>();
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取翻译表详情 ℹ️
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取翻译表详情")]
|
||||
[ApiDescriptionSettings(Name = "Detail"), HttpGet]
|
||||
public async Task<SysLangText> Detail([FromQuery] QueryByIdSysLangTextInput input)
|
||||
{
|
||||
return await _sysLangTextRep.GetFirstAsync(u => u.Id == input.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加翻译表 ➕
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("增加翻译表")]
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
public async Task<long> Add(AddSysLangTextInput input)
|
||||
{
|
||||
var entity = input.Adapt<SysLangText>();
|
||||
return await _sysLangTextRep.InsertAsync(entity) ? entity.Id : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新翻译表 ✏️
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("更新翻译表")]
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
public async Task Update(UpdateSysLangTextInput input)
|
||||
{
|
||||
var entity = input.Adapt<SysLangText>();
|
||||
await _sysLangTextRep.AsUpdateable(entity)
|
||||
.ExecuteCommandAsync();
|
||||
_sysLangTextCacheService.UpdateCache(entity.EntityName, entity.FieldName, entity.EntityId, entity.LangCode, entity.Content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除翻译表 ❌
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("删除翻译表")]
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
public async Task Delete(DeleteSysLangTextInput input)
|
||||
{
|
||||
var entity = await _sysLangTextRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
|
||||
await _sysLangTextRep.DeleteAsync(entity); //真删除
|
||||
_sysLangTextCacheService.DeleteCache(entity.EntityName, entity.FieldName, entity.EntityId, entity.LangCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量删除翻译表 ❌
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("批量删除翻译表")]
|
||||
[ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
|
||||
public async Task BatchDelete([Required(ErrorMessage = "主键列表不能为空")] List<DeleteSysLangTextInput> input)
|
||||
{
|
||||
var exp = Expressionable.Create<SysLangText>();
|
||||
foreach (var row in input) exp = exp.Or(it => it.Id == row.Id);
|
||||
var list = await _sysLangTextRep.AsQueryable().Where(exp.ToExpression()).ToListAsync();
|
||||
|
||||
await _sysLangTextRep.DeleteAsync(list); //真删除
|
||||
foreach (var item in list)
|
||||
{
|
||||
_sysLangTextCacheService.DeleteCache(item.EntityName, item.FieldName, item.EntityId, item.LangCode);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object _sysLangTextBatchSaveLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 批量保存翻译表 ✏️
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("批量保存翻译表")]
|
||||
[ApiDescriptionSettings(Name = "BatchSave"), HttpPost]
|
||||
public void BatchSave([Required(ErrorMessage = "列表不能为空")] List<ImportSysLangTextInput> input)
|
||||
{
|
||||
lock (_sysLangTextBatchSaveLock)
|
||||
{
|
||||
// 校验并过滤必填基本类型为null的字段
|
||||
var rows = input.Where(x =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(x.Error)) return false;
|
||||
if (x.EntityId == null)
|
||||
{
|
||||
x.Error = "所属实体ID不能为空";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).Adapt<List<SysLangText>>();
|
||||
|
||||
var storageable = _sysLangTextRep.Context.Storageable(rows)
|
||||
.SplitError(it => string.IsNullOrWhiteSpace(it.Item.EntityName), "所属实体名不能为空")
|
||||
.SplitError(it => it.Item.EntityName?.Length > 255, "所属实体名长度不能超过255个字符")
|
||||
.SplitError(it => string.IsNullOrWhiteSpace(it.Item.FieldName), "字段名不能为空")
|
||||
.SplitError(it => it.Item.FieldName?.Length > 255, "字段名长度不能超过255个字符")
|
||||
.SplitError(it => string.IsNullOrWhiteSpace(it.Item.LangCode), "语言代码不能为空")
|
||||
.SplitError(it => it.Item.LangCode?.Length > 255, "语言代码长度不能超过255个字符")
|
||||
.SplitError(it => string.IsNullOrWhiteSpace(it.Item.Content), "翻译内容不能为空")
|
||||
.WhereColumns(it => new { it.EntityId, it.EntityName, it.FieldName, it.LangCode })
|
||||
.SplitInsert(it => it.NotAny())
|
||||
.SplitUpdate(it => it.Any())
|
||||
.ToStorage();
|
||||
|
||||
storageable.AsInsertable.ExecuteCommand();// 不存在插入
|
||||
storageable.AsUpdateable.UpdateColumns(it => new
|
||||
{
|
||||
it.EntityName,
|
||||
it.EntityId,
|
||||
it.FieldName,
|
||||
it.LangCode,
|
||||
it.Content,
|
||||
}).ExecuteCommand();// 存在更新
|
||||
foreach (var item in rows)
|
||||
{
|
||||
_sysLangTextCacheService.DeleteCache(item.EntityName, item.FieldName, item.EntityId, item.LangCode);
|
||||
}
|
||||
if (storageable.ErrorList.Any())
|
||||
{
|
||||
throw Oops.Oh($"处理过程中出现以下错误:{string.Join(";", storageable.ErrorList.Distinct())}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出翻译表记录 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("导出翻译表记录")]
|
||||
[ApiDescriptionSettings(Name = "Export"), HttpPost, NonUnify]
|
||||
public async Task<IActionResult> Export(PageSysLangTextInput input)
|
||||
{
|
||||
var list = (await Page(input)).Items?.Adapt<List<ExportSysLangTextOutput>>() ?? new();
|
||||
if (input.SelectKeyList?.Count > 0) list = list.Where(x => input.SelectKeyList.Contains(x.Id)).ToList();
|
||||
return ExcelHelper.ExportTemplate(list, "翻译表导出记录");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载翻译表数据导入模板 ⬇️
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("下载翻译表数据导入模板")]
|
||||
[ApiDescriptionSettings(Name = "Import"), HttpGet, NonUnify]
|
||||
public IActionResult DownloadTemplate()
|
||||
{
|
||||
return ExcelHelper.ExportTemplate(new List<ExportSysLangTextOutput>(), "翻译表导入模板");
|
||||
}
|
||||
|
||||
private static readonly object _sysLangTextImportLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 导入翻译表记录 💾
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("导入翻译表记录")]
|
||||
[ApiDescriptionSettings(Name = "Import"), HttpPost, NonUnify, UnitOfWork]
|
||||
public IActionResult ImportData([Required] IFormFile file)
|
||||
{
|
||||
lock (_sysLangTextImportLock)
|
||||
{
|
||||
var stream = ExcelHelper.ImportData<ImportSysLangTextInput, SysLangText>(file, (list, markerErrorAction) =>
|
||||
{
|
||||
_sqlSugarClient.Utilities.PageEach(list, 2048, pageItems =>
|
||||
{
|
||||
// 校验并过滤必填基本类型为null的字段
|
||||
var rows = pageItems.Where(x =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(x.Error)) return false;
|
||||
if (x.EntityId == null)
|
||||
{
|
||||
x.Error = "所属实体ID不能为空";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).Adapt<List<SysLangText>>();
|
||||
|
||||
var storageable = _sysLangTextRep.Context.Storageable(rows)
|
||||
.SplitError(it => string.IsNullOrWhiteSpace(it.Item.EntityName), "所属实体名不能为空")
|
||||
.SplitError(it => it.Item.EntityName?.Length > 255, "所属实体名长度不能超过255个字符")
|
||||
.SplitError(it => string.IsNullOrWhiteSpace(it.Item.FieldName), "字段名不能为空")
|
||||
.SplitError(it => it.Item.FieldName?.Length > 255, "字段名长度不能超过255个字符")
|
||||
.SplitError(it => string.IsNullOrWhiteSpace(it.Item.LangCode), "语言代码不能为空")
|
||||
.SplitError(it => it.Item.LangCode?.Length > 255, "语言代码长度不能超过255个字符")
|
||||
.SplitError(it => string.IsNullOrWhiteSpace(it.Item.Content), "翻译内容不能为空")
|
||||
.SplitError(it => it.Item.Content?.Length > 255, "翻译内容长度不能超过255个字符")
|
||||
.WhereColumns(it => new { it.EntityId, it.EntityName, it.FieldName, it.LangCode })
|
||||
.SplitInsert(it => it.NotAny())
|
||||
.SplitUpdate(it => it.Any())
|
||||
.ToStorage();
|
||||
|
||||
storageable.AsInsertable.ExecuteCommand();// 不存在插入
|
||||
storageable.AsUpdateable.UpdateColumns(it => new
|
||||
{
|
||||
it.EntityName,
|
||||
it.EntityId,
|
||||
it.FieldName,
|
||||
it.LangCode,
|
||||
it.Content,
|
||||
}).ExecuteCommand();// 存在更新
|
||||
|
||||
foreach (var item in rows)
|
||||
{
|
||||
_sysLangTextCacheService.DeleteCache(item.EntityName, item.FieldName, item.EntityId, item.LangCode);
|
||||
}
|
||||
// 标记错误信息
|
||||
markerErrorAction.Invoke(storageable, pageItems, rows);
|
||||
});
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DEEPSEEK 翻译接口
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("DEEPSEEK 翻译接口")]
|
||||
[ApiDescriptionSettings(Name = "AiTranslateText"), HttpPost]
|
||||
public async Task<string> AiTranslateText(AiTranslateTextInput input)
|
||||
{
|
||||
// 需要先把DeepSeek.example复制改名为DeepSeek.json文件,添加你的 API KEY
|
||||
var deepSeekOptions = App.GetConfig<DeepSeekOptions>("DeepSeekSettings", true);
|
||||
if (deepSeekOptions == null)
|
||||
{
|
||||
throw new InvalidOperationException("DeepSeek.json文件 未定义");
|
||||
}
|
||||
if (string.IsNullOrEmpty(deepSeekOptions.ApiKey))
|
||||
{
|
||||
throw new InvalidOperationException("环境变量 DEEPSEEK_API_KEY 未定义");
|
||||
}
|
||||
|
||||
using (HttpClient client = new HttpClient())
|
||||
{
|
||||
// 构建请求头
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {deepSeekOptions.ApiKey}");
|
||||
|
||||
// 构建系统提示词
|
||||
string systemPrompt = BuildSystemPrompt(deepSeekOptions.SourceLang, input.TargetLang);
|
||||
|
||||
// 构建请求体
|
||||
var requestBody = new
|
||||
{
|
||||
model = "deepseek-chat",
|
||||
messages = new[]
|
||||
{
|
||||
new { role = "system", content = systemPrompt },
|
||||
new { role = "user", content = input.OriginalText }
|
||||
},
|
||||
temperature = 0.3,
|
||||
max_tokens = 2000
|
||||
};
|
||||
|
||||
// 使用 Newtonsoft.Json 序列化
|
||||
var json = JsonConvert.SerializeObject(requestBody);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
// 发送请求
|
||||
HttpResponseMessage response = await client.PostAsync(deepSeekOptions.ApiUrl, content);
|
||||
|
||||
// 处理响应
|
||||
string responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
// 使用 Newtonsoft.Json 反序列化错误响应
|
||||
var errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(responseBody);
|
||||
string errorMsg = errorResponse?.error?.message ?? $"HTTP {response.StatusCode}: {response.ReasonPhrase}";
|
||||
throw new HttpRequestException($"翻译API返回错误:{errorMsg}");
|
||||
}
|
||||
|
||||
// 解析有效响应
|
||||
var result = JsonConvert.DeserializeObject<TranslationResponse>(responseBody);
|
||||
|
||||
if (result?.choices == null || result.choices.Length == 0 ||
|
||||
result.choices[0]?.message?.content == null)
|
||||
{
|
||||
throw new InvalidOperationException("API返回无效的翻译结果");
|
||||
}
|
||||
|
||||
return result.choices[0].message.content.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
// JSON 响应模型
|
||||
private class TranslationResponse
|
||||
{
|
||||
public Choice[] choices { get; set; }
|
||||
}
|
||||
|
||||
private class Choice
|
||||
{
|
||||
public Message message { get; set; }
|
||||
}
|
||||
|
||||
private class Message
|
||||
{
|
||||
public string content { get; set; }
|
||||
}
|
||||
|
||||
private class ErrorResponse
|
||||
{
|
||||
public ErrorInfo error { get; set; }
|
||||
}
|
||||
|
||||
private class ErrorInfo
|
||||
{
|
||||
public string message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成提示词
|
||||
/// </summary>
|
||||
/// <param name="sourceLang"></param>
|
||||
/// <param name="targetLang"></param>
|
||||
/// <returns></returns>
|
||||
private static string BuildSystemPrompt(string sourceLang, string targetLang)
|
||||
{
|
||||
return $@"作为企业软件系统专业翻译,严格遵守以下铁律:
|
||||
|
||||
■ 核心原则
|
||||
1. 严格逐符号翻译({sourceLang}→{targetLang})
|
||||
2. 禁止添加/删除/改写任何内容
|
||||
3. 保持批量翻译的编号格式
|
||||
|
||||
■ 符号保留规则
|
||||
! 所有符号必须原样保留:
|
||||
• 编程符号:\${{ }} <% %> @ # & |
|
||||
• UI占位符:{{0}} %s [ ]
|
||||
• 货币单位:¥100.00 kg cm²
|
||||
• 中文符号:【 】 《 》 :
|
||||
|
||||
■ 中文符号位置规范
|
||||
# 三级处理机制:
|
||||
1. 成对符号必须保持完整结构:
|
||||
✓ 正确:【Warning】Text
|
||||
✗ 禁止:Warning【 】Text
|
||||
|
||||
2. 独立符号位置:
|
||||
• 优先句尾 → Text】?
|
||||
• 次选句首 → 】Text?
|
||||
• 禁止句中 → Text】Text?
|
||||
|
||||
3. 跨字符串符号处理:
|
||||
• 前段含【时 → 保留在段尾(""Synchronize【"")
|
||||
• 后段含】时 → 保留在段首(""】authorization data?"")
|
||||
• 符号后接字母时添加空格:】 Authorization
|
||||
|
||||
■ 语法规范
|
||||
• 外文 → 被动语态(""Item was created"")
|
||||
• 中文 → 主动语态(""已创建项目"")
|
||||
• 禁止推测上下文(只翻译当前字符串内容)
|
||||
|
||||
■ 错误预防(绝对禁止)
|
||||
✗ 将中文符号改为西式符号(】→])
|
||||
✗ 移动非中文符号位置
|
||||
✗ 添加原文不存在的内容
|
||||
✗ 合并/拆分原始字符串
|
||||
|
||||
■ 批量处理
|
||||
▸ 严格保持原始JSON结构
|
||||
▸ 语言键名精确匹配(zh-cn/en/it等)";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 导出日志数据
|
||||
/// </summary>
|
||||
[ExcelExporter(Name = "日志数据", TableStyle = OfficeOpenXml.Table.TableStyles.None, AutoFitAllColumn = true)]
|
||||
public class ExportLogDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录器类别名称
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "记录器类别名称", IsBold = true)]
|
||||
public string LogName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日志级别
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "日志级别", IsBold = true)]
|
||||
public string LogLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 事件Id
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "事件Id", IsBold = true)]
|
||||
public string EventId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日志消息
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "日志消息", IsBold = true)]
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 异常对象
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "异常对象", IsBold = true)]
|
||||
public string Exception { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前状态值
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "当前状态值", IsBold = true)]
|
||||
public string State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日志记录时间
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "日志记录时间", IsBold = true)]
|
||||
public DateTime LogDateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 线程Id
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "线程Id", IsBold = true)]
|
||||
public int ThreadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求跟踪Id
|
||||
/// </summary>
|
||||
[ExporterHeader(DisplayName = "请求跟踪Id", IsBold = true)]
|
||||
public string TraceId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class PageOpLogInput : PageVisLogInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 模块名称
|
||||
/// </summary>
|
||||
public string? ControllerName { get; set; }
|
||||
}
|
||||
|
||||
public class PageExLogInput : PageOpLogInput
|
||||
{
|
||||
}
|
||||
|
||||
public class PageVisLogInput : PageLogInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 方法名称
|
||||
///</summary>
|
||||
public string? ActionName { get; set; }
|
||||
}
|
||||
|
||||
public class PageLogInput : BasePageTimeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号
|
||||
/// </summary>
|
||||
public string? Account { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作用时
|
||||
/// </summary>
|
||||
public long? Elapsed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public string Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IP地址
|
||||
/// </summary>
|
||||
public string? RemoteIp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
public long TenantId { get; set; }
|
||||
}
|
||||
|
||||
public class LogInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class LogVisOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录地点
|
||||
/// </summary>
|
||||
public string Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 经度
|
||||
/// </summary>
|
||||
public double? Longitude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 维度
|
||||
/// </summary>
|
||||
public double? Latitude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 真实姓名
|
||||
/// </summary>
|
||||
public string RealName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日志时间
|
||||
/// </summary>
|
||||
public DateTime? LogDateTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统差异日志服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 330)]
|
||||
public class SysLogDiffService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysLogDiff> _sysLogDiffRep;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public SysLogDiffService(UserManager userManager, SqlSugarRepository<SysLogDiff> sysLogDiffRep)
|
||||
{
|
||||
_sysLogDiffRep = sysLogDiffRep;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取差异日志分页列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[DisplayName("获取差异日志分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysLogDiff>> Page(PageLogInput input)
|
||||
{
|
||||
return await _sysLogDiffRep.AsQueryable()
|
||||
.WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime)
|
||||
.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取差异日志详情 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[DisplayName("获取差异日志详情")]
|
||||
public async Task<SysLogDiff> GetDetail(long id)
|
||||
{
|
||||
return await _sysLogDiffRep.GetFirstAsync(u => u.Id == id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统异常日志服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 350)]
|
||||
public class SysLogExService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysLogEx> _sysLogExRep;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public SysLogExService(UserManager userManager, SqlSugarRepository<SysLogEx> sysLogExRep)
|
||||
{
|
||||
_sysLogExRep = sysLogExRep;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取异常日志分页列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[DisplayName("获取异常日志分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysLogEx>> Page(PageExLogInput input)
|
||||
{
|
||||
return await _sysLogExRep.AsQueryable()
|
||||
.WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account == input.Account)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.ControllerName), u => u.ControllerName == input.ControllerName)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.ActionName), u => u.ActionName == input.ActionName)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.RemoteIp), u => u.RemoteIp == input.RemoteIp)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Elapsed.ToString()), u => u.Elapsed >= input.Elapsed)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status == "200", u => u.Status == "200")
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status != "200", u => u.Status != "200")
|
||||
//.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.IgnoreColumns(u => new { u.RequestParam, u.ReturnResult, u.Message })
|
||||
.OrderBuilder(input)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取异常日志详情 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[DisplayName("获取异常日志详情")]
|
||||
public async Task<SysLogEx> GetDetail(long id)
|
||||
{
|
||||
return await _sysLogExRep.GetFirstAsync(u => u.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空异常日志 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Clear"), HttpPost]
|
||||
[DisplayName("清空异常日志")]
|
||||
public void Clear()
|
||||
{
|
||||
_sysLogExRep.AsSugarClient().DbMaintenance.TruncateTable<SysLogEx>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出异常日志 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Export"), NonUnify]
|
||||
[DisplayName("导出异常日志")]
|
||||
public async Task<IActionResult> ExportLogEx(LogInput input)
|
||||
{
|
||||
var logExList = await _sysLogExRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()),
|
||||
u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime)
|
||||
.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.Select<ExportLogDto>().ToListAsync();
|
||||
|
||||
IExcelExporter excelExporter = new ExcelExporter();
|
||||
var res = await excelExporter.ExportAsByteArray(logExList);
|
||||
return new FileStreamResult(new MemoryStream(res), "application/octet-stream") { FileDownloadName = DateTime.Now.ToString("yyyyMMddHHmm") + "异常日志.xlsx" };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统操作日志服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 360)]
|
||||
public class SysLogOpService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysLogOp> _sysLogOpRep;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public SysLogOpService(UserManager userManager, SqlSugarRepository<SysLogOp> sysLogOpRep)
|
||||
{
|
||||
_sysLogOpRep = sysLogOpRep;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取操作日志分页列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[DisplayName("获取操作日志分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysLogOp>> Page(PageOpLogInput input)
|
||||
{
|
||||
return await _sysLogOpRep.AsQueryable()
|
||||
.WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account == input.Account)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.RemoteIp), u => u.RemoteIp == input.RemoteIp)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.ControllerName), u => u.ControllerName == input.ControllerName)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.ActionName), u => u.ActionName == input.ActionName)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Elapsed.ToString()), u => u.Elapsed >= input.Elapsed)
|
||||
//.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.IgnoreColumns(u => new { u.RequestParam, u.ReturnResult, u.Message })
|
||||
.OrderBuilder(input)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取操作日志详情 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[DisplayName("获取操作日志详情")]
|
||||
public async Task<SysLogOp> GetDetail(long id)
|
||||
{
|
||||
return await _sysLogOpRep.GetFirstAsync(u => u.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空操作日志 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Clear"), HttpPost]
|
||||
[DisplayName("清空操作日志")]
|
||||
public void Clear()
|
||||
{
|
||||
_sysLogOpRep.AsSugarClient().DbMaintenance.TruncateTable<SysLogOp>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出操作日志 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Export"), NonUnify]
|
||||
[DisplayName("导出操作日志")]
|
||||
public async Task<IActionResult> ExportLogOp(LogInput input)
|
||||
{
|
||||
var logOpList = await _sysLogOpRep.AsQueryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()),
|
||||
u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime)
|
||||
.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.Select<ExportLogDto>().ToListAsync();
|
||||
|
||||
IExcelExporter excelExporter = new ExcelExporter();
|
||||
var res = await excelExporter.ExportAsByteArray(logOpList);
|
||||
return new FileStreamResult(new MemoryStream(res), "application/octet-stream") { FileDownloadName = DateTime.Now.ToString("yyyyMMddHHmm") + "操作日志.xlsx" };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统访问日志服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 340)]
|
||||
public class SysLogVisService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysLogVis> _sysLogVisRep;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public SysLogVisService(UserManager userManager, SqlSugarRepository<SysLogVis> sysLogVisRep)
|
||||
{
|
||||
_sysLogVisRep = sysLogVisRep;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取访问日志分页列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SuppressMonitor]
|
||||
[DisplayName("获取访问日志分页列表")]
|
||||
public async Task<SqlSugarPagedList<SysLogVis>> Page(PageVisLogInput input)
|
||||
{
|
||||
return await _sysLogVisRep.AsQueryable()
|
||||
.WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account == input.Account)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.ActionName), u => u.ActionName == input.ActionName)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.RemoteIp), u => u.RemoteIp == input.RemoteIp)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Elapsed.ToString()), u => u.Elapsed >= input.Elapsed)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status == "200", u => u.Status == "200")
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status != "200", u => u.Status != "200")
|
||||
.OrderBy(u => u.CreateTime, OrderByType.Desc)
|
||||
.ToPagedListAsync(input.Page, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空访问日志 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Clear"), HttpPost]
|
||||
[DisplayName("清空访问日志")]
|
||||
public void Clear()
|
||||
{
|
||||
_sysLogVisRep.AsSugarClient().DbMaintenance.TruncateTable<SysLogVis>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取访问日志列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取访问日志列表")]
|
||||
public async Task<List<LogVisOutput>> GetList()
|
||||
{
|
||||
return await _sysLogVisRep.AsQueryable()
|
||||
.Where(u => u.Longitude > 0 && u.Longitude > 0)
|
||||
.Select(u => new LogVisOutput
|
||||
{
|
||||
Location = u.Location,
|
||||
Longitude = (double?)u.Longitude,
|
||||
Latitude = (double?)u.Latitude,
|
||||
RealName = u.RealName,
|
||||
LogDateTime = u.LogDateTime
|
||||
}).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
public class MenuInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单类型(1目录 2菜单 3按钮)
|
||||
/// </summary>
|
||||
public MenuTypeEnum? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
public virtual long TenantId { get; set; }
|
||||
}
|
||||
|
||||
public class AddMenuInput : SysMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "菜单名称不能为空")]
|
||||
public override string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
public long TenantId { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateMenuInput : AddMenuInput
|
||||
{
|
||||
}
|
||||
|
||||
public class DeleteMenuInput : BaseIdInput
|
||||
{
|
||||
}
|
||||
|
||||
public class MenuStatusInput : BaseStatusInput
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统菜单返回结果
|
||||
/// </summary>
|
||||
public class MenuOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父Id
|
||||
/// </summary>
|
||||
public long Pid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单类型(0目录 1菜单 2按钮)
|
||||
/// </summary>
|
||||
public MenuTypeEnum Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 路由地址
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 组件路径
|
||||
/// </summary>
|
||||
public string Component { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 权限标识
|
||||
/// </summary>
|
||||
public string Permission { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 重定向
|
||||
/// </summary>
|
||||
public string Redirect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
public int OrderNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public StatusEnum Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public virtual DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public virtual DateTime UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者姓名
|
||||
/// </summary>
|
||||
public virtual string CreateUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者姓名
|
||||
/// </summary>
|
||||
public virtual string UpdateUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单Meta
|
||||
/// </summary>
|
||||
public SysMenuMeta Meta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单子项
|
||||
/// </summary>
|
||||
public List<MenuOutput> Children { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 菜单Meta配置
|
||||
/// </summary>
|
||||
public class SysMenuMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
public string Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否内嵌
|
||||
/// </summary>
|
||||
public bool IsIframe { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外链链接
|
||||
/// </summary>
|
||||
public string IsLink { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否隐藏
|
||||
/// </summary>
|
||||
public bool IsHide { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否缓存
|
||||
/// </summary>
|
||||
public bool IsKeepAlive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否固定
|
||||
/// </summary>
|
||||
public bool IsAffix { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置菜单对象映射
|
||||
/// </summary>
|
||||
public class SysMenuMapper : IRegister
|
||||
{
|
||||
public void Register(TypeAdapterConfig config)
|
||||
{
|
||||
config.ForType<SysMenu, MenuOutput>()
|
||||
.Map(t => t.Meta.Title, o => o.Title)
|
||||
.Map(t => t.Meta.Icon, o => o.Icon)
|
||||
.Map(t => t.Meta.IsIframe, o => o.IsIframe)
|
||||
.Map(t => t.Meta.IsLink, o => o.OutLink)
|
||||
.Map(t => t.Meta.IsHide, o => o.IsHide)
|
||||
.Map(t => t.Meta.IsKeepAlive, o => o.IsKeepAlive)
|
||||
.Map(t => t.Meta.IsAffix, o => o.IsAffix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 系统菜单服务 🧩
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings(Order = 450)]
|
||||
public class SysMenuService : IDynamicApiController, ITransient
|
||||
{
|
||||
private readonly SqlSugarRepository<SysTenantMenu> _sysTenantMenuRep;
|
||||
private readonly SqlSugarRepository<SysMenu> _sysMenuRep;
|
||||
private readonly SysRoleMenuService _sysRoleMenuService;
|
||||
private readonly SysUserRoleService _sysUserRoleService;
|
||||
private readonly SysUserMenuService _sysUserMenuService;
|
||||
private readonly SysCacheService _sysCacheService;
|
||||
private readonly UserManager _userManager;
|
||||
private readonly SysLangTextCacheService _sysLangTextCacheService;
|
||||
private readonly SysLangTextService _sysLangTextService;
|
||||
|
||||
public SysMenuService(
|
||||
SqlSugarRepository<SysTenantMenu> sysTenantMenuRep,
|
||||
SqlSugarRepository<SysMenu> sysMenuRep,
|
||||
SysRoleMenuService sysRoleMenuService,
|
||||
SysUserRoleService sysUserRoleService,
|
||||
SysUserMenuService sysUserMenuService,
|
||||
SysCacheService sysCacheService,
|
||||
UserManager userManager,
|
||||
SysLangTextCacheService sysLangTextCacheService,
|
||||
SysLangTextService sysLangTextService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sysMenuRep = sysMenuRep;
|
||||
_sysRoleMenuService = sysRoleMenuService;
|
||||
_sysUserRoleService = sysUserRoleService;
|
||||
_sysUserMenuService = sysUserMenuService;
|
||||
_sysTenantMenuRep = sysTenantMenuRep;
|
||||
_sysCacheService = sysCacheService;
|
||||
_sysLangTextCacheService = sysLangTextCacheService;
|
||||
_sysLangTextService = sysLangTextService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取登录菜单树 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取登录菜单树")]
|
||||
public async Task<List<MenuOutput>> GetLoginMenuTree()
|
||||
{
|
||||
var sysDefaultLang = App.GetOptions<LocalizationSettingsOptions>().DefaultCulture;
|
||||
var langCode = _userManager.LangCode;
|
||||
var (query, _) = GetSugarQueryableAndTenantId(_userManager.TenantId);
|
||||
|
||||
// 查询菜单主表(过滤非按钮和禁用)
|
||||
var menuQuery = query.Where(u => u.Type != MenuTypeEnum.Btn && u.Status == StatusEnum.Enable);
|
||||
|
||||
if (!(_userManager.SuperAdmin || _userManager.SysAdmin))
|
||||
{
|
||||
var menuIdList = await GetMenuIdList();
|
||||
menuQuery = menuQuery.Where(u => menuIdList.Contains(u.Id));
|
||||
}
|
||||
|
||||
// 查询主表(不再 LEFT JOIN)
|
||||
var menuList = await menuQuery
|
||||
.OrderBy(u => new { u.OrderNo, u.Id })
|
||||
.ToListAsync();
|
||||
|
||||
// 仅当用户语言和系统默认语言不同时,才进行翻译,避免不必要的性能开销
|
||||
if (langCode != sysDefaultLang)
|
||||
{
|
||||
// 调用缓存翻译:翻译 Title 字段
|
||||
var fields = new List<LangFieldMap<SysMenu>>
|
||||
{
|
||||
new LangFieldMap<SysMenu>
|
||||
{
|
||||
EntityName = "SysMenu",
|
||||
FieldName = "Title",
|
||||
IdSelector = m => m.Id,
|
||||
SetTranslatedValue = (m, val) => m.Title = val
|
||||
}
|
||||
};
|
||||
await _sysLangTextCacheService.TranslateMultiFields(menuList, fields, langCode);
|
||||
}
|
||||
|
||||
// 构造树
|
||||
var menuTree = menuList.ToTree(
|
||||
it => it.Children, it => it.Pid, 0
|
||||
);
|
||||
|
||||
// 转换为输出 DTO
|
||||
return menuTree.Adapt<List<MenuOutput>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取菜单列表 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取菜单列表")]
|
||||
public async Task<List<SysMenu>> GetList([FromQuery] MenuInput input)
|
||||
{
|
||||
var langCode = _userManager.LangCode;
|
||||
var menuIdList = _userManager.SuperAdmin || _userManager.SysAdmin ? new List<long>() : await GetMenuIdList();
|
||||
var (query, _) = GetSugarQueryableAndTenantId(input.TenantId);
|
||||
|
||||
// 有条件直接查询菜单列表(带 Title、Type 过滤)
|
||||
if (!string.IsNullOrWhiteSpace(input.Title) || input.Type is > 0)
|
||||
{
|
||||
var menuList = await query
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Title), u => u.Title.Contains(input.Title))
|
||||
.WhereIF(input.Type is > 0, u => u.Type == input.Type)
|
||||
.WhereIF(menuIdList.Count > 0, u => menuIdList.Contains(u.Id))
|
||||
.OrderBy(u => new { u.OrderNo, u.Id })
|
||||
.ToListAsync();
|
||||
|
||||
// 走缓存批量翻译
|
||||
var fields = new List<LangFieldMap<SysMenu>>
|
||||
{
|
||||
new LangFieldMap<SysMenu>
|
||||
{
|
||||
EntityName = "SysMenu",
|
||||
FieldName = "Title",
|
||||
IdSelector = m => m.Id,
|
||||
SetTranslatedValue = (m, val) => m.Title = val
|
||||
}
|
||||
};
|
||||
await _sysLangTextCacheService.TranslateMultiFields(menuList, fields, langCode);
|
||||
|
||||
return menuList.Distinct().ToList();
|
||||
}
|
||||
|
||||
// 无筛选条件则走全量树形结构(带权限)
|
||||
if (!(_userManager.SuperAdmin || _userManager.SysAdmin))
|
||||
{
|
||||
query = query.Where(u => menuIdList.Contains(u.Id));
|
||||
}
|
||||
|
||||
var menuFullList = await query
|
||||
.OrderBy(u => new { u.OrderNo, u.Id })
|
||||
.ToListAsync();
|
||||
|
||||
// 走缓存批量翻译
|
||||
var treeFields = new List<LangFieldMap<SysMenu>>
|
||||
{
|
||||
new LangFieldMap<SysMenu>
|
||||
{
|
||||
EntityName = "SysMenu",
|
||||
FieldName = "Title",
|
||||
IdSelector = m => m.Id,
|
||||
SetTranslatedValue = (m, val) => m.Title = val
|
||||
}
|
||||
};
|
||||
await _sysLangTextCacheService.TranslateMultiFields(menuFullList, treeFields, langCode);
|
||||
|
||||
// 组装树
|
||||
var menuTree = menuFullList.ToTree(it => it.Children, it => it.Pid, 0);
|
||||
return menuTree.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加菜单 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Add"), HttpPost]
|
||||
[DisplayName("增加菜单")]
|
||||
public async Task<long> AddMenu(AddMenuInput input)
|
||||
{
|
||||
var (query, tenantId) = GetSugarQueryableAndTenantId(input.TenantId);
|
||||
|
||||
var isExist = input.Type != MenuTypeEnum.Btn
|
||||
? await query.AnyAsync(u => u.Title == input.Title && u.Pid == input.Pid)
|
||||
: await query.AnyAsync(u => u.Permission == input.Permission);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D4000);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.Name) && await query.AnyAsync(u => u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D4009);
|
||||
|
||||
if (input.Pid != 0 && await query.AnyAsync(u => u.Id == input.Pid && u.Type == MenuTypeEnum.Btn)) throw Oops.Oh(ErrorCodeEnum.D4010);
|
||||
|
||||
// 校验菜单参数
|
||||
var sysMenu = input.Adapt<SysMenu>();
|
||||
CheckMenuParam(sysMenu);
|
||||
|
||||
// 保存租户菜单权限
|
||||
await _sysMenuRep.InsertAsync(sysMenu);
|
||||
await _sysTenantMenuRep.InsertAsync(new SysTenantMenu { TenantId = tenantId, MenuId = sysMenu.Id });
|
||||
|
||||
// 清除缓存
|
||||
DeleteMenuCache();
|
||||
|
||||
return sysMenu.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新菜单 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[ApiDescriptionSettings(Name = "Update"), HttpPost]
|
||||
[DisplayName("更新菜单")]
|
||||
public async Task UpdateMenu(UpdateMenuInput input)
|
||||
{
|
||||
if (!_userManager.SuperAdmin && new SysMenuSeedData().HasData().Any(u => u.Id == input.Id)) throw Oops.Oh(ErrorCodeEnum.D4012);
|
||||
|
||||
if (input.Id == input.Pid) throw Oops.Oh(ErrorCodeEnum.D4008);
|
||||
var (query, _) = GetSugarQueryableAndTenantId(input.TenantId);
|
||||
|
||||
var isExist = input.Type != MenuTypeEnum.Btn
|
||||
? await query.AnyAsync(u => u.Title == input.Title && u.Type == input.Type && u.Pid == input.Pid && u.Id != input.Id)
|
||||
: await query.AnyAsync(u => u.Permission == input.Permission && u.Id != input.Id);
|
||||
if (isExist) throw Oops.Oh(ErrorCodeEnum.D4000);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.Name) && await query.AnyAsync(u => u.Id != input.Id && u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D4009);
|
||||
|
||||
if (input.Pid != 0 && await query.AnyAsync(u => u.Id == input.Pid && u.Type == MenuTypeEnum.Btn)) throw Oops.Oh(ErrorCodeEnum.D4010);
|
||||
|
||||
// 校验菜单参数
|
||||
var sysMenu = input.Adapt<SysMenu>();
|
||||
CheckMenuParam(sysMenu);
|
||||
|
||||
await _sysMenuRep.AsTenant().UseTranAsync(async () =>
|
||||
{
|
||||
// 更新菜单
|
||||
await _sysMenuRep.AsUpdateable(sysMenu).ExecuteCommandAsync();
|
||||
|
||||
// 同步更新翻译表
|
||||
var menuTranslation = await _sysLangTextCacheService.GetTranslationEntity("SysMenu", "Title", sysMenu.Id, _userManager.LangCode);
|
||||
if (!menuTranslation.IsNullOrEmpty())
|
||||
{
|
||||
await _sysLangTextService.Update(new UpdateSysLangTextInput
|
||||
{
|
||||
Id = menuTranslation.Id,
|
||||
EntityName = "SysMenu",
|
||||
EntityId = sysMenu.Id,
|
||||
FieldName = "Title",
|
||||
LangCode = _userManager.LangCode,
|
||||
Content = sysMenu.Title
|
||||
});
|
||||
}
|
||||
}, err =>
|
||||
{
|
||||
Oops.Oh("更新数据时发生错误", err.Message);
|
||||
});
|
||||
|
||||
// 清除缓存
|
||||
DeleteMenuCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除菜单 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
|
||||
[DisplayName("删除菜单")]
|
||||
public async Task DeleteMenu(DeleteMenuInput input)
|
||||
{
|
||||
if (!_userManager.SuperAdmin && new SysMenuSeedData().HasData().Any(u => u.Id == input.Id)) throw Oops.Oh(ErrorCodeEnum.D4013);
|
||||
|
||||
var menuTreeList = await _sysMenuRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id);
|
||||
var menuIdList = menuTreeList.Select(u => u.Id).ToList();
|
||||
|
||||
await _sysMenuRep.DeleteAsync(u => menuIdList.Contains(u.Id));
|
||||
|
||||
// 级联删除租户菜单数据
|
||||
await _sysTenantMenuRep.AsDeleteable().Where(u => menuIdList.Contains(u.MenuId)).ExecuteCommandAsync();
|
||||
|
||||
// 级联删除角色菜单数据
|
||||
await _sysRoleMenuService.DeleteRoleMenuByMenuIdList(menuIdList);
|
||||
|
||||
// 级联删除用户收藏菜单
|
||||
await _sysUserMenuService.DeleteMenuList(menuIdList);
|
||||
|
||||
// 清除缓存
|
||||
DeleteMenuCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置菜单状态 🔖
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[UnitOfWork]
|
||||
[DisplayName("设置菜单状态")]
|
||||
public virtual async Task<int> SetStatus(MenuStatusInput input)
|
||||
{
|
||||
if (_userManager.UserId == input.Id)
|
||||
throw Oops.Oh(ErrorCodeEnum.D1026);
|
||||
|
||||
var menu = await _sysMenuRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
|
||||
menu.Status = input.Status;
|
||||
var rows = await _sysMenuRep.AsUpdateable(menu).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync();
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加和编辑时检查菜单数据
|
||||
/// </summary>
|
||||
/// <param name="menu"></param>
|
||||
private static void CheckMenuParam(SysMenu menu)
|
||||
{
|
||||
var permission = menu.Permission;
|
||||
if (menu.Type == MenuTypeEnum.Btn)
|
||||
{
|
||||
menu.Name = null;
|
||||
menu.Path = null;
|
||||
menu.Component = null;
|
||||
menu.Icon = null;
|
||||
menu.Redirect = null;
|
||||
menu.OutLink = null;
|
||||
menu.IsHide = false;
|
||||
menu.IsKeepAlive = true;
|
||||
menu.IsAffix = false;
|
||||
menu.IsIframe = false;
|
||||
|
||||
if (string.IsNullOrEmpty(permission)) throw Oops.Oh(ErrorCodeEnum.D4003);
|
||||
if (!permission.Contains(':')) throw Oops.Oh(ErrorCodeEnum.D4004);
|
||||
}
|
||||
else
|
||||
{
|
||||
menu.Permission = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户拥有按钮权限集合(缓存) 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取按钮权限集合")]
|
||||
public async Task<List<string>> GetOwnBtnPermList()
|
||||
{
|
||||
var userId = _userManager.UserId;
|
||||
var permissions = _sysCacheService.Get<List<string>>(CacheConst.KeyUserButton + userId);
|
||||
if (permissions != null) return permissions;
|
||||
|
||||
var menuIdList = _userManager.SuperAdmin || _userManager.SysAdmin ? new() : await GetMenuIdList();
|
||||
if (menuIdList.Count <= 0 && !_userManager.SuperAdmin)
|
||||
{
|
||||
//_sysCacheService.Set(CacheConst.KeyUserButton + userId, new List<string>(), TimeSpan.FromDays(7));
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
permissions = await _sysMenuRep.AsQueryable()
|
||||
.InnerJoinIF<SysTenantMenu>(!_userManager.SuperAdmin, (u, t) => t.TenantId == _userManager.TenantId && u.Id == t.MenuId)
|
||||
.Where(u => u.Type == MenuTypeEnum.Btn)
|
||||
.WhereIF(menuIdList.Count > 0, u => menuIdList.Contains(u.Id))
|
||||
.Select(u => u.Permission).ToListAsync();
|
||||
|
||||
_sysCacheService.Set(CacheConst.KeyUserButton + userId, permissions, TimeSpan.FromDays(7));
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统所有按钮权限集合(缓存)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<string>> GetAllBtnPermList()
|
||||
{
|
||||
var permissions = _sysCacheService.Get<List<string>>(CacheConst.KeyUserButton + 0);
|
||||
if (permissions != null && permissions.Count != 0) return permissions;
|
||||
|
||||
permissions = await _sysMenuRep.AsQueryable()
|
||||
.Where(u => u.Type == MenuTypeEnum.Btn)
|
||||
.Select(u => u.Permission).ToListAsync();
|
||||
_sysCacheService.Set(CacheConst.KeyUserButton + 0, permissions);
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据租户id获取构建菜单联表查询实例
|
||||
/// </summary>
|
||||
/// <param name="tenantId"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public (ISugarQueryable<SysMenu, SysTenantMenu> query, long tenantId) GetSugarQueryableAndTenantId(long tenantId)
|
||||
{
|
||||
if (!_userManager.SuperAdmin) tenantId = _userManager.TenantId;
|
||||
|
||||
// 超管用户菜单范围:种子菜单 + 租户id菜单
|
||||
ISugarQueryable<SysMenu, SysTenantMenu> query;
|
||||
if (_userManager.SuperAdmin)
|
||||
{
|
||||
if (tenantId <= 0)
|
||||
{
|
||||
query = _sysMenuRep.AsQueryable().InnerJoinIF<SysTenantMenu>(false, (u, t) => true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 指定租户的菜单
|
||||
var menuIds = _sysTenantMenuRep.AsQueryable().Where(u => u.TenantId == tenantId).ToList(u => u.MenuId) ?? new();
|
||||
|
||||
// 种子菜单
|
||||
//menuIds.AddRange(new SysMenuSeedData().HasData().Select(u => u.Id).ToList());
|
||||
|
||||
menuIds = menuIds.Distinct().ToList();
|
||||
query = _sysMenuRep.AsQueryable().InnerJoinIF<SysTenantMenu>(false, (u, t) => true).Where(u => menuIds.Contains(u.Id));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
query = _sysMenuRep.AsQueryable().InnerJoinIF<SysTenantMenu>(tenantId > 0, (u, t) => t.TenantId == tenantId && u.Id == t.MenuId);
|
||||
}
|
||||
|
||||
return (query, tenantId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除菜单和按钮缓存
|
||||
/// </summary>
|
||||
[NonAction]
|
||||
public void DeleteMenuCache()
|
||||
{
|
||||
// _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserMenu);
|
||||
_sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserButton);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户菜单Id集合
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<long>> GetMenuIdList()
|
||||
{
|
||||
var roleIdList = await _sysUserRoleService.GetUserRoleIdList(_userManager.UserId);
|
||||
return await _sysRoleMenuService.GetRoleMenuIdList(roleIdList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 排除前端存在全选的父级菜单
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
public async Task<List<long>> ExcludeParentMenuOfFullySelected(List<long> menuIds)
|
||||
{
|
||||
// 获取当前用户菜单
|
||||
var (query, _) = GetSugarQueryableAndTenantId(0);
|
||||
var menuList = await query.ToListAsync();
|
||||
|
||||
// 排除列表,防止前端全选问题
|
||||
var exceptList = new List<long>();
|
||||
foreach (var id in menuIds)
|
||||
{
|
||||
// 排除按钮菜单
|
||||
if (menuList.Any(u => u.Id == id && u.Type == MenuTypeEnum.Btn)) continue;
|
||||
|
||||
// 如果没有子集或有全部子集权限
|
||||
var children = menuList.ToChildList(u => u.Id, u => u.Pid, id, false).ToList();
|
||||
if (children.Count == 0 || children.All(u => menuIds.Contains(u.Id))) continue;
|
||||
|
||||
// 排除没有全部子集权限的菜单
|
||||
exceptList.Add(id);
|
||||
}
|
||||
return menuIds.Except(exceptList).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
public class MessageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 接收者用户Id
|
||||
/// </summary>
|
||||
public long ReceiveUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收者名称
|
||||
/// </summary>
|
||||
public string ReceiveUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID列表
|
||||
/// </summary>
|
||||
public List<long> UserIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息标题
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息类型
|
||||
/// </summary>
|
||||
public MessageTypeEnum MessageType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息内容
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送者Id
|
||||
/// </summary>
|
||||
public string SendUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送者名称
|
||||
/// </summary>
|
||||
public string SendUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送时间
|
||||
/// </summary>
|
||||
public DateTime SendTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
namespace Admin.NET.Core;
|
||||
|
||||
public class SmsVerifyCodeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 手机号码
|
||||
/// </summary>
|
||||
/// <example>admin</example>
|
||||
[Required(ErrorMessage = "手机号码不能为空")]
|
||||
[DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")]
|
||||
public string Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 验证码
|
||||
/// </summary>
|
||||
/// <example>123456</example>
|
||||
[Required(ErrorMessage = "验证码不能为空"), MinLength(4, ErrorMessage = "验证码不能少于4个字符")]
|
||||
public string Code { get; set; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user