feat: initialize Evergreen Health backend

This commit is contained in:
fuenmao
2026-09-21 14:08:22 +08:00
commit 8553df5dd2
493 changed files with 75649 additions and 0 deletions
@@ -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;
}
}
+116
View File
@@ -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,
};
}
}
+90
View File
@@ -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);
}
}
+45
View File
@@ -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);
}
}
}
+398
View File
@@ -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 =&gt; p.Id, (p, val) =&gt; 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&lt;LangFieldMap&lt;Product&gt;&gt;
/// {
/// new LangFieldMap&lt;Product&gt; {
/// EntityName = "Product",
/// FieldName = "Name",
/// IdSelector = p =&gt; p.Id,
/// SetTranslatedValue = (p, val) =&gt; p.Name = val
/// },
/// new LangFieldMap&lt;Product&gt; {
/// EntityName = "Product",
/// FieldName = "Description",
/// IdSelector = p =&gt; p.Id,
/// SetTranslatedValue = (p, val) =&gt; 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