commit 8553df5dd210175506b8722f51b50e449bee4dfd Author: fuenmao <980740792@qq.com> Date: Mon Sep 21 14:08:22 2026 +0800 feat: initialize Evergreen Health backend diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..989c702 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,177 @@ +[*.cs] +#### 命名样式 #### + +# 命名规则 + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# 符号规范 + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# 命名样式 + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +csharp_using_directive_placement = outside_namespace:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = false:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_prefer_static_local_function = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_space_around_binary_operators = before_and_after +csharp_indent_labels = one_less_than_current + +[*.vb] +#### 命名样式 #### + +# 命名规则 + +dotnet_naming_rule.interface_should_be_以_i_开始.severity = suggestion +dotnet_naming_rule.interface_should_be_以_i_开始.symbols = interface +dotnet_naming_rule.interface_should_be_以_i_开始.style = 以_i_开始 + +dotnet_naming_rule.类型_should_be_帕斯卡拼写法.severity = suggestion +dotnet_naming_rule.类型_should_be_帕斯卡拼写法.symbols = 类型 +dotnet_naming_rule.类型_should_be_帕斯卡拼写法.style = 帕斯卡拼写法 + +dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.severity = suggestion +dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.symbols = 非字段成员 +dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯卡拼写法 + +# 符号规范 + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.类型.required_modifiers = + +dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method +dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.非字段成员.required_modifiers = + +# 命名样式 + +dotnet_naming_style.以_i_开始.required_prefix = I +dotnet_naming_style.以_i_开始.required_suffix = +dotnet_naming_style.以_i_开始.word_separator = +dotnet_naming_style.以_i_开始.capitalization = pascal_case + +dotnet_naming_style.帕斯卡拼写法.required_prefix = +dotnet_naming_style.帕斯卡拼写法.required_suffix = +dotnet_naming_style.帕斯卡拼写法.word_separator = +dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case + +dotnet_naming_style.帕斯卡拼写法.required_prefix = +dotnet_naming_style.帕斯卡拼写法.required_suffix = +dotnet_naming_style.帕斯卡拼写法.word_separator = +dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case + +[*.{cs,vb}] +end_of_line = crlf +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +indent_size = 4 +tab_width = 4 +dotnet_style_operator_placement_when_wrapping = beginning_of_line + +# Add copyright file header +file_header_template = Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。\n\n本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。\n\n不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1911d33 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Build output +**/bin/ +**/obj/ +publish/ +logs/ + +# IDE and user settings +.vs/ +.idea/ +.vscode/ +*.user +*.suo + +# Local secrets and runtime files +**/Configuration/Database.local.json +**/wwwroot/upload/ +*.db +*.db-shm +*.db-wal + +# OS files +.DS_Store +Thumbs.db diff --git a/Admin.NET.Core/Admin.NET.Core.csproj b/Admin.NET.Core/Admin.NET.Core.csproj new file mode 100644 index 0000000..0524b16 --- /dev/null +++ b/Admin.NET.Core/Admin.NET.Core.csproj @@ -0,0 +1,74 @@ + + + + net8.0 + 1701;1702;1591;8632 + + enable + true + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Admin.NET.Core/Attribute/CommonValidationAttribute.cs b/Admin.NET.Core/Attribute/CommonValidationAttribute.cs new file mode 100644 index 0000000..e58d08a --- /dev/null +++ b/Admin.NET.Core/Attribute/CommonValidationAttribute.cs @@ -0,0 +1,92 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通用接口参数验证特性类 +/// +[AttributeUsage(AttributeTargets.Property)] +public class CommonValidationAttribute : ValidationAttribute +{ + private readonly Dictionary _conditions; + private static readonly Dictionary CompiledConditions = new(); + + /// + /// + /// 条件对参数,长度必须为偶数
+ /// 奇数字符串参数:动态条件
+ /// 偶数字符串参数:提示消息 + /// + /// + /// + /// public class ModelInput { + /// + /// + /// public string A { get; set; } + /// + /// + /// [CommonValidation( + /// "A == 1 && B == null", "当 A == 1时,B不能为空", + /// "C == 2 && B == null", "当 C == 2时,B不能为空" + /// )] + /// public string B { get; set; } + /// } + /// + /// + public CommonValidationAttribute(params string[] conditionPairs) + { + if (conditionPairs.Length % 2 != 0) throw new ArgumentException("条件对必须以偶数个字符串的形式提供。"); + + var conditions = new Dictionary(); + for (int i = 0; i < conditionPairs.Length; i += 2) + conditions.Add(conditionPairs[i], conditionPairs[i + 1]); + + _conditions = conditions; + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + foreach (var (expr, errorMessage) in _conditions) + { + var conditionKey = $"{validationContext.ObjectType.FullName}.{expr}"; + if (!CompiledConditions.TryGetValue(conditionKey, out var condition)) + { + condition = CreateCondition(validationContext.ObjectType, expr); + CompiledConditions[conditionKey] = condition; + } + + if ((bool)condition.DynamicInvoke(validationContext.ObjectInstance)!) + { + return new ValidationResult(errorMessage ?? $"[{validationContext.MemberName}]校验失败"); + } + } + + return ValidationResult.Success; + } + + private Delegate CreateCondition(Type modelType, string expression) + { + try + { + // 创建参数表达式 + var parameter = Expression.Parameter(typeof(object), "x"); + + // 构建 Lambda 表达式 + var lambda = DynamicExpressionParser.ParseLambda(new[] { Expression.Parameter(modelType, "x") }, typeof(bool), expression); + + // 创建新的 Lambda 表达式,接受 object 参数并调用编译后的表达式 + var invokeExpression = Expression.Invoke(lambda, Expression.Convert(parameter, modelType)); + var finalLambda = Expression.Lambda>(invokeExpression, parameter); + + return finalLambda.Compile(); + } + catch (Exception ex) + { + throw new ArgumentException($"无法解析表达式 '{expression}': {ex.Message}", ex); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/ConstAttribute.cs b/Admin.NET.Core/Attribute/ConstAttribute.cs new file mode 100644 index 0000000..baf5a60 --- /dev/null +++ b/Admin.NET.Core/Attribute/ConstAttribute.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 常量特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)] +public class ConstAttribute : Attribute +{ + public string Name { get; set; } + + public ConstAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/CustomJsonPropertyAttribute.cs b/Admin.NET.Core/Attribute/CustomJsonPropertyAttribute.cs new file mode 100644 index 0000000..a5895f4 --- /dev/null +++ b/Admin.NET.Core/Attribute/CustomJsonPropertyAttribute.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 自定义Json转换字段名 +/// +[AttributeUsage(AttributeTargets.Property)] +public class CustomJsonPropertyAttribute : Attribute +{ + public string Name { get; } + + public CustomJsonPropertyAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/CustomUnifyResultAttribute.cs b/Admin.NET.Core/Attribute/CustomUnifyResultAttribute.cs new file mode 100644 index 0000000..0543422 --- /dev/null +++ b/Admin.NET.Core/Attribute/CustomUnifyResultAttribute.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 自定义规范化结果特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)] +public class CustomUnifyResultAttribute : Attribute +{ + public string Name { get; set; } + + public CustomUnifyResultAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/DataMaskAttribute.cs b/Admin.NET.Core/Attribute/DataMaskAttribute.cs new file mode 100644 index 0000000..d7eafc2 --- /dev/null +++ b/Admin.NET.Core/Attribute/DataMaskAttribute.cs @@ -0,0 +1,59 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据脱敏特性(支持自定义脱敏位置和脱敏字符) +/// +[AttributeUsage(AttributeTargets.Property)] +public class DataMaskAttribute : Attribute +{ + /// + /// 脱敏起始位置(从0开始) + /// + private int StartIndex { get; } + + /// + /// 脱敏长度 + /// + private int Length { get; } + + /// + /// 脱敏字符(默认*) + /// + private char MaskChar { get; set; } = '*'; + + /// + /// 是否保留原始长度(默认true) + /// + private bool KeepLength { get; set; } = true; + + public DataMaskAttribute(int startIndex, int length) + { + if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex)); + if (length <= 0) throw new ArgumentOutOfRangeException(nameof(length)); + + StartIndex = startIndex; + Length = length; + } + + /// + /// 执行脱敏处理 + /// + public string Mask(string input) + { + if (string.IsNullOrEmpty(input) || input.Length <= StartIndex) + return input; + + var maskedLength = Math.Min(Length, input.Length - StartIndex); + var maskStr = new string(MaskChar, KeepLength ? maskedLength : Math.Min(4, maskedLength)); + + return input.Substring(0, StartIndex) + maskStr + + (StartIndex + maskedLength < input.Length ? + input.Substring(StartIndex + maskedLength) : ""); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/DictAttribute.cs b/Admin.NET.Core/Attribute/DictAttribute.cs new file mode 100644 index 0000000..2a1fae8 --- /dev/null +++ b/Admin.NET.Core/Attribute/DictAttribute.cs @@ -0,0 +1,138 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 字典值合规性校验特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true, Inherited = true)] +public class DictAttribute : ValidationAttribute, ITransient +{ + /// + /// 字典编码 + /// + public string DictTypeCode { get; } + + /// + /// 是否允许空字符串 + /// + public bool AllowEmptyStrings { get; set; } = false; + + /// + /// 允许空值,有值才验证,默认 false + /// + public bool AllowNullValue { get; set; } = false; + + /// + /// 字典值合规性校验特性 + /// + /// + /// + public DictAttribute(string dictTypeCode = "", string errorMessage = "字典值不合法!") + { + DictTypeCode = dictTypeCode; + ErrorMessage = errorMessage; + } + + /// + /// 字典值合规性校验 + /// + /// + /// + /// + protected override ValidationResult IsValid(object? value, ValidationContext validationContext) + { + // 判断是否允许空值 + if (AllowNullValue && value == null) return ValidationResult.Success; + + // 获取属性的类型 + var property = validationContext.ObjectType.GetProperty(validationContext.MemberName!); + if (property == null) return new ValidationResult($"未知属性: {validationContext.MemberName}"); + + string importHeaderName = GetImporterHeaderName(property, validationContext.MemberName); + + var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + + // 先尝试从 ValidationContext 的依赖注入容器中拿服务,拿不到或类型不匹配时,再从全局的 App 容器中获取 + if (validationContext.GetService(typeof(SysDictDataService)) is not SysDictDataService sysDictDataService) + sysDictDataService = App.GetRequiredService(); + + // 获取字典值列表 + var dictDataList = sysDictDataService.GetDataList(DictTypeCode).GetAwaiter().GetResult(); + + // 使用 HashSet 来提高查找效率 + var dictHash = new HashSet(dictDataList.Select(u => u.Value)); + + // 判断是否为集合类型 + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + // 如果是空集合并且允许空值,则直接返回成功 + if (value == null && AllowNullValue) return ValidationResult.Success; + + // 处理集合为空的情况 + var collection = value as IEnumerable; + if (collection == null) return ValidationResult.Success; + + // 获取集合的元素类型 + var elementType = propertyType.GetGenericArguments()[0]; + var underlyingElementType = Nullable.GetUnderlyingType(elementType) ?? elementType; + + // 如果元素类型是枚举,则逐个验证 + if (underlyingElementType.IsEnum) + { + foreach (var item in collection) + { + if (item == null && AllowNullValue) continue; + + if (!Enum.IsDefined(underlyingElementType, item!)) + return new ValidationResult($"提示:{ErrorMessage}|枚举值【{item}】不是有效的【{underlyingElementType.Name}】枚举类型值!", [importHeaderName]); + } + return ValidationResult.Success; + } + + foreach (var item in collection) + { + if (item == null && AllowNullValue) continue; + + var itemString = item?.ToString(); + if (!dictHash.Contains(itemString)) + return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{itemString}】!", [importHeaderName]); + } + + return ValidationResult.Success; + } + + var valueAsString = value?.ToString(); + + // 是否忽略空字符串 + if (AllowEmptyStrings && string.IsNullOrEmpty(valueAsString)) return ValidationResult.Success; + + // 枚举类型验证 + if (propertyType.IsEnum) + { + if (!Enum.IsDefined(propertyType, value!)) return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!", [importHeaderName]); + return ValidationResult.Success; + } + + if (!dictHash.Contains(valueAsString)) + return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{valueAsString}】!", [importHeaderName]); + + return ValidationResult.Success; + } + + /// + /// 获取本字段上 [ImporterHeader(Name = "xxx")] 里的Name,如果没有则使用defaultName. + /// 用于在从excel导入数据时,能让调用者知道是哪个字段验证失败,而不是抛异常 + /// + private static string GetImporterHeaderName(PropertyInfo property, string defaultName) + { + var importerHeader = property.GetCustomAttribute(); + string importerHeaderName = importerHeader?.Name ?? defaultName; + return importerHeaderName; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/EnumAttribute.cs b/Admin.NET.Core/Attribute/EnumAttribute.cs new file mode 100644 index 0000000..96e3295 --- /dev/null +++ b/Admin.NET.Core/Attribute/EnumAttribute.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举值合规性校验特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Enum | AttributeTargets.Field, AllowMultiple = true)] +public class EnumAttribute : ValidationAttribute, ITransient +{ + /// + /// 枚举值合规性校验特性 + /// + /// + public EnumAttribute(string errorMessage = "枚举值不合法!") + { + ErrorMessage = errorMessage; + } + + /// + /// 枚举值合规性校验 + /// + /// + /// + /// + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + // 获取属性的类型 + var property = validationContext.ObjectType.GetProperty(validationContext.MemberName); + if (property == null) + return new ValidationResult($"未知属性: {validationContext.MemberName}"); + + var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + + // 检查属性类型是否为枚举或可空枚举类型 + if (!propertyType.IsEnum) + return new ValidationResult($"属性类型'{validationContext.MemberName}'不是有效的枚举类型!"); + + // 检查枚举值是否有效 + if (value == null && Nullable.GetUnderlyingType(property.PropertyType) == null) + return new ValidationResult($"提示:{ErrorMessage}|枚举值不能为 null!"); + + if (value != null && !Enum.IsDefined(propertyType, value)) + return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!"); + + return ValidationResult.Success; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/IdempotentAttribute.cs b/Admin.NET.Core/Attribute/IdempotentAttribute.cs new file mode 100644 index 0000000..ed692a4 --- /dev/null +++ b/Admin.NET.Core/Attribute/IdempotentAttribute.cs @@ -0,0 +1,112 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; +using System.Security.Claims; + +namespace Admin.NET.Core; + +/// +/// 防止重复请求过滤器特性(此特性使用了分布式锁,需确保系统支持分布式锁) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)] +public class IdempotentAttribute : Attribute, IAsyncActionFilter +{ + /// + /// 请求间隔时间/秒 + /// + public int IntervalTime { get; set; } = 5; + + /// + /// 错误提示内容 + /// + public string Message { get; set; } = "你操作频率过快,请稍后重试!"; + + /// + /// 缓存前缀: Key+请求路由+用户Id+请求参数 + /// + public string CacheKey { get; set; } = CacheConst.KeyIdempotent; + + /// + /// 是否直接抛出异常:Ture是,False返回上次请求结果 + /// + public bool ThrowBah { get; set; } + + /// + /// 锁前缀 + /// + public string LockPrefix { get; set; } = "lock_"; + + public IdempotentAttribute() + { + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var httpContext = context.HttpContext; + var path = httpContext.Request.Path.Value.ToString(); + var userId = httpContext.User?.FindFirstValue(ClaimConst.UserId); + var cacheExpireTime = TimeSpan.FromSeconds(IntervalTime); + + var parameters = JsonConvert.SerializeObject(context.ActionArguments, Formatting.None, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Include, + DefaultValueHandling = DefaultValueHandling.Include + }); + + var cacheKey = CacheKey + MD5Encryption.Encrypt($"{path}{userId}{parameters}"); + var sysCacheService = httpContext.RequestServices.GetService(); + try + { + // 分布式锁 + using var distributedLock = sysCacheService.BeginCacheLock($"{LockPrefix}{cacheKey}") ?? throw Oops.Oh(Message); + + var cacheValue = sysCacheService.Get(cacheKey); + if (cacheValue != null) + { + if (ThrowBah) throw Oops.Oh(Message); + context.Result = new ObjectResult(cacheValue.Value); + return; + } + else + { + var resultContext = await next(); + // 缓存请求结果,null值不缓存 + if (resultContext.Result is ObjectResult { Value: { } } objectResult) + { + var typeName = objectResult.Value.GetType().Name; + var responseData = new ResponseData + { + Type = typeName, + Value = objectResult.Value + }; + sysCacheService.Set(cacheKey, responseData, cacheExpireTime); + } + } + } + catch (Exception ex) + { + throw Oops.Oh($"{Message}-{ex}"); + } + } + + /// + /// 请求结果数据 + /// + private class ResponseData + { + /// + /// 结果类型 + /// + public string Type { get; set; } + + /// + /// 请求结果 + /// + public dynamic Value { get; set; } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/IgnoreEnumToDictAttribute.cs b/Admin.NET.Core/Attribute/IgnoreEnumToDictAttribute.cs new file mode 100644 index 0000000..067dc0a --- /dev/null +++ b/Admin.NET.Core/Attribute/IgnoreEnumToDictAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 忽略枚举类型转字典特性(标记在枚举类型) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Enum, AllowMultiple = true, Inherited = true)] +public class IgnoreEnumToDictAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/IgnoreTableAttribute.cs b/Admin.NET.Core/Attribute/IgnoreTableAttribute.cs new file mode 100644 index 0000000..5fb65b9 --- /dev/null +++ b/Admin.NET.Core/Attribute/IgnoreTableAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 忽略表结构初始化特性(标记在实体) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class IgnoreTableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/IgnoreUpdateSeedAttribute.cs b/Admin.NET.Core/Attribute/IgnoreUpdateSeedAttribute.cs new file mode 100644 index 0000000..9ea18da --- /dev/null +++ b/Admin.NET.Core/Attribute/IgnoreUpdateSeedAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 忽略更新种子特性(标记在种子类) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class IgnoreUpdateSeedAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs b/Admin.NET.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs new file mode 100644 index 0000000..1bccc81 --- /dev/null +++ b/Admin.NET.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 忽略更新种子列特性(标记在实体属性) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] +public class IgnoreUpdateSeedColumnAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/ImportDictAttribute.cs b/Admin.NET.Core/Attribute/ImportDictAttribute.cs new file mode 100644 index 0000000..552df72 --- /dev/null +++ b/Admin.NET.Core/Attribute/ImportDictAttribute.cs @@ -0,0 +1,24 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 属性字典配置 +/// +[AttributeUsage(AttributeTargets.Property)] +public class ImportDictAttribute : Attribute +{ + /// + /// 字典Code + /// + public string TypeCode { get; set; } + + /// + /// 目标对象名称 + /// + public string TargetPropName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/IncreSeedAttribute.cs b/Admin.NET.Core/Attribute/IncreSeedAttribute.cs new file mode 100644 index 0000000..9ab1f02 --- /dev/null +++ b/Admin.NET.Core/Attribute/IncreSeedAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 增量种子特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class IncreSeedAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/IncreTableAttribute.cs b/Admin.NET.Core/Attribute/IncreTableAttribute.cs new file mode 100644 index 0000000..31f31b7 --- /dev/null +++ b/Admin.NET.Core/Attribute/IncreTableAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 增量表特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class IncreTableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/LogTableAttribute.cs b/Admin.NET.Core/Attribute/LogTableAttribute.cs new file mode 100644 index 0000000..aa07fd4 --- /dev/null +++ b/Admin.NET.Core/Attribute/LogTableAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 日志表特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class LogTableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/MaskNewtonsoftJsonConverter.cs b/Admin.NET.Core/Attribute/MaskNewtonsoftJsonConverter.cs new file mode 100644 index 0000000..a09c801 --- /dev/null +++ b/Admin.NET.Core/Attribute/MaskNewtonsoftJsonConverter.cs @@ -0,0 +1,60 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core; + +/// +/// 字符串掩码 +/// +[SuppressSniffer] +public class MaskNewtonsoftJsonConverter : JsonConverter +{ + public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.Value.ToString(); + } + + public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString().Mask()); + } +} + +/// +/// 身份证掩码 +/// +[SuppressSniffer] +public class MaskIdCardNewtonsoftJsonConverter : JsonConverter +{ + public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.Value.ToString(); + } + + public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString().MaskIdCard()); + } +} + +/// +/// 邮箱掩码 +/// +[SuppressSniffer] +public class MaskEmailNewtonsoftJsonConverter : JsonConverter +{ + public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.Value.ToString(); + } + + public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString().MaskEmail()); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/MaskSystemTextJsonConverter.cs b/Admin.NET.Core/Attribute/MaskSystemTextJsonConverter.cs new file mode 100644 index 0000000..6b1a12f --- /dev/null +++ b/Admin.NET.Core/Attribute/MaskSystemTextJsonConverter.cs @@ -0,0 +1,61 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Admin.NET.Core; + +/// +/// 字符串掩码 +/// +[SuppressSniffer] +public class MaskSystemTextJsonConverter : JsonConverter +{ + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString().Mask()); + } +} + +/// +/// 身份证掩码 +/// +[SuppressSniffer] +public class MaskIdCardSystemTextJsonConverter : JsonConverter +{ + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString().MaskIdCard()); + } +} + +/// +/// 邮箱掩码 +/// +[SuppressSniffer] +public class MaskEmailSystemTextJsonConverter : JsonConverter +{ + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString().MaskEmail()); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/MaxValueAttribute.cs b/Admin.NET.Core/Attribute/MaxValueAttribute.cs new file mode 100644 index 0000000..8132890 --- /dev/null +++ b/Admin.NET.Core/Attribute/MaxValueAttribute.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 最大值校验 +/// +[SuppressSniffer] +public class MaxValueAttribute : ValidationAttribute +{ + private double MaxValue { get; } + + /// + /// 最大值 + /// + /// + public MaxValueAttribute(double value) => this.MaxValue = value; + + /// + /// 最大值校验 + /// + /// + /// + public override bool IsValid(object value) + { + return value == null || Convert.ToDouble(value) <= this.MaxValue; + } + + /// + /// 错误信息 + /// + /// + /// + public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name); +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/MinValueAttribute.cs b/Admin.NET.Core/Attribute/MinValueAttribute.cs new file mode 100644 index 0000000..40c7378 --- /dev/null +++ b/Admin.NET.Core/Attribute/MinValueAttribute.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 最小值校验 +/// +[SuppressSniffer] +public class MinValueAttribute : ValidationAttribute +{ + private double MinValue { get; set; } + + /// + /// 最小值 + /// + /// + public MinValueAttribute(double value) => this.MinValue = value; + + /// + /// 最小值校验 + /// + /// + /// + public override bool IsValid(object value) + { + return value == null || Convert.ToDouble(value) > this.MinValue; + } + + /// + /// 错误信息 + /// + /// + /// + public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name); +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/NotEmptyAttribute.cs b/Admin.NET.Core/Attribute/NotEmptyAttribute.cs new file mode 100644 index 0000000..a021120 --- /dev/null +++ b/Admin.NET.Core/Attribute/NotEmptyAttribute.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 校验集合不能为空 +/// +[SuppressSniffer] +public class NotEmptyAttribute : ValidationAttribute +{ + /// + /// 校验集合不能为空 + /// + /// + /// + public override bool IsValid(object value) => (value as IEnumerable)?.GetEnumerator().MoveNext() ?? false; + + /// + /// 错误信息 + /// + /// + /// + public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name); +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/OwnerOrgAttribute.cs b/Admin.NET.Core/Attribute/OwnerOrgAttribute.cs new file mode 100644 index 0000000..468272c --- /dev/null +++ b/Admin.NET.Core/Attribute/OwnerOrgAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 所属机构数据权限 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] +public class OwnerOrgAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/OwnerUserAttribute.cs b/Admin.NET.Core/Attribute/OwnerUserAttribute.cs new file mode 100644 index 0000000..4fc7c69 --- /dev/null +++ b/Admin.NET.Core/Attribute/OwnerUserAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 所属用户数据权限 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] +public class OwnerUserAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/SeedDataAttribute.cs b/Admin.NET.Core/Attribute/SeedDataAttribute.cs new file mode 100644 index 0000000..1463eff --- /dev/null +++ b/Admin.NET.Core/Attribute/SeedDataAttribute.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 种子数据特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class SeedDataAttribute : Attribute +{ + /// + /// 排序(越大越后执行) + /// + public int Order { get; set; } = 0; + + public SeedDataAttribute(int orderNo) + { + Order = orderNo; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/SysTableAttribute.cs b/Admin.NET.Core/Attribute/SysTableAttribute.cs new file mode 100644 index 0000000..f526b42 --- /dev/null +++ b/Admin.NET.Core/Attribute/SysTableAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统表特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class SysTableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Attribute/ThemeAttribute.cs b/Admin.NET.Core/Attribute/ThemeAttribute.cs new file mode 100644 index 0000000..48be9d9 --- /dev/null +++ b/Admin.NET.Core/Attribute/ThemeAttribute.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举拓展主题样式 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Enum | AttributeTargets.Field)] +public class ThemeAttribute : Attribute +{ + public string Theme { get; private set; } + + public ThemeAttribute(string theme) + { + this.Theme = theme; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Cache/CacheSetup.cs b/Admin.NET.Core/Cache/CacheSetup.cs new file mode 100644 index 0000000..5d55ecf --- /dev/null +++ b/Admin.NET.Core/Cache/CacheSetup.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.Extensions.DependencyInjection.Extensions; +using NewLife.Caching.Services; + +namespace Admin.NET.Core; + +public static class CacheSetup +{ + /// + /// 缓存注册(新生命Redis组件) + /// + /// + public static void AddCache(this IServiceCollection services) + { + var cacheOptions = App.GetConfig("Cache", true); + if (cacheOptions.CacheType == CacheTypeEnum.Redis.ToString()) + { + var redis = new FullRedis(new RedisOptions + { + Configuration = cacheOptions.Redis.Configuration, + Prefix = cacheOptions.Redis.Prefix + }) + { + // 自动检测集群节点 + AutoDetect = App.GetConfig("Cache:Redis:AutoDetect", true) + }; + // 最大消息大小 + if (cacheOptions.Redis.MaxMessageSize > 0) + redis.MaxMessageSize = cacheOptions.Redis.MaxMessageSize; + + // 注入 Redis 缓存提供者 + services.AddSingleton(u => new RedisCacheProvider(u) { Cache = redis }); + } + + // 内存缓存兜底。在没有配置Redis时,使用内存缓存,逻辑代码无需修改 + services.TryAddSingleton(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Cache/SqlSugarCache.cs b/Admin.NET.Core/Cache/SqlSugarCache.cs new file mode 100644 index 0000000..6bbcda6 --- /dev/null +++ b/Admin.NET.Core/Cache/SqlSugarCache.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// SqlSugar二级缓存 +/// +public class SqlSugarCache : ICacheService +{ + /// + /// 系统缓存服务 + /// + private static readonly SysCacheService _cache = App.GetRequiredService(); + + public void Add(string key, V value) + { + _cache.Set($"{CacheConst.SqlSugar}{key}", value); + } + + public void Add(string key, V value, int cacheDurationInSeconds) + { + _cache.Set($"{CacheConst.SqlSugar}{key}", value, TimeSpan.FromSeconds(cacheDurationInSeconds)); + } + + public bool ContainsKey(string key) + { + return _cache.ExistKey($"{CacheConst.SqlSugar}{key}"); + } + + public V Get(string key) + { + return _cache.Get($"{CacheConst.SqlSugar}{key}"); + } + + public IEnumerable GetAllKey() + { + return _cache.GetKeysByPrefixKey(CacheConst.SqlSugar); + } + + public V GetOrCreate(string key, Func create, int cacheDurationInSeconds = int.MaxValue) + { + return _cache.GetOrAdd($"{CacheConst.SqlSugar}{key}", (cacheKey) => + { + return create(); + }, cacheDurationInSeconds); + } + + public void Remove(string key) + { + _cache.Remove(key); // SqlSugar调用Remove方法时,key中已包含了CacheConst.SqlSugar前缀 + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Const/AlipayConst.cs b/Admin.NET.Core/Const/AlipayConst.cs new file mode 100644 index 0000000..d6c4dda --- /dev/null +++ b/Admin.NET.Core/Const/AlipayConst.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 支付宝支付常量 +/// +[SuppressSniffer] +public class AlipayConst +{ + /// + /// 单笔无密转账【业务场景】固定值 + /// + public const string BizScene = "DIRECT_TRANSFER"; + + /// + /// 单笔无密转账【销售产品码】固定值 + /// + public const string ProductCode = "TRANS_ACCOUNT_NO_PWD"; + + /// + /// 交易状态参数名 + /// + public const string TradeStatus = "trade_status"; + + /// + /// 交易成功标识 + /// + public const string TradeSuccess = "TRADE_SUCCESS"; + + /// + /// 授权类型 + /// + public const string GrantType = "authorization_code"; +} \ No newline at end of file diff --git a/Admin.NET.Core/Const/CacheConst.cs b/Admin.NET.Core/Const/CacheConst.cs new file mode 100644 index 0000000..3a0a3b9 --- /dev/null +++ b/Admin.NET.Core/Const/CacheConst.cs @@ -0,0 +1,128 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 缓存相关常量 +/// +public class CacheConst +{ + /// + /// 用户权限缓存(按钮集合) + /// + public const string KeyUserButton = "sys_user_button:"; + + /// + /// 用户机构缓存 + /// + public const string KeyUserOrg = "sys_user_org:"; + + /// + /// 角色最大数据范围缓存 + /// + public const string KeyRoleMaxDataScope = "sys_role_maxDataScope:"; + + /// + /// 在线用户缓存 + /// + public const string KeyUserOnline = "sys_user_online:"; + + /// + /// 图形验证码缓存 + /// + public const string KeyVerCode = "sys_verCode:"; + + /// + /// 手机验证码缓存 + /// + public const string KeyPhoneVerCode = "sys_phoneVerCode:"; + + /// + /// 密码错误次数缓存 + /// + public const string KeyPasswordErrorTimes = "sys_password_error_times:"; + + /// + /// 租户缓存 + /// + public const string KeyTenant = "sys_tenant"; + + /// + /// 常量下拉框 + /// + public const string KeyConst = "sys_const:"; + + /// + /// 所有缓存关键字集合 + /// + public const string KeyAll = "sys_keys"; + + /// + /// SqlSugar二级缓存 + /// + public const string SqlSugar = "sys_sqlSugar:"; + + /// + /// 开放接口身份缓存 + /// + public const string KeyOpenAccess = "sys_open_access:"; + + /// + /// 开放接口身份随机数缓存 + /// + public const string KeyOpenAccessNonce = "sys_open_access_nonce:"; + + /// + /// 登录黑名单 + /// + public const string KeyBlacklist = "sys_blacklist:"; + + /// + /// 系统配置缓存 + /// + public const string KeyConfig = "sys_config:"; + + /// + /// 系统租户配置缓存 + /// + public const string KeyTenantConfig = "sys_tenant_config:"; + + /// + /// 系统用户配置缓存 + /// + public const string KeyUserConfig = "sys_user_config:"; + + /// + /// 系统字典缓存 + /// + public const string KeyDict = "sys_dict:"; + + /// + /// 系统租户字典缓存 + /// + public const string KeyTenantDict = "sys_tenant_dict:"; + + /// + /// 重复请求(幂等)字典缓存 + /// + public const string KeyIdempotent = "sys_idempotent:"; + + /// + /// Excel临时文件缓存 + /// + public const string KeyExcelTemp = "sys_excel_temp:"; + + /// + /// 系统更新命令日志缓存 + /// + public const string KeySysUpdateLog = "sys_update_log"; + + /// + /// 系统更新间隔标记缓存 + /// + public const string KeySysUpdateInterval = "sys_update_interval"; +} \ No newline at end of file diff --git a/Admin.NET.Core/Const/ClaimConst.cs b/Admin.NET.Core/Const/ClaimConst.cs new file mode 100644 index 0000000..7b3218b --- /dev/null +++ b/Admin.NET.Core/Const/ClaimConst.cs @@ -0,0 +1,73 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Claim相关常量 +/// +public class ClaimConst +{ + /// + /// 用户Id + /// + public const string UserId = "UserId"; + + /// + /// 账号 + /// + public const string Account = "Account"; + + /// + /// 真实姓名 + /// + public const string RealName = "RealName"; + + /// + /// 昵称 + /// + public const string NickName = "NickName"; + + /// + /// 账号类型 + /// + public const string AccountType = "AccountType"; + + /// + /// 租户Id + /// + public const string TenantId = "TenantId"; + + /// + /// 组织机构Id + /// + public const string OrgId = "OrgId"; + + /// + /// 组织机构名称 + /// + public const string OrgName = "OrgName"; + + /// + /// 组织机构类型 + /// + public const string OrgType = "OrgType"; + + /// + /// 微信OpenId + /// + public const string OpenId = "OpenId"; + + /// + /// 登录模式PC、APP + /// + public const string LoginMode = "LoginMode"; + + /// + /// 语言代码 + /// + public const string LangCode = "LangCode"; +} \ No newline at end of file diff --git a/Admin.NET.Core/Const/CommonConst.cs b/Admin.NET.Core/Const/CommonConst.cs new file mode 100644 index 0000000..dec1d9f --- /dev/null +++ b/Admin.NET.Core/Const/CommonConst.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通用常量 +/// +[Const("平台配置")] +public class CommonConst +{ + /// + /// 日志分组名称 + /// + public const string SysLogCategoryName = "System.Logging.LoggingMonitor"; + + /// + /// 事件-增加异常日志 + /// + public const string AddExLog = "Add:ExLog"; + + /// + /// 事件-发送异常邮件 + /// + public const string SendErrorMail = "Send:ErrorMail"; + + /// + /// 默认基本角色名称 + /// + public const string DefaultBaseRoleName = "默认基本角色"; + + /// + /// 默认基本角色编码 + /// + public const string DefaultBaseRoleCode = "default_base_role"; +} \ No newline at end of file diff --git a/Admin.NET.Core/Const/ConfigConst.cs b/Admin.NET.Core/Const/ConfigConst.cs new file mode 100644 index 0000000..bc35f05 --- /dev/null +++ b/Admin.NET.Core/Const/ConfigConst.cs @@ -0,0 +1,148 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 配置常量 +/// +public class ConfigConst +{ + /// + /// 演示环境 + /// + public const string SysDemoEnv = "sys_demo"; + + /// + /// 默认密码 + /// + public const string SysPassword = "sys_password"; + + /// + /// 密码最大错误次数 + /// + public const string SysPasswordMaxErrorTimes = "sys_password_max_error_times"; + + /// + /// 日志保留天数 + /// + public const string SysLogRetentionDays = "sys_log_retention_days"; + + /// + /// 记录操作日志 + /// + public const string SysOpLog = "sys_oplog"; + + /// + /// 单设备登录 + /// + public const string SysSingleLogin = "sys_single_login"; + + /// + /// 登入登出提醒 + /// + public const string SysLoginOutReminder = "sys_login_out_reminder"; + + /// + /// 登陆时隐藏租户 + /// + public const string SysHideTenantLogin = "sys_hide_tenant_login"; + + /// + /// 登录二次验证 + /// + public const string SysSecondVer = "sys_second_ver"; + + /// + /// 图形验证码 + /// + public const string SysCaptcha = "sys_captcha"; + + /// + /// Token过期时间 + /// + public const string SysTokenExpire = "sys_token_expire"; + + /// + /// RefreshToken过期时间 + /// + public const string SysRefreshTokenExpire = "sys_refresh_token_expire"; + + /// + /// 发送异常日志邮件 + /// + public const string SysErrorMail = "sys_error_mail"; + + /// + /// 域登录验证 + /// + public const string SysDomainLogin = "sys_domain_login"; + + // /// + // /// 租户域名隔离登录验证 + // /// + // public const string SysTenantHostLogin = "sys_tenant_host_login"; + + /// + /// 数据校验日志 + /// + public const string SysValidationLog = "sys_validation_log"; + + /// + /// 行政区域同步层级 1-省级,2-市级,3-区县级,4-街道级,5-村级 + /// + public const string SysRegionSyncLevel = "sys_region_sync_level"; + + /// + /// Default 分组 + /// + public const string SysDefaultGroup = "Default"; + + /// + /// 支付宝授权页面地址 + /// + public const string AlipayAuthPageUrl = "alipay_auth_page_url_"; + + // /// + // /// 系统图标 + // /// + // public const string SysWebLogo = "sys_web_logo"; + // + // /// + // /// 系统主标题 + // /// + // public const string SysWebTitle = "sys_web_title"; + // + // /// + // /// 系统副标题 + // /// + // public const string SysWebViceTitle = "sys_web_viceTitle"; + // + // /// + // /// 系统描述 + // /// + // public const string SysWebViceDesc = "sys_web_viceDesc"; + // + // /// + // /// 水印内容 + // /// + // public const string SysWebWatermark = "sys_web_watermark"; + // + // /// + // /// 版权说明 + // /// + // public const string SysWebCopyright = "sys_web_copyright"; + // + // /// + // /// ICP备案号 + // /// + // public const string SysWebIcp = "sys_web_icp"; + // + // /// + // /// ICP地址 + // /// + // public const string SysWebIcpUrl = "sys_web_icpUrl"; +} \ No newline at end of file diff --git a/Admin.NET.Core/Const/SqlSugarConst.cs b/Admin.NET.Core/Const/SqlSugarConst.cs new file mode 100644 index 0000000..ddbcbd4 --- /dev/null +++ b/Admin.NET.Core/Const/SqlSugarConst.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// SqlSugar相关常量 +/// +public class SqlSugarConst +{ + /// + /// 默认主数据库标识(默认租户) + /// + public const string MainConfigId = "1300000000001"; + + /// + /// 默认日志数据库标识 + /// + public const string LogConfigId = "1300000000002"; + + /// + /// 默认表主键 + /// + public const string PrimaryKey = "Id"; + + /// + /// 默认租户Id + /// + public const long DefaultTenantId = 1300000000001; +} \ No newline at end of file diff --git a/Admin.NET.Core/ElasticSearch/ElasticSearchClientContainer.cs b/Admin.NET.Core/ElasticSearch/ElasticSearchClientContainer.cs new file mode 100644 index 0000000..6c25664 --- /dev/null +++ b/Admin.NET.Core/ElasticSearch/ElasticSearchClientContainer.cs @@ -0,0 +1,47 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Elastic.Clients.Elasticsearch; + +namespace Admin.NET.Core; + +/// +/// ES客户端容器 +/// +public class ElasticSearchClientContainer +{ + private readonly Dictionary _clients; + + /// + /// 初始化容器(通过字典注入所有客户端) + /// + public ElasticSearchClientContainer(Dictionary clients) + { + _clients = clients ?? throw new ArgumentNullException(nameof(clients)); + } + + /// + /// 日志专用客户端 + /// + public ElasticsearchClient Logging => GetClient(EsClientTypeEnum.Logging); + + /// + /// 业务数据同步客户端 + /// + public ElasticsearchClient Business => GetClient(EsClientTypeEnum.Business); + + /// + /// 根据类型获取客户端(内部校验,避免未注册的类型) + /// + private ElasticsearchClient GetClient(EsClientTypeEnum type) + { + if (_clients.TryGetValue(type, out var client)) + { + return client; + } + throw new KeyNotFoundException($"未注册的ES客户端类型:{type},请检查注册配置"); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/ElasticSearch/ElasticSearchClientFactory.cs b/Admin.NET.Core/ElasticSearch/ElasticSearchClientFactory.cs new file mode 100644 index 0000000..95592b4 --- /dev/null +++ b/Admin.NET.Core/ElasticSearch/ElasticSearchClientFactory.cs @@ -0,0 +1,85 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Elastic.Clients.Elasticsearch; +using Elastic.Transport; + +namespace Admin.NET.Core; + +public class ElasticSearchClientFactory +{ + /// + /// 创建 ES 客户端(通用方法) + /// + /// 配置类型(支持通用或场景专用) + /// 配置文件路径(如 "ElasticSearch:Logging") + /// ES 客户端实例(或 null if 未启用) + public static ElasticsearchClient? CreateClient(string configPath) where TOptions : ElasticSearchOptions, new() + { + // 从配置文件读取当前场景的配置 + var options = App.GetConfig(configPath); + if (options == null) + throw Oops.Oh($"未找到{configPath}配置项"); + + if (!options.Enabled) + return null; + + // 验证服务地址 + if (options.ServerUris == null || !options.ServerUris.Any()) + throw new ArgumentException($"ES 配置 {configPath} 未设置 ServerUris"); + + // 构建连接池(支持集群) + var uris = options.ServerUris.Select(uri => new Uri(uri)).ToList(); + var connectionPool = new StaticNodePool(uris); + var connectionSettings = new ElasticsearchClientSettings(connectionPool) + .DefaultIndex(options.DefaultIndex) // 设置默认索引 + .DisableDirectStreaming() // 开启请求/响应日志,方便排查问题 + .OnRequestCompleted(response => + { + if (response.HttpStatusCode == 401) + { + Console.WriteLine("ES 请求被拒绝:未提供有效认证信息"); + } + }); + + // 配置认证 + ConfigureAuthentication(connectionSettings, options); + + // 配置 HTTPS 证书指纹 + if (!string.IsNullOrEmpty(options.Fingerprint)) + connectionSettings.CertificateFingerprint(options.Fingerprint); + + return new ElasticsearchClient(connectionSettings); + } + + /// + /// 配置认证(通用逻辑) + /// + private static void ConfigureAuthentication(ElasticsearchClientSettings settings, ElasticSearchOptions options) + { + switch (options.AuthType) + { + case ElasticSearchAuthTypeEnum.Basic: + settings.Authentication(new BasicAuthentication(options.User, options.Password)); + break; + + case ElasticSearchAuthTypeEnum.ApiKey: + settings.Authentication(new ApiKey(options.ApiKey)); + break; + + case ElasticSearchAuthTypeEnum.Base64ApiKey: + settings.Authentication(new Base64ApiKey(options.Base64ApiKey)); + break; + + case ElasticSearchAuthTypeEnum.None: + // 无需认证 + break; + + default: + throw new ArgumentOutOfRangeException(nameof(options.AuthType)); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/ElasticSearch/ElasticSearchSetup.cs b/Admin.NET.Core/ElasticSearch/ElasticSearchSetup.cs new file mode 100644 index 0000000..8990d11 --- /dev/null +++ b/Admin.NET.Core/ElasticSearch/ElasticSearchSetup.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Elastic.Clients.Elasticsearch; + +namespace Admin.NET.Core.ElasticSearch; + +/// +/// ES服务注册 +/// +public static class ElasticSearchSetup +{ + /// + /// 注册所有ES客户端(日志+业务) + /// + public static void AddElasticSearchClients(this IServiceCollection services) + { + // 1. 创建客户端字典(枚举→客户端实例) + var clients = new Dictionary(); + + // 2. 注册日志客户端 + var loggingClient = ElasticSearchClientFactory.CreateClient(configPath: "ElasticSearch:Logging"); + if (loggingClient != null) + { + clients[EsClientTypeEnum.Logging] = loggingClient; + } + + // 3. 注册业务客户端 + var businessClient = ElasticSearchClientFactory.CreateClient(configPath: "ElasticSearch:Business"); + if (businessClient != null) + { + clients[EsClientTypeEnum.Business] = businessClient; + } + + // 4. 将客户端容器注册为单例(全局唯一) + services.AddSingleton(new ElasticSearchClientContainer(clients)); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/EntityBase.cs b/Admin.NET.Core/Entity/EntityBase.cs new file mode 100644 index 0000000..c334a2e --- /dev/null +++ b/Admin.NET.Core/Entity/EntityBase.cs @@ -0,0 +1,203 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 框架实体基类Id +/// +public abstract class EntityBaseId +{ + /// + /// 雪花Id + /// + [SugarColumn(ColumnName = "Id", ColumnDescription = "主键Id", IsPrimaryKey = true, IsIdentity = false)] + public virtual long Id { get; set; } +} + +/// +/// 框架实体基类 +/// +[SugarIndex("index_{table}_CT", nameof(CreateTime), OrderByType.Asc)] +public abstract class EntityBase : EntityBaseId +{ + /// + /// 创建时间 + /// + [SugarColumn(ColumnDescription = "创建时间", IsNullable = true, IsOnlyIgnoreUpdate = true)] + public virtual DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public virtual DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + [OwnerUser] + [SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)] + public virtual long? CreateUserId { get; set; } + + ///// + ///// 创建者 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(CreateUserId))] + //public virtual SysUser CreateUser { get; set; } + + /// + /// 创建者姓名 + /// + [SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)] + public virtual string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + [SugarColumn(ColumnDescription = "修改者Id")] + public virtual long? UpdateUserId { get; set; } + + ///// + ///// 修改者 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(UpdateUserId))] + //public virtual SysUser UpdateUser { get; set; } + + /// + /// 修改者姓名 + /// + [SugarColumn(ColumnDescription = "修改者姓名", Length = 64)] + public virtual string? UpdateUserName { get; set; } +} + +/// +/// 框架实体基类(删除标志) +/// +[SugarIndex("index_{table}_D", nameof(IsDelete), OrderByType.Asc)] +[SugarIndex("index_{table}_DT", nameof(DeleteTime), OrderByType.Asc)] +public abstract class EntityBaseDel : EntityBase, IDeletedFilter +{ + /// + /// 软删除 + /// + [SugarColumn(ColumnDescription = "软删除")] + public virtual bool IsDelete { get; set; } = false; + + /// + /// 软删除时间 + /// + [SugarColumn(ColumnDescription = "软删除时间")] + public virtual DateTime? DeleteTime { get; set; } +} + +/// +/// 机构实体基类(数据权限) +/// +public abstract class EntityBaseOrg : EntityBase, IOrgIdFilter +{ + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id", IsNullable = true)] + public virtual long OrgId { get; set; } + + ///// + ///// 创建者部门Id + ///// + //[SugarColumn(ColumnDescription = "创建者部门Id", IsOnlyIgnoreUpdate = true)] + //public virtual long? CreateOrgId { get; set; } + + ///// + ///// 创建者部门 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(CreateOrgId))] + //public virtual SysOrg CreateOrg { get; set; } + + ///// + ///// 创建者部门名称 + ///// + //[SugarColumn(ColumnDescription = "创建者部门名称", Length = 64, IsOnlyIgnoreUpdate = true)] + //public virtual string? CreateOrgName { get; set; } +} + +/// +/// 机构实体基类(数据权限、删除标志) +/// +public abstract class EntityBaseOrgDel : EntityBaseDel, IOrgIdFilter +{ + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id", IsNullable = true)] + public virtual long OrgId { get; set; } +} + +/// +/// 租户实体基类 +/// +public abstract class EntityBaseTenant : EntityBase, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户实体基类(删除标志) +/// +public abstract class EntityBaseTenantDel : EntityBaseDel, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户实体基类Id +/// +public abstract class EntityBaseTenantId : EntityBaseId, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户机构实体基类(数据权限) +/// +public abstract class EntityBaseTenantOrg : EntityBaseOrg, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户机构实体基类(数据权限、删除标志) +/// +public abstract class EntityBaseTenantOrgDel : EntityBaseOrgDel, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/IEntityFilter.cs b/Admin.NET.Core/Entity/IEntityFilter.cs new file mode 100644 index 0000000..ec4f57b --- /dev/null +++ b/Admin.NET.Core/Entity/IEntityFilter.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 假删除接口过滤器 +/// +public interface IDeletedFilter +{ + /// + /// 软删除 + /// + bool IsDelete { get; set; } +} + +/// +/// 租户Id接口过滤器 +/// +public interface ITenantIdFilter +{ + /// + /// 租户Id + /// + long? TenantId { get; set; } +} + +/// +/// 机构Id接口过滤器 +/// +public interface IOrgIdFilter +{ + /// + /// 机构Id + /// + long OrgId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysAlipayAuthInfo.cs b/Admin.NET.Core/Entity/SysAlipayAuthInfo.cs new file mode 100644 index 0000000..8842546 --- /dev/null +++ b/Admin.NET.Core/Entity/SysAlipayAuthInfo.cs @@ -0,0 +1,149 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 支付宝授权记录表 +/// +[SugarTable(null, "支付宝授权记录表")] +[SysTable] +[SugarIndex("index_{table}_U", nameof(UserId), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(OpenId), OrderByType.Asc)] +public class SysAlipayAuthInfo : EntityBase +{ + /// + /// 商户AppId + /// + [SugarColumn(ColumnDescription = "商户AppId", Length = 64)] + public string? AppId { get; set; } + + /// + /// 开放ID + /// + [SugarColumn(ColumnDescription = "开放ID", Length = 64)] + public string? OpenId { get; set; } + + /// + /// 用户ID + /// + [SugarColumn(ColumnDescription = "用户ID", Length = 64)] + public string? UserId { get; set; } + + /// + /// 性别 + /// + [SugarColumn(ColumnDescription = "性别", Length = 8)] + public GenderEnum Gender { get; set; } + + /// + /// 年龄 + /// + [SugarColumn(ColumnDescription = "年龄", Length = 16)] + public int Age { get; set; } + + /// + /// 手机号 + /// + [SugarColumn(ColumnDescription = "手机号", Length = 32)] + public string Mobile { get; set; } + + /// + /// 显示名称 + /// + [SugarColumn(ColumnDescription = "显示名称", Length = 128)] + public string DisplayName { get; set; } + + /// + /// 昵称 + /// + [SugarColumn(ColumnDescription = "昵称", Length = 64)] + public string NickName { get; set; } + + /// + /// 用户名 + /// + [SugarColumn(ColumnDescription = "用户名", Length = 64)] + public string UserName { get; set; } + + /// + /// 头像 + /// + [SugarColumn(ColumnDescription = "头像", Length = 512)] + public string? Avatar { get; set; } + + /// + /// 邮箱 + /// + [SugarColumn(ColumnDescription = "邮箱", Length = 128)] + public string? Email { get; set; } + + /// + /// 用户民族 + /// + [SugarColumn(ColumnDescription = "用户民族", Length = 32)] + public string? UserNation { get; set; } + + /// + /// 淘宝ID + /// + [SugarColumn(ColumnDescription = "淘宝ID", Length = 64)] + public string? TaobaoId { get; set; } + + /// + /// 电话 + /// + [SugarColumn(ColumnDescription = "电话", Length = 32)] + public string? Phone { get; set; } + + /// + /// 生日 + /// + [SugarColumn(ColumnDescription = "生日", Length = 32)] + public string? PersonBirthday { get; set; } + + /// + /// 职业 + /// + [SugarColumn(ColumnDescription = "职业", Length = 64)] + public string? Profession { get; set; } + + /// + /// 省份 + /// + [SugarColumn(ColumnDescription = "省份", Length = 64)] + public string? Province { get; set; } + + /// + /// 用户状态 + /// + [SugarColumn(ColumnDescription = "用户状态", Length = 32)] + public string? UserStatus { get; set; } + + /// + /// 学历 + /// + [SugarColumn(ColumnDescription = "学历", Length = 32)] + public string? Degree { get; set; } + + /// + /// 用户类型 + /// + [SugarColumn(ColumnDescription = "用户类型", Length = 32)] + public string? UserType { get; set; } + + /// + /// 邮编 + /// + [SugarColumn(ColumnDescription = "邮编", Length = 16)] + public string? Zip { get; set; } + + /// + /// 地址 + /// + [SugarColumn(ColumnDescription = "地址", Length = 256)] + public string? Address { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysAlipayTransaction.cs b/Admin.NET.Core/Entity/SysAlipayTransaction.cs new file mode 100644 index 0000000..5913471 --- /dev/null +++ b/Admin.NET.Core/Entity/SysAlipayTransaction.cs @@ -0,0 +1,108 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 支付宝交易记录表 +/// +[SugarTable(null, "支付宝交易记录表")] +[SysTable] +[SugarIndex("index_{table}_U", nameof(UserId), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(TradeNo), OrderByType.Asc)] +[SugarIndex("index_{table}_O", nameof(OutTradeNo), OrderByType.Asc)] +public class SysAlipayTransaction : EntityBase +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id", Length = 64)] + public long UserId { get; set; } + + /// + /// 交易号 + /// + [SugarColumn(ColumnDescription = "交易号", Length = 64)] + public string? TradeNo { get; set; } + + /// + /// 商户订单号 + /// + [SugarColumn(ColumnDescription = "商户订单号", Length = 64)] + public string OutTradeNo { get; set; } + + /// + /// 交易金额 + /// + [SugarColumn(ColumnDescription = "交易金额", Length = 20)] + public decimal TotalAmount { get; set; } + + /// + /// 交易状态 + /// + [SugarColumn(ColumnDescription = "交易状态", Length = 32)] + public string TradeStatus { get; set; } + + /// + /// 交易完成时间 + /// + [SugarColumn(ColumnDescription = "交易完成时间")] + public DateTime? FinishTime { get; set; } + + /// + /// 交易标题 + /// + [SugarColumn(ColumnDescription = "交易标题", Length = 256)] + public string Subject { get; set; } + + /// + /// 交易描述 + /// + [SugarColumn(ColumnDescription = "交易描述", Length = 512)] + public string? Body { get; set; } + + /// + /// 买家支付宝账号 + /// + [SugarColumn(ColumnDescription = "买家支付宝账号", Length = 128)] + public string? BuyerLogonId { get; set; } + + /// + /// 买家支付宝用户ID + /// + [SugarColumn(ColumnDescription = "买家支付宝用户ID", Length = 32)] + public string? BuyerUserId { get; set; } + + /// + /// 卖家支付宝用户ID + /// + [SugarColumn(ColumnDescription = "卖家支付宝用户ID", Length = 32)] + public string? SellerUserId { get; set; } + + /// + /// 商户AppId + /// + [SugarColumn(ColumnDescription = "商户AppId", Length = 64)] + public string? AppId { get; set; } + + /// + /// 交易扩展信息 + /// + [SugarColumn(ColumnDescription = "交易扩展信息", Length = 1024)] + public string? ExtendInfo { get; set; } + + /// + /// 交易异常信息 + /// + [SugarColumn(ColumnDescription = "交易扩展信息", Length = 1024)] + public string? ErrorInfo { get; set; } + + /// + /// 交易备注 + /// + [SugarColumn(ColumnDescription = "交易备注", Length = 512)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysCodeGen.cs b/Admin.NET.Core/Entity/SysCodeGen.cs new file mode 100644 index 0000000..c6d80a0 --- /dev/null +++ b/Admin.NET.Core/Entity/SysCodeGen.cs @@ -0,0 +1,159 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 代码生成表 +/// +[SugarTable(null, "代码生成表")] +[SysTable] +[SugarIndex("index_{table}_B", nameof(BusName), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(TableName), OrderByType.Asc)] +public partial class SysCodeGen : EntityBase +{ + /// + /// 作者姓名 + /// + [SugarColumn(ColumnDescription = "作者姓名", Length = 32)] + [MaxLength(32)] + public string? AuthorName { get; set; } + + /// + /// 是否移除表前缀 + /// + [SugarColumn(ColumnDescription = "是否移除表前缀", Length = 8)] + [MaxLength(8)] + public string? TablePrefix { get; set; } + + /// + /// 生成方式 + /// + [SugarColumn(ColumnDescription = "生成方式", Length = 32)] + [MaxLength(32)] + public string? GenerateType { get; set; } + + /// + /// 库定位器名 + /// + [SugarColumn(ColumnDescription = "库定位器名", Length = 64)] + [MaxLength(64)] + public string? ConfigId { get; set; } + + /// + /// 库名 + /// + [SugarColumn(IsIgnore = true)] + public string DbNickName + { + get + { + try + { + var dbOptions = App.GetConfig("DbConnection", true); + var config = dbOptions.ConnectionConfigs.FirstOrDefault(m => m.ConfigId.ToString() == ConfigId); + return config.DbNickName; + } + catch (Exception) + { + return null; + } + } + } + + /// + /// 数据库名(保留字段) + /// + [SugarColumn(ColumnDescription = "数据库库名", Length = 64)] + [MaxLength(64)] + public string? DbName { get; set; } + + /// + /// 数据库类型 + /// + [SugarColumn(ColumnDescription = "数据库类型", Length = 64)] + [MaxLength(64)] + public string? DbType { get; set; } + + /// + /// 数据库链接 + /// + [SugarColumn(ColumnDescription = "数据库链接", Length = 256)] + [MaxLength(256)] + public string? ConnectionString { get; set; } + + /// + /// 数据库表名 + /// + [SugarColumn(ColumnDescription = "数据库表名", Length = 128)] + [MaxLength(128)] + public string? TableName { get; set; } + + /// + /// 命名空间 + /// + [SugarColumn(ColumnDescription = "命名空间", Length = 128)] + [MaxLength(128)] + public string? NameSpace { get; set; } + + /// + /// 业务名 + /// + [SugarColumn(ColumnDescription = "业务名", Length = 128)] + [MaxLength(128)] + public string? BusName { get; set; } + + /// + /// 表唯一字段配置 + /// + [SugarColumn(ColumnDescription = "表唯一字段配置", Length = 512)] + [MaxLength(128)] + public string? TableUniqueConfig { get; set; } + + /// + /// 是否生成菜单 + /// + [SugarColumn(ColumnDescription = "是否生成菜单")] + public bool GenerateMenu { get; set; } = true; + + /// + /// 菜单图标 + /// + [SugarColumn(ColumnDescription = "菜单图标", Length = 32)] + public string? MenuIcon { get; set; } = "ele-Menu"; + + /// + /// 菜单编码 + /// + [SugarColumn(ColumnDescription = "菜单编码")] + public long? MenuPid { get; set; } + + /// + /// 页面目录 + /// + [SugarColumn(ColumnDescription = "页面目录", Length = 32)] + public string? PagePath { get; set; } + + /// + /// 支持打印类型 + /// + [SugarColumn(ColumnDescription = "支持打印类型", Length = 32)] + [MaxLength(32)] + public string? PrintType { get; set; } + + /// + /// 打印模版名称 + /// + [SugarColumn(ColumnDescription = "打印模版名称", Length = 32)] + [MaxLength(32)] + public string? PrintName { get; set; } + + /// + /// 表唯一字段列表 + /// + [SugarColumn(IsIgnore = true)] + public virtual List TableUniqueList => string.IsNullOrWhiteSpace(TableUniqueConfig) ? null : JSON.Deserialize>(TableUniqueConfig); +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysCodeGenConfig.cs b/Admin.NET.Core/Entity/SysCodeGenConfig.cs new file mode 100644 index 0000000..ff7697e --- /dev/null +++ b/Admin.NET.Core/Entity/SysCodeGenConfig.cs @@ -0,0 +1,207 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 代码生成字段配置表 +/// +[SugarTable(null, "代码生成字段配置表")] +[SysTable] +public partial class SysCodeGenConfig : EntityBase +{ + /// + /// 代码生成主表Id + /// + [SugarColumn(ColumnDescription = "主表Id")] + public long CodeGenId { get; set; } + + /// + /// 数据库字段名 + /// + [SugarColumn(ColumnDescription = "字段名称", Length = 128)] + [Required, MaxLength(128)] + public virtual string ColumnName { get; set; } + + /// + /// 主键 + /// + [SugarColumn(ColumnDescription = "主键", Length = 8)] + [MaxLength(8)] + public string? ColumnKey { get; set; } + + /// + /// 实体属性名 + /// + [SugarColumn(ColumnDescription = "属性名称", Length = 128)] + [Required, MaxLength(128)] + public virtual string PropertyName { get; set; } + + /// + /// 字段数据长度 + /// + [SugarColumn(ColumnDescription = "字段数据长度", DefaultValue = "0")] + public int ColumnLength { get; set; } + + /// + /// 字段描述 + /// + [SugarColumn(ColumnDescription = "字段描述", Length = 128)] + [MaxLength(128)] + public string? ColumnComment { get; set; } + + /// + /// 数据库中类型(物理类型) + /// + [SugarColumn(ColumnDescription = "数据库中类型", Length = 64)] + [MaxLength(64)] + public string? DataType { get; set; } + + /// + /// .NET数据类型 + /// + [SugarColumn(ColumnDescription = "NET数据类型", Length = 64)] + [MaxLength(64)] + public string? NetType { get; set; } + + /// + /// 字段数据默认值 + /// + [SugarColumn(ColumnDescription = "默认值")] + public string? DefaultValue { get; set; } + + /// + /// 作用类型(字典) + /// + [SugarColumn(ColumnDescription = "作用类型", Length = 64)] + [MaxLength(64)] + public string? EffectType { get; set; } + + /// + /// 外键库标识 + /// + [SugarColumn(ColumnDescription = "外键库标识", Length = 20)] + [MaxLength(20)] + public string? FkConfigId { get; set; } + + /// + /// 外键实体名称 + /// + [SugarColumn(ColumnDescription = "外键实体名称", Length = 64)] + [MaxLength(64)] + public string? FkEntityName { get; set; } + + /// + /// 外键表名称 + /// + [SugarColumn(ColumnDescription = "外键表名称", Length = 128)] + [MaxLength(128)] + public string? FkTableName { get; set; } + + /// + /// 外键显示字段 + /// + [SugarColumn(ColumnDescription = "外键显示字段", Length = 64)] + [MaxLength(64)] + public string? FkDisplayColumns { get; set; } + + /// + /// 外键链接字段 + /// + [SugarColumn(ColumnDescription = "外键链接字段", Length = 64)] + [MaxLength(64)] + public string? FkLinkColumnName { get; set; } + + /// + /// 外键显示字段.NET类型 + /// + [SugarColumn(ColumnDescription = "外键显示字段.NET类型", Length = 64)] + [MaxLength(64)] + public string? FkColumnNetType { get; set; } + + /// + /// 父级字段 + /// + [SugarColumn(ColumnDescription = "父级字段", Length = 128)] + [MaxLength(128)] + public string? PidColumn { get; set; } + + /// + /// 字典编码 + /// + [SugarColumn(ColumnDescription = "字典编码", Length = 64)] + [MaxLength(64)] + public string? DictTypeCode { get; set; } + + /// + /// 查询方式 + /// + [SugarColumn(ColumnDescription = "查询方式", Length = 16)] + [MaxLength(16)] + public string? QueryType { get; set; } + + /// + /// 是否是查询条件 + /// + [SugarColumn(ColumnDescription = "是否是查询条件", Length = 8)] + [MaxLength(8)] + public string? WhetherQuery { get; set; } + + /// + /// 列表是否缩进(字典) + /// + [SugarColumn(ColumnDescription = "列表是否缩进", Length = 8)] + [MaxLength(8)] + public string? WhetherRetract { get; set; } + + /// + /// 是否必填(字典) + /// + [SugarColumn(ColumnDescription = "是否必填", Length = 8)] + [MaxLength(8)] + public string? WhetherRequired { get; set; } + + /// + /// 是否可排序(字典) + /// + [SugarColumn(ColumnDescription = "是否可排序", Length = 8)] + [MaxLength(8)] + public string? WhetherSortable { get; set; } + + /// + /// 列表显示 + /// + [SugarColumn(ColumnDescription = "列表显示", Length = 8)] + [MaxLength(8)] + public string? WhetherTable { get; set; } + + /// + /// 增改 + /// + [SugarColumn(ColumnDescription = "增改", Length = 8)] + [MaxLength(8)] + public string? WhetherAddUpdate { get; set; } + + /// + /// 导入 + /// + [SugarColumn(ColumnDescription = "导入", Length = 8)] + [MaxLength(8)] + public string? WhetherImport { get; set; } + + /// + /// 是否通用字段 + /// + [SugarColumn(ColumnDescription = "是否通用字段", Length = 8)] + [MaxLength(8)] + public string? WhetherCommon { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysConfig.cs b/Admin.NET.Core/Entity/SysConfig.cs new file mode 100644 index 0000000..d5afeae --- /dev/null +++ b/Admin.NET.Core/Entity/SysConfig.cs @@ -0,0 +1,65 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统配置参数表 +/// +[SugarTable(null, "系统配置参数表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)] +public partial class SysConfig : EntityBase +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + [IgnoreUpdateSeedColumn] + public string? Value { get; set; } + + /// + /// 是否是内置参数(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是内置参数", DefaultValue = "1")] + public YesNoEnum SysFlag { get; set; } = YesNoEnum.Y; + + /// + /// 分组编码 + /// + [SugarColumn(ColumnDescription = "分组编码", Length = 64)] + [MaxLength(64)] + public string? GroupCode { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysDictData.cs b/Admin.NET.Core/Entity/SysDictData.cs new file mode 100644 index 0000000..735af86 --- /dev/null +++ b/Admin.NET.Core/Entity/SysDictData.cs @@ -0,0 +1,105 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统字典值表 +/// +[SugarTable(null, "系统字典值表")] +[SysTable] +[SugarIndex("index_{table}_TV", nameof(DictTypeId), OrderByType.Asc, nameof(Value), OrderByType.Asc, IsUnique = true)] +public partial class SysDictData : EntityBase +{ + /// + /// 字典类型Id + /// + [SugarColumn(ColumnDescription = "字典类型Id")] + public long DictTypeId { get; set; } + + /// + /// 字典类型 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(DictTypeId))] + public SysDictType DictType { get; set; } + + /// + /// 显示文本 + /// + [SugarColumn(ColumnDescription = "显示文本", Length = 256)] + [Required, MaxLength(256)] + public virtual string Label { get; set; } + + /// + /// 值 + /// + [SugarColumn(ColumnDescription = "值", Length = 256)] + [Required, MaxLength(256)] + public virtual string Value { get; set; } + + /// + /// 编码 + /// + /// + /// + [SugarColumn(ColumnDescription = "编码", Length = 256)] + public virtual string? Code { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 256)] + [MaxLength(256)] + public virtual string? Name { get; set; } + + /// + /// 显示样式-标签颜色 + /// + [SugarColumn(ColumnDescription = "显示样式-标签颜色", Length = 16)] + [MaxLength(16)] + public string? TagType { get; set; } + + /// + /// 显示样式-Style(控制显示样式) + /// + [SugarColumn(ColumnDescription = "显示样式-Style", Length = 512)] + [MaxLength(512)] + public string? StyleSetting { get; set; } + + /// + /// 显示样式-Class(控制显示样式) + /// + [SugarColumn(ColumnDescription = "显示样式-Class", Length = 512)] + [MaxLength(512)] + public string? ClassSetting { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 2048)] + [MaxLength(2048)] + public string? Remark { get; set; } + + /// + /// 拓展数据(保存业务功能的配置项) + /// + [SugarColumn(ColumnDescription = "拓展数据(保存业务功能的配置项)", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ExtData { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysDictDataTenant.cs b/Admin.NET.Core/Entity/SysDictDataTenant.cs new file mode 100644 index 0000000..d14eac7 --- /dev/null +++ b/Admin.NET.Core/Entity/SysDictDataTenant.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户字典值表 +/// +[SugarTable(null, "系统租户字典值表")] +[SysTable] +[SugarIndex("index_{table}_TV", nameof(DictTypeId), OrderByType.Asc, nameof(Value), OrderByType.Asc)] +public partial class SysDictDataTenant : SysDictData, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysDictType.cs b/Admin.NET.Core/Entity/SysDictType.cs new file mode 100644 index 0000000..b9d0f9f --- /dev/null +++ b/Admin.NET.Core/Entity/SysDictType.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统字典类型表 +/// +[SugarTable(null, "系统字典类型表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysDictType : EntityBase +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [Required, MaxLength(64)] + public virtual string Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 是否是内置字典(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是内置字典", DefaultValue = "1")] + public virtual YesNoEnum SysFlag { get; set; } = YesNoEnum.Y; + + /// + /// 是否是租户字典(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是租户字典", DefaultValue = "2")] + public virtual YesNoEnum IsTenant { get; set; } = YesNoEnum.N; + + /// + /// 字典值集合 + /// + [Navigate(NavigateType.OneToMany, nameof(SysDictData.DictTypeId))] + public List Children { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysFile.cs b/Admin.NET.Core/Entity/SysFile.cs new file mode 100644 index 0000000..27149d5 --- /dev/null +++ b/Admin.NET.Core/Entity/SysFile.cs @@ -0,0 +1,104 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统文件表 +/// +[SugarTable(null, "系统文件表")] +[SysTable] +[SugarIndex("index_{table}_F", nameof(FileName), OrderByType.Asc)] +public partial class SysFile : EntityBaseTenantOrg +{ + /// + /// 提供者 + /// + [SugarColumn(ColumnDescription = "提供者", Length = 128)] + [MaxLength(128)] + public string? Provider { get; set; } + + /// + /// 仓储名称 + /// + [SugarColumn(ColumnDescription = "仓储名称", Length = 128)] + [MaxLength(128)] + public string? BucketName { get; set; } + + /// + /// 文件名称(源文件名) + /// + [SugarColumn(ColumnDescription = "文件名称", Length = 128)] + [MaxLength(128)] + public string? FileName { get; set; } + + /// + /// 文件后缀 + /// + [SugarColumn(ColumnDescription = "文件后缀", Length = 16)] + [MaxLength(16)] + public string? Suffix { get; set; } + + /// + /// 存储路径 + /// + [SugarColumn(ColumnDescription = "存储路径", Length = 512)] + [MaxLength(512)] + public string? FilePath { get; set; } + + /// + /// 文件大小KB + /// + [SugarColumn(ColumnDescription = "文件大小KB")] + public long SizeKb { get; set; } + + /// + /// 文件大小信息-计算后的 + /// + [SugarColumn(ColumnDescription = "文件大小信息", Length = 64)] + [MaxLength(64)] + public string? SizeInfo { get; set; } + + /// + /// 外链地址-OSS上传后生成外链地址方便前端预览 + /// + [SugarColumn(ColumnDescription = "外链地址", Length = 512)] + [MaxLength(512)] + public string? Url { get; set; } + + /// + /// 文件MD5 + /// + [SugarColumn(ColumnDescription = "文件MD5", Length = 128)] + [MaxLength(128)] + public string? FileMd5 { get; set; } + + /// + /// 文件类别 + /// + [SugarColumn(ColumnDescription = "文件类别", Length = 128)] + [MaxLength(128)] + public virtual string? FileType { get; set; } + + /// + /// 文件别名 + /// + [SugarColumn(ColumnDescription = "文件别名", Length = 128)] + [MaxLength(128)] + public string? FileAlias { get; set; } + + /// + /// 是否公开 + /// + [SugarColumn(ColumnDescription = "是否公开")] + public virtual bool IsPublic { get; set; } = false; + + /// + /// 业务数据Id + /// + [SugarColumn(ColumnDescription = "业务数据Id")] + public long? DataId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysFileProvider.cs b/Admin.NET.Core/Entity/SysFileProvider.cs new file mode 100644 index 0000000..6fd9b54 --- /dev/null +++ b/Admin.NET.Core/Entity/SysFileProvider.cs @@ -0,0 +1,118 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core; + +/// +/// 系统文件存储提供者表 +/// +[SugarTable(null, "系统文件存储提供者表")] +[SysTable] +[SugarIndex("index_{table}_BucketName", nameof(BucketName), OrderByType.Asc)] +[SugarIndex("index_{table}_IsEnable", nameof(IsEnable), OrderByType.Desc)] +[SugarIndex("index_{table}_IsDefault", nameof(IsDefault), OrderByType.Desc)] +public partial class SysFileProvider : EntityBaseTenant +{ + /// + /// 存储提供者(Minio, QCloud,Aliyun 等等) + /// + [SugarColumn(ColumnDescription = "存储提供者", Length = 16)] + [Required, MaxLength(16)] + public virtual string Provider { get; set; } + + /// + /// 存储桶名称 + /// + [SugarColumn(ColumnDescription = "存储桶名称", Length = 32)] + [Required, MaxLength(32)] + public virtual string BucketName { get; set; } + + /// + /// 访问密钥 (填入 阿里云(Aliyun)/Minio:的 AccessKey,腾讯云(QCloud): 的 SecretId) + /// + [SugarColumn(ColumnDescription = "访问密钥", Length = 128)] + [MaxLength(128)] + public virtual string? AccessKey { get; set; } + + /// + /// 密钥 + /// + [SugarColumn(ColumnDescription = "密钥", Length = 128)] + [MaxLength(128)] + public virtual string? SecretKey { get; set; } + + /// + /// 地域 + /// + [SugarColumn(ColumnDescription = "地域", Length = 64)] + [MaxLength(64)] + public virtual string? Region { get; set; } + + /// + /// 端点地址(填入 阿里云(Aliyun)/Minio:的 endpoint/Api address,腾讯云(QCloud): 的 AppId) + /// + [SugarColumn(ColumnDescription = "端点地址", Length = 256)] + [MaxLength(256)] + public virtual string? Endpoint { get; set; } + + /// + /// 是否启用HTTPS + /// + [SugarColumn(ColumnDescription = "是否启用HTTPS")] + public virtual bool? IsEnableHttps { get; set; } = true; + + /// + /// 是否启用缓存 + /// + [SugarColumn(ColumnDescription = "是否启用缓存")] + public virtual bool? IsEnableCache { get; set; } = true; + + /// + /// 是否启用 + /// + [SugarColumn(ColumnDescription = "是否启用")] + public virtual bool? IsEnable { get; set; } = true; + + /// + /// 是否默认提供者 + /// + [SugarColumn(ColumnDescription = "是否默认提供者")] + public virtual bool? IsDefault { get; set; } = false; + + /// + /// 自定义域名 + /// + [SugarColumn(ColumnDescription = "自定义域名", Length = 256)] + [MaxLength(256)] + public virtual string? SinceDomain { get; set; } + + /// + /// 排序号 + /// + [SugarColumn(ColumnDescription = "排序号")] + public virtual int? OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 512)] + [MaxLength(512)] + public virtual string? Remark { get; set; } + + /// + /// 获取显示名称 + /// + [SugarColumn(IsIgnore = true)] + public virtual string DisplayName => $"{Provider}-{BucketName}"; + + /// + /// 获取配置键名 + /// + [SugarColumn(IsIgnore = true)] + public virtual string ConfigKey => $"{Provider}_{BucketName}_{Id}"; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysJobCluster.cs b/Admin.NET.Core/Entity/SysJobCluster.cs new file mode 100644 index 0000000..6687cf6 --- /dev/null +++ b/Admin.NET.Core/Entity/SysJobCluster.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统作业集群表 +/// +[SugarTable(null, "系统作业集群表")] +[SysTable] +public partial class SysJobCluster : EntityBaseId +{ + /// + /// 作业集群Id + /// + [SugarColumn(ColumnDescription = "作业集群Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string ClusterId { get; set; } + + /// + /// 描述信息 + /// + [SugarColumn(ColumnDescription = "描述信息", Length = 128)] + [MaxLength(128)] + public string? Description { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public ClusterStatus Status { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public DateTime? UpdatedTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysJobDetail.cs b/Admin.NET.Core/Entity/SysJobDetail.cs new file mode 100644 index 0000000..3c2868f --- /dev/null +++ b/Admin.NET.Core/Entity/SysJobDetail.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统作业信息表 +/// +[SugarTable(null, "系统作业信息表")] +[SysTable] +[SugarIndex("index_{table}_J", nameof(JobId), OrderByType.Asc)] +public partial class SysJobDetail : EntityBaseId +{ + /// + /// 作业Id + /// + [SugarColumn(ColumnDescription = "作业Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string JobId { get; set; } + + /// + /// 组名称 + /// + [SugarColumn(ColumnDescription = "组名称", Length = 128)] + [MaxLength(128)] + public string? GroupName { get; set; } = "default"; + + /// + /// 作业类型FullName + /// + [SugarColumn(ColumnDescription = "作业类型", Length = 128)] + [MaxLength(128)] + public string? JobType { get; set; } + + /// + /// 程序集Name + /// + [SugarColumn(ColumnDescription = "程序集", Length = 128)] + [MaxLength(128)] + public string? AssemblyName { get; set; } + + /// + /// 描述信息 + /// + [SugarColumn(ColumnDescription = "描述信息", Length = 128)] + [MaxLength(128)] + public string? Description { get; set; } + + /// + /// 是否并行执行 + /// + [SugarColumn(ColumnDescription = "是否并行执行")] + public bool Concurrent { get; set; } = true; + + /// + /// 是否扫描特性触发器 + /// + [SugarColumn(ColumnDescription = "是否扫描特性触发器", ColumnName = "annotation")] + public bool IncludeAnnotation { get; set; } = false; + + /// + /// 额外数据 + /// + [SugarColumn(ColumnDescription = "额外数据", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Properties { get; set; } = "{}"; + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public DateTime? UpdatedTime { get; set; } + + /// + /// 作业创建类型 + /// + [SugarColumn(ColumnDescription = "作业创建类型")] + public JobCreateTypeEnum CreateType { get; set; } = JobCreateTypeEnum.BuiltIn; + + /// + /// 脚本代码 + /// + [SugarColumn(ColumnDescription = "脚本代码", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ScriptCode { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysJobTrigger.cs b/Admin.NET.Core/Entity/SysJobTrigger.cs new file mode 100644 index 0000000..dc6d5c8 --- /dev/null +++ b/Admin.NET.Core/Entity/SysJobTrigger.cs @@ -0,0 +1,147 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统作业触发器表 +/// +[SugarTable(null, "系统作业触发器表")] +[SysTable] +public partial class SysJobTrigger : EntityBaseId +{ + /// + /// 触发器Id + /// + [SugarColumn(ColumnDescription = "触发器Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string TriggerId { get; set; } + + /// + /// 作业Id + /// + [SugarColumn(ColumnDescription = "作业Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string JobId { get; set; } + + /// + /// 触发器类型FullName + /// + [SugarColumn(ColumnDescription = "触发器类型", Length = 128)] + [MaxLength(128)] + public string? TriggerType { get; set; } + + /// + /// 程序集Name + /// + [SugarColumn(ColumnDescription = "程序集", Length = 128)] + [MaxLength(128)] + public string? AssemblyName { get; set; } = "Furion.Pure"; + + /// + /// 参数 + /// + [SugarColumn(ColumnDescription = "参数", Length = 128)] + [MaxLength(128)] + public string? Args { get; set; } + + /// + /// 描述信息 + /// + [SugarColumn(ColumnDescription = "描述信息", Length = 128)] + [MaxLength(128)] + public string? Description { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public TriggerStatus Status { get; set; } = TriggerStatus.Ready; + + /// + /// 起始时间 + /// + [SugarColumn(ColumnDescription = "起始时间")] + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + [SugarColumn(ColumnDescription = "结束时间")] + public DateTime? EndTime { get; set; } + + /// + /// 最近运行时间 + /// + [SugarColumn(ColumnDescription = "最近运行时间")] + public DateTime? LastRunTime { get; set; } + + /// + /// 下一次运行时间 + /// + [SugarColumn(ColumnDescription = "下一次运行时间")] + public DateTime? NextRunTime { get; set; } + + /// + /// 触发次数 + /// + [SugarColumn(ColumnDescription = "触发次数")] + public long NumberOfRuns { get; set; } + + /// + /// 最大触发次数(0:不限制,n:N次) + /// + [SugarColumn(ColumnDescription = "最大触发次数")] + public long MaxNumberOfRuns { get; set; } + + /// + /// 出错次数 + /// + [SugarColumn(ColumnDescription = "出错次数")] + public long NumberOfErrors { get; set; } + + /// + /// 最大出错次数(0:不限制,n:N次) + /// + [SugarColumn(ColumnDescription = "最大出错次数")] + public long MaxNumberOfErrors { get; set; } + + /// + /// 重试次数 + /// + [SugarColumn(ColumnDescription = "重试次数")] + public int NumRetries { get; set; } + + /// + /// 重试间隔时间(ms) + /// + [SugarColumn(ColumnDescription = "重试间隔时间(ms)")] + public int RetryTimeout { get; set; } = 1000; + + /// + /// 是否立即启动 + /// + [SugarColumn(ColumnDescription = "是否立即启动")] + public bool StartNow { get; set; } = true; + + /// + /// 是否启动时执行一次 + /// + [SugarColumn(ColumnDescription = "是否启动时执行一次")] + public bool RunOnStart { get; set; } = false; + + /// + /// 是否在启动时重置最大触发次数等于一次的作业 + /// + [SugarColumn(ColumnDescription = "是否重置触发次数")] + public bool ResetOnlyOnce { get; set; } = true; + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public DateTime? UpdatedTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysJobTriggerRecord.cs b/Admin.NET.Core/Entity/SysJobTriggerRecord.cs new file mode 100644 index 0000000..080c5cc --- /dev/null +++ b/Admin.NET.Core/Entity/SysJobTriggerRecord.cs @@ -0,0 +1,72 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统作业触发器运行记录表 +/// +[SugarTable(null, "系统作业触发器运行记录表")] +[SysTable] +public partial class SysJobTriggerRecord : EntityBaseId +{ + /// + /// 作业Id + /// + [SugarColumn(ColumnDescription = "作业Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string JobId { get; set; } + + /// + /// 触发器Id + /// + [SugarColumn(ColumnDescription = "触发器Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string TriggerId { get; set; } + + /// + /// 当前运行次数 + /// + [SugarColumn(ColumnDescription = "当前运行次数")] + public long NumberOfRuns { get; set; } + + /// + /// 最近运行时间 + /// + [SugarColumn(ColumnDescription = "最近运行时间")] + public DateTime? LastRunTime { get; set; } + + /// + /// 下一次运行时间 + /// + [SugarColumn(ColumnDescription = "下一次运行时间")] + public DateTime? NextRunTime { get; set; } + + /// + /// 触发器状态 + /// + [SugarColumn(ColumnDescription = "触发器状态")] + public TriggerStatus Status { get; set; } = TriggerStatus.Ready; + + /// + /// 本次执行结果 + /// + [SugarColumn(ColumnDescription = "本次执行结果", Length = 128)] + [MaxLength(128)] + public string? Result { get; set; } + + /// + /// 本次执行耗时 + /// + [SugarColumn(ColumnDescription = "本次执行耗时")] + public long ElapsedTime { get; set; } + + /// + /// 创建时间 + /// + [SugarColumn(ColumnDescription = "创建时间")] + public DateTime? CreatedTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysLang.cs b/Admin.NET.Core/Entity/SysLang.cs new file mode 100644 index 0000000..8be0706 --- /dev/null +++ b/Admin.NET.Core/Entity/SysLang.cs @@ -0,0 +1,86 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +[SugarTable(null, "语言配置")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public class SysLang : EntityBase +{ + /// + /// 语言名称 + /// + [SugarColumn(ColumnDescription = "语言名称")] + public string Name { get; set; } + + /// + /// 语言代码(如 zh-CN) + /// + [SugarColumn(ColumnDescription = "语言代码")] + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [SugarColumn(ColumnDescription = "ISO 语言代码")] + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [SugarColumn(ColumnDescription = "URL 语言代码")] + public string UrlCode { get; set; } + + /// + /// 书写方向(1=从左到右,2=从右到左) + /// + [SugarColumn(ColumnDescription = "书写方向", DefaultValue = "1")] + public DirectionEnum Direction { get; set; } = DirectionEnum.Ltr; + + /// + /// 日期格式(如 YYYY-MM-DD) + /// + [SugarColumn(ColumnDescription = "日期格式")] + public string DateFormat { get; set; } + + /// + /// 时间格式(如 HH:MM:SS) + /// + [SugarColumn(ColumnDescription = "时间格式")] + public string TimeFormat { get; set; } + + /// + /// 每周起始日(如 0=星期日,1=星期一) + /// + [SugarColumn(ColumnDescription = "每周起始日", DefaultValue = "7")] + public WeekEnum WeekStart { get; set; } = WeekEnum.Sunday; + + /// + /// 分组符号(如 ,) + /// + [SugarColumn(ColumnDescription = "分组符号")] + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [SugarColumn(ColumnDescription = "小数点符号")] + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + [SugarColumn(ColumnDescription = "千分位分隔符")] + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [SugarColumn(ColumnDescription = "是否启用")] + public bool Active { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysLangText.cs b/Admin.NET.Core/Entity/SysLangText.cs new file mode 100644 index 0000000..f1258cf --- /dev/null +++ b/Admin.NET.Core/Entity/SysLangText.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +[SugarTable(null, "翻译表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(EntityName), OrderByType.Asc)] +[SugarIndex("index_{table}_F", nameof(FieldName), OrderByType.Asc)] +public class SysLangText : EntityBase +{ + /// + /// 所属实体名 + /// + [SugarColumn(ColumnDescription = "所属实体名")] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [SugarColumn(ColumnDescription = "所属实体ID")] + public long EntityId { get; set; } + + /// + /// 字段名 + /// + [SugarColumn(ColumnDescription = "字段名")] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + [SugarColumn(ColumnDescription = "语言代码")] + public string LangCode { get; set; } + + /// + /// 多语言内容 + /// + [SugarColumn(ColumnDescription = "翻译内容")] + public string Content { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysLdap.cs b/Admin.NET.Core/Entity/SysLdap.cs new file mode 100644 index 0000000..f27d1aa --- /dev/null +++ b/Admin.NET.Core/Entity/SysLdap.cs @@ -0,0 +1,89 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统域登录信息配置表 +/// +[SugarTable(null, "系统域登录信息配置表")] +[SysTable] +public class SysLdap : EntityBaseTenantDel +{ + /// + /// 主机 + /// + [SugarColumn(ColumnDescription = "主机", Length = 128)] + [Required] + public virtual string Host { get; set; } + + /// + /// 端口 + /// + [SugarColumn(ColumnDescription = "端口")] + public virtual int Port { get; set; } + + /// + /// 用户搜索基准 + /// + [SugarColumn(ColumnDescription = "用户搜索基准", Length = 128)] + [Required] + public virtual string BaseDn { get; set; } + + /// + /// 绑定DN(有管理权限制的用户) + /// + [SugarColumn(ColumnDescription = "绑定DN", Length = 128)] + [Required] + public virtual string BindDn { get; set; } + + /// + /// 绑定密码(有管理权限制的用户密码) + /// + [SugarColumn(ColumnDescription = "绑定密码", Length = 512)] + [Required] + public virtual string BindPass { get; set; } + + /// + /// 用户过滤规则 + /// + [SugarColumn(ColumnDescription = "用户过滤规则", Length = 128)] + [Required] + public virtual string AuthFilter { get; set; } = "sAMAccountName=%s"; + + /// + /// Ldap版本 + /// + [SugarColumn(ColumnDescription = "Ldap版本")] + public int Version { get; set; } + + /// + /// 绑定域账号字段属性值 + /// + [SugarColumn(ColumnDescription = "绑定域账号字段属性值", Length = 32)] + [Required] + public virtual string BindAttrAccount { get; set; } = "sAMAccountName"; + + /// + /// 绑定用户EmployeeId属性值 + /// + [SugarColumn(ColumnDescription = "绑定用户EmployeeId属性值", Length = 32)] + [Required] + public virtual string BindAttrEmployeeId { get; set; } = "EmployeeId"; + + /// + /// 绑定Code属性值 + /// + [SugarColumn(ColumnDescription = "绑定对象Code属性值", Length = 64)] + [Required] + public virtual string BindAttrCode { get; set; } = "objectGUID"; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysLogDiff.cs b/Admin.NET.Core/Entity/SysLogDiff.cs new file mode 100644 index 0000000..96366b7 --- /dev/null +++ b/Admin.NET.Core/Entity/SysLogDiff.cs @@ -0,0 +1,52 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统差异日志表 +/// +[SugarTable(null, "系统差异日志表")] +[SysTable] +[LogTable] +public partial class SysLogDiff : EntityBaseTenant +{ + /// + /// 差异数据 + /// + [SugarColumn(ColumnDescription = "差异数据", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? DiffData { get; set; } + + /// + /// Sql + /// + [SugarColumn(ColumnDescription = "Sql", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Sql { get; set; } + + /// + /// 参数 手动传入的参数 + /// + [SugarColumn(ColumnDescription = "参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Parameters { get; set; } + + /// + /// 业务对象 + /// + [SugarColumn(ColumnDescription = "业务对象", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? BusinessData { get; set; } + + /// + /// 差异操作 + /// + [SugarColumn(ColumnDescription = "差异操作", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? DiffType { get; set; } + + /// + /// 耗时 + /// + [SugarColumn(ColumnDescription = "耗时")] + public long? Elapsed { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysLogEx.cs b/Admin.NET.Core/Entity/SysLogEx.cs new file mode 100644 index 0000000..e650144 --- /dev/null +++ b/Admin.NET.Core/Entity/SysLogEx.cs @@ -0,0 +1,72 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统异常日志表 +/// +[SugarTable(null, "系统异常日志表")] +[SysTable] +[LogTable] +public partial class SysLogEx : SysLogVis +{ + /// + /// 请求方式 + /// + [SugarColumn(ColumnDescription = "请求方式", Length = 32)] + [MaxLength(32)] + public string? HttpMethod { get; set; } + + /// + /// 请求地址 + /// + [SugarColumn(ColumnDescription = "请求地址", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestUrl { get; set; } + + /// + /// 请求参数 + /// + [SugarColumn(ColumnDescription = "请求参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestParam { get; set; } + + /// + /// 返回结果 + /// + [SugarColumn(ColumnDescription = "返回结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ReturnResult { get; set; } + + /// + /// 事件Id + /// + [SugarColumn(ColumnDescription = "事件Id")] + public int? EventId { get; set; } + + /// + /// 线程Id + /// + [SugarColumn(ColumnDescription = "线程Id")] + public int? ThreadId { get; set; } + + /// + /// 请求跟踪Id + /// + [SugarColumn(ColumnDescription = "请求跟踪Id", Length = 128)] + [MaxLength(128)] + public string? TraceId { get; set; } + + /// + /// 异常信息 + /// + [SugarColumn(ColumnDescription = "异常信息", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Exception { get; set; } + + /// + /// 日志消息Json + /// + [SugarColumn(ColumnDescription = "日志消息Json", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Message { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysLogOp.cs b/Admin.NET.Core/Entity/SysLogOp.cs new file mode 100644 index 0000000..e5ba1c1 --- /dev/null +++ b/Admin.NET.Core/Entity/SysLogOp.cs @@ -0,0 +1,72 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统操作日志表 +/// +[SugarTable(null, "系统操作日志表")] +[SysTable] +[LogTable] +public partial class SysLogOp : SysLogVis +{ + /// + /// 请求方式 + /// + [SugarColumn(ColumnDescription = "请求方式", Length = 32)] + [MaxLength(32)] + public string? HttpMethod { get; set; } + + /// + /// 请求地址 + /// + [SugarColumn(ColumnDescription = "请求地址", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestUrl { get; set; } + + /// + /// 请求参数 + /// + [SugarColumn(ColumnDescription = "请求参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestParam { get; set; } + + /// + /// 返回结果 + /// + [SugarColumn(ColumnDescription = "返回结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ReturnResult { get; set; } + + /// + /// 事件Id + /// + [SugarColumn(ColumnDescription = "事件Id")] + public int? EventId { get; set; } + + /// + /// 线程Id + /// + [SugarColumn(ColumnDescription = "线程Id")] + public int? ThreadId { get; set; } + + /// + /// 请求跟踪Id + /// + [SugarColumn(ColumnDescription = "请求跟踪Id", Length = 128)] + [MaxLength(128)] + public string? TraceId { get; set; } + + /// + /// 异常信息 + /// + [SugarColumn(ColumnDescription = "异常信息", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Exception { get; set; } + + /// + /// 日志消息Json + /// + [SugarColumn(ColumnDescription = "日志消息Json", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Message { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysLogVis.cs b/Admin.NET.Core/Entity/SysLogVis.cs new file mode 100644 index 0000000..6f269b5 --- /dev/null +++ b/Admin.NET.Core/Entity/SysLogVis.cs @@ -0,0 +1,116 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统访问日志表 +/// +[SugarTable(null, "系统访问日志表")] +[SysTable] +[LogTable] +public partial class SysLogVis : EntityBaseTenant +{ + /// + /// 模块名称 + /// + [SugarColumn(ColumnDescription = "模块名称", Length = 256)] + [MaxLength(256)] + public string? ControllerName { get; set; } + + /// + /// 方法名称 + /// + [SugarColumn(ColumnDescription = "方法名称", Length = 256)] + [MaxLength(256)] + public string? ActionName { get; set; } + + /// + /// 显示名称 + /// + [SugarColumn(ColumnDescription = "显示名称", Length = 256)] + [MaxLength(256)] + public string? DisplayTitle { get; set; } + + /// + /// 执行状态 + /// + [SugarColumn(ColumnDescription = "执行状态", Length = 32)] + [MaxLength(32)] + public string? Status { get; set; } + + /// + /// IP地址 + /// + [SugarColumn(ColumnDescription = "IP地址", Length = 256)] + [MaxLength(256)] + public string? RemoteIp { get; set; } + + /// + /// 登录地点 + /// + [SugarColumn(ColumnDescription = "登录地点", Length = 128)] + [MaxLength(128)] + public string? Location { get; set; } + + /// + /// 经度 + /// + [SugarColumn(ColumnDescription = "经度")] + public decimal? Longitude { get; set; } + + /// + /// 维度 + /// + [SugarColumn(ColumnDescription = "维度")] + public decimal? Latitude { get; set; } + + /// + /// 浏览器 + /// + [SugarColumn(ColumnDescription = "浏览器", Length = 1024)] + [MaxLength(1024)] + public string? Browser { get; set; } + + /// + /// 操作系统 + /// + [SugarColumn(ColumnDescription = "操作系统", Length = 256)] + [MaxLength(256)] + public string? Os { get; set; } + + /// + /// 操作用时 + /// + [SugarColumn(ColumnDescription = "操作用时")] + public long? Elapsed { get; set; } + + /// + /// 日志时间 + /// + [SugarColumn(ColumnDescription = "日志时间")] + public DateTime? LogDateTime { get; set; } + + /// + /// 日志级别 + /// + [SugarColumn(ColumnDescription = "日志级别")] + public LogLevel? LogLevel { get; set; } + + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [MaxLength(32)] + public string? Account { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public string? RealName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysMenu.cs b/Admin.NET.Core/Entity/SysMenu.cs new file mode 100644 index 0000000..a89a776 --- /dev/null +++ b/Admin.NET.Core/Entity/SysMenu.cs @@ -0,0 +1,134 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统菜单表 +/// +[SugarTable(null, "系统菜单表")] +[SysTable] +[SugarIndex("index_{table}_T", nameof(Title), OrderByType.Asc)] +[SugarIndex("index_{table}_T2", nameof(Type), OrderByType.Asc)] +public partial class SysMenu : EntityBase +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 菜单类型(1目录 2菜单 3按钮) + /// + [SugarColumn(ColumnDescription = "菜单类型")] + public MenuTypeEnum Type { get; set; } + + /// + /// 路由名称 + /// + [SugarColumn(ColumnDescription = "路由名称", Length = 64)] + [MaxLength(64)] + public string? Name { get; set; } + + /// + /// 路由地址 + /// + [SugarColumn(ColumnDescription = "路由地址", Length = 128)] + [MaxLength(128)] + public string? Path { get; set; } + + /// + /// 组件路径 + /// + [SugarColumn(ColumnDescription = "组件路径", Length = 128)] + [MaxLength(128)] + public string? Component { get; set; } + + /// + /// 重定向 + /// + [SugarColumn(ColumnDescription = "重定向", Length = 128)] + [MaxLength(128)] + public string? Redirect { get; set; } + + /// + /// 权限标识 + /// + [SugarColumn(ColumnDescription = "权限标识", Length = 128)] + [MaxLength(128)] + public string? Permission { get; set; } + + /// + /// 菜单名称 + /// + [SugarColumn(ColumnDescription = "菜单名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Title { get; set; } + + /// + /// 图标 + /// + [SugarColumn(ColumnDescription = "图标", Length = 128)] + [MaxLength(128)] + public string? Icon { get; set; } = "ele-Menu"; + + /// + /// 是否内嵌 + /// + [SugarColumn(ColumnDescription = "是否内嵌")] + public bool IsIframe { get; set; } + + /// + /// 外链链接 + /// + [SugarColumn(ColumnDescription = "外链链接", Length = 256)] + [MaxLength(256)] + public string? OutLink { get; set; } + + /// + /// 是否隐藏 + /// + [SugarColumn(ColumnDescription = "是否隐藏")] + public bool IsHide { get; set; } + + /// + /// 是否缓存 + /// + [SugarColumn(ColumnDescription = "是否缓存")] + public bool IsKeepAlive { get; set; } = true; + + /// + /// 是否固定 + /// + [SugarColumn(ColumnDescription = "是否固定")] + public bool IsAffix { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 菜单子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } = new(); +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysNotice.cs b/Admin.NET.Core/Entity/SysNotice.cs new file mode 100644 index 0000000..7aa7d01 --- /dev/null +++ b/Admin.NET.Core/Entity/SysNotice.cs @@ -0,0 +1,82 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统通知公告表 +/// +[SugarTable(null, "系统通知公告表")] +[SysTable] +[SugarIndex("index_{table}_T", nameof(Type), OrderByType.Asc)] +public partial class SysNotice : EntityBase +{ + /// + /// 标题 + /// + [SugarColumn(ColumnDescription = "标题", Length = 32)] + [Required, MaxLength(32)] + [SensitiveDetection('*')] + public virtual string Title { get; set; } + + /// + /// 内容 + /// + [SugarColumn(ColumnDescription = "内容", ColumnDataType = StaticConfig.CodeFirst_BigString)] + [Required] + [SensitiveDetection('*')] + public virtual string Content { get; set; } + + /// + /// 类型(1通知 2公告) + /// + [SugarColumn(ColumnDescription = "类型(1通知 2公告)")] + public NoticeTypeEnum Type { get; set; } + + /// + /// 发布人Id + /// + [SugarColumn(ColumnDescription = "发布人Id")] + public long PublicUserId { get; set; } + + /// + /// 发布人姓名 + /// + [SugarColumn(ColumnDescription = "发布人姓名", Length = 32)] + [MaxLength(32)] + public string? PublicUserName { get; set; } + + /// + /// 发布机构Id + /// + [SugarColumn(ColumnDescription = "发布机构Id")] + public long PublicOrgId { get; set; } + + /// + /// 发布机构名称 + /// + [SugarColumn(ColumnDescription = "发布机构名称", Length = 64)] + [MaxLength(64)] + public string? PublicOrgName { get; set; } + + /// + /// 发布时间 + /// + [SugarColumn(ColumnDescription = "发布时间")] + public DateTime? PublicTime { get; set; } + + /// + /// 撤回时间 + /// + [SugarColumn(ColumnDescription = "撤回时间")] + public DateTime? CancelTime { get; set; } + + /// + /// 状态(0草稿 1发布 2撤回 3删除) + /// + [SugarColumn(ColumnDescription = "状态(0草稿 1发布 2撤回 3删除)")] + public NoticeStatusEnum Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysNoticeUser.cs b/Admin.NET.Core/Entity/SysNoticeUser.cs new file mode 100644 index 0000000..5f71ba4 --- /dev/null +++ b/Admin.NET.Core/Entity/SysNoticeUser.cs @@ -0,0 +1,45 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统通知公告用户表 +/// +[SugarTable(null, "系统通知公告用户表")] +[SysTable] +public partial class SysNoticeUser : EntityBaseId +{ + /// + /// 通知公告Id + /// + [SugarColumn(ColumnDescription = "通知公告Id")] + public long NoticeId { get; set; } + + /// + /// 通知公告 + /// + [Navigate(NavigateType.OneToOne, nameof(NoticeId))] + public SysNotice SysNotice { get; set; } + + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 阅读时间 + /// + [SugarColumn(ColumnDescription = "阅读时间")] + public DateTime? ReadTime { get; set; } + + /// + /// 状态(0未读 1已读) + /// + [SugarColumn(ColumnDescription = "状态(0未读 1已读)")] + public NoticeUserStatusEnum ReadStatus { get; set; } = NoticeUserStatusEnum.UNREAD; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysOnlineUser.cs b/Admin.NET.Core/Entity/SysOnlineUser.cs new file mode 100644 index 0000000..d4eb387 --- /dev/null +++ b/Admin.NET.Core/Entity/SysOnlineUser.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统在线用户表 +/// +[SugarTable(null, "系统在线用户表")] +[SysTable] +public partial class SysOnlineUser : EntityBaseTenantId +{ + /// + /// 连接Id + /// + [SugarColumn(ColumnDescription = "连接Id")] + public string? ConnectionId { get; set; } + + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [Required, MaxLength(32)] + public virtual string UserName { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public string? RealName { get; set; } + + /// + /// 连接时间 + /// + [SugarColumn(ColumnDescription = "连接时间")] + public DateTime? Time { get; set; } + + /// + /// 连接IP + /// + [SugarColumn(ColumnDescription = "连接IP", Length = 256)] + [MaxLength(256)] + public string? Ip { get; set; } + + /// + /// 浏览器 + /// + [SugarColumn(ColumnDescription = "浏览器", Length = 128)] + [MaxLength(128)] + public string? Browser { get; set; } + + /// + /// 操作系统 + /// + [SugarColumn(ColumnDescription = "操作系统", Length = 128)] + [MaxLength(128)] + public string? Os { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysOpenAccess.cs b/Admin.NET.Core/Entity/SysOpenAccess.cs new file mode 100644 index 0000000..8680426 --- /dev/null +++ b/Admin.NET.Core/Entity/SysOpenAccess.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 开放接口身份表 +/// +[SugarTable(null, "开放接口身份表")] +[SysTable] +[SugarIndex("index_{table}_A", nameof(AccessKey), OrderByType.Asc)] +public partial class SysOpenAccess : EntityBase +{ + /// + /// 身份标识 + /// + [SugarColumn(ColumnDescription = "身份标识", Length = 128)] + [Required, MaxLength(128)] + public virtual string AccessKey { get; set; } + + /// + /// 密钥 + /// + [SugarColumn(ColumnDescription = "密钥", Length = 256)] + [Required, MaxLength(256)] + public virtual string AccessSecret { get; set; } + + /// + /// 绑定租户Id + /// + [SugarColumn(ColumnDescription = "绑定租户Id")] + public long BindTenantId { get; set; } + + /// + /// 绑定租户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(BindTenantId))] + public SysTenant BindTenant { get; set; } + + /// + /// 绑定用户Id + /// + [SugarColumn(ColumnDescription = "绑定用户Id")] + public virtual long BindUserId { get; set; } + + /// + /// 绑定用户 + /// + [Navigate(NavigateType.OneToOne, nameof(BindUserId))] + public SysUser BindUser { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysOrg.cs b/Admin.NET.Core/Entity/SysOrg.cs new file mode 100644 index 0000000..d64a8a3 --- /dev/null +++ b/Admin.NET.Core/Entity/SysOrg.cs @@ -0,0 +1,96 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统机构表 +/// +[SugarTable(null, "系统机构表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(Type), OrderByType.Asc)] +public partial class SysOrg : EntityBaseTenant +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 级别 + /// + [SugarColumn(ColumnDescription = "级别")] + public int? Level { get; set; } + + /// + /// 机构类型-数据字典 + /// + [SugarColumn(ColumnDescription = "机构类型", Length = 64)] + [MaxLength(64)] + public virtual string? Type { get; set; } + + /// + /// 负责人Id + /// + [SugarColumn(ColumnDescription = "负责人Id", IsNullable = true)] + public long? DirectorId { get; set; } + + /// + /// 负责人 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(DirectorId))] + public SysUser Director { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 机构子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } + + /// + /// 是否禁止选中 + /// + [SugarColumn(IsIgnore = true)] + public bool Disabled { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysPlugin.cs b/Admin.NET.Core/Entity/SysPlugin.cs new file mode 100644 index 0000000..86594dd --- /dev/null +++ b/Admin.NET.Core/Entity/SysPlugin.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统动态插件表 +/// +[SugarTable(null, "系统动态插件表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +public partial class SysPlugin : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// C#代码 + /// + [SugarColumn(ColumnDescription = "C#代码", ColumnDataType = StaticConfig.CodeFirst_BigString)] + [Required] + public virtual string CsharpCode { get; set; } + + /// + /// 程序集名称 + /// + [SugarColumn(ColumnDescription = "程序集名称", Length = 512)] + [MaxLength(512)] + public string? AssemblyName { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysPos.cs b/Admin.NET.Core/Entity/SysPos.cs new file mode 100644 index 0000000..7045eb8 --- /dev/null +++ b/Admin.NET.Core/Entity/SysPos.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统职位表 +/// +[SugarTable(null, "系统职位表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysPos : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 在职人员 + /// + [SugarColumn(IsIgnore = true)] + public List UserList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysPrint.cs b/Admin.NET.Core/Entity/SysPrint.cs new file mode 100644 index 0000000..269d42a --- /dev/null +++ b/Admin.NET.Core/Entity/SysPrint.cs @@ -0,0 +1,75 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统打印模板表 +/// +[SugarTable(null, "系统打印模板表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +public partial class SysPrint : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 打印模板 + /// + [SugarColumn(ColumnDescription = "打印模板", ColumnDataType = StaticConfig.CodeFirst_BigString)] + [Required] + public virtual string Template { get; set; } + + /// + /// 打印类型 + /// + [SugarColumn(ColumnDescription = "打印类型")] + [Required] + public virtual PrintTypeEnum? PrintType { get; set; } + + /// + /// 客户端服务地址 + /// + [SugarColumn(ColumnDescription = "客户端服务地址", Length = 128)] + [MaxLength(128)] + public virtual string? ClientServiceAddress { get; set; } + + /// + /// 打印参数 + /// + [SugarColumn(ColumnDescription = "打印参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string? PrintParam { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 打印预览测试数据 + /// + [SugarColumn(ColumnDescription = "打印预览测试数据", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? PrintDataDemo { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysRegion.cs b/Admin.NET.Core/Entity/SysRegion.cs new file mode 100644 index 0000000..7ada329 --- /dev/null +++ b/Admin.NET.Core/Entity/SysRegion.cs @@ -0,0 +1,109 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统行政地区表 +/// +[SugarTable(null, "系统行政地区表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)] +public partial class SysRegion : EntityBaseId +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 128)] + [Required, MaxLength(128)] + public virtual string Name { get; set; } + + /// + /// 简称 + /// + [SugarColumn(ColumnDescription = "简称", Length = 32)] + [MaxLength(32)] + public string? ShortName { get; set; } + + /// + /// 组合名 + /// + [SugarColumn(ColumnDescription = "组合名", Length = 64)] + [MaxLength(64)] + public string? MergerName { get; set; } + + /// + /// 行政代码 + /// + [SugarColumn(ColumnDescription = "行政代码", Length = 32)] + [MaxLength(32)] + public string? Code { get; set; } + + /// + /// 邮政编码 + /// + [SugarColumn(ColumnDescription = "邮政编码", Length = 6)] + [MaxLength(6)] + public string? ZipCode { get; set; } + + /// + /// 区号 + /// + [SugarColumn(ColumnDescription = "区号", Length = 6)] + [MaxLength(6)] + public string? CityCode { get; set; } + + /// + /// 层级 + /// + [SugarColumn(ColumnDescription = "层级")] + public int Level { get; set; } + + /// + /// 拼音 + /// + [SugarColumn(ColumnDescription = "拼音", Length = 128)] + [MaxLength(128)] + public string? PinYin { get; set; } + + /// + /// 经度 + /// + [SugarColumn(ColumnDescription = "经度")] + public float Lng { get; set; } + + /// + /// 维度 + /// + [SugarColumn(ColumnDescription = "维度")] + public float Lat { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 机构子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysRole.cs b/Admin.NET.Core/Entity/SysRole.cs new file mode 100644 index 0000000..2f0eae4 --- /dev/null +++ b/Admin.NET.Core/Entity/SysRole.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色表 +/// +[SugarTable(null, "系统角色表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysRole : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 数据范围(1全部数据 2本部门及以下数据 3本部门数据 4仅本人数据 5自定义数据) + /// + [SugarColumn(ColumnDescription = "数据范围")] + public DataScopeEnum DataScope { get; set; } = DataScopeEnum.Self; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysRoleMenu.cs b/Admin.NET.Core/Entity/SysRoleMenu.cs new file mode 100644 index 0000000..04e3f1d --- /dev/null +++ b/Admin.NET.Core/Entity/SysRoleMenu.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色菜单表 +/// +[SugarTable(null, "系统角色菜单表")] +[SysTable] +public class SysRoleMenu : EntityBaseId +{ + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } + + /// + /// 菜单 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(MenuId))] + public SysMenu SysMenu { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysRoleOrg.cs b/Admin.NET.Core/Entity/SysRoleOrg.cs new file mode 100644 index 0000000..c4622f8 --- /dev/null +++ b/Admin.NET.Core/Entity/SysRoleOrg.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色机构表 +/// +[SugarTable(null, "系统角色机构表")] +[SysTable] +public class SysRoleOrg : EntityBaseId +{ + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public long OrgId { get; set; } + + /// + /// 机构 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysSchedule.cs b/Admin.NET.Core/Entity/SysSchedule.cs new file mode 100644 index 0000000..4361bd3 --- /dev/null +++ b/Admin.NET.Core/Entity/SysSchedule.cs @@ -0,0 +1,52 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统日程表 +/// +[SugarTable(null, "系统日程表")] +[SysTable] +public class SysSchedule : EntityBaseTenant +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 日程日期 + /// + [SugarColumn(ColumnDescription = "日程日期")] + public DateTime? ScheduleTime { get; set; } + + /// + /// 开始时间 + /// + [SugarColumn(ColumnDescription = "开始时间", Length = 10)] + public string? StartTime { get; set; } + + /// + /// 结束时间 + /// + [SugarColumn(ColumnDescription = "结束时间", Length = 10)] + public string? EndTime { get; set; } + + /// + /// 日程内容 + /// + [SugarColumn(ColumnDescription = "日程内容", Length = 256)] + [Required, MaxLength(256)] + public virtual string Content { get; set; } + + /// + /// 完成状态 + /// + [SugarColumn(ColumnDescription = "完成状态")] + public FinishStatusEnum Status { get; set; } = FinishStatusEnum.UnFinish; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysTemplate.cs b/Admin.NET.Core/Entity/SysTemplate.cs new file mode 100644 index 0000000..9364cd6 --- /dev/null +++ b/Admin.NET.Core/Entity/SysTemplate.cs @@ -0,0 +1,63 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统模板表 +/// +[SysTable] +[SugarTable(null, "系统模板表")] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)] +[SugarIndex("index_{table}_G", nameof(GroupName), OrderByType.Asc)] +public partial class SysTemplate : EntityBaseTenant +{ + /// + /// 名称 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "名称", Length = 128)] + public virtual string Name { get; set; } + + /// + /// 分组名称 + /// + [SugarColumn(ColumnDescription = "分组名称")] + public virtual TemplateTypeEnum Type { get; set; } + + /// + /// 编码 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "编码", Length = 128)] + public virtual string Code { get; set; } + + /// + /// 分组名称 + /// + [MaxLength(32)] + [SugarColumn(ColumnDescription = "分组名称", Length = 32)] + public virtual string GroupName { get; set; } + + /// + /// 模板内容 + /// + [SugarColumn(ColumnDescription = "模板内容", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string Content { get; set; } + + /// + /// 备注 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "备注", Length = 128)] + public virtual string? Remark { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public virtual int OrderNo { get; set; } = 100; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysTenant.cs b/Admin.NET.Core/Entity/SysTenant.cs new file mode 100644 index 0000000..9f2090e --- /dev/null +++ b/Admin.NET.Core/Entity/SysTenant.cs @@ -0,0 +1,145 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户表 +/// +[SugarTable(null, "系统租户表")] +[SysTable] +public partial class SysTenant : EntityBase +{ + /// + /// 租管用户Id + /// + [SugarColumn(ColumnDescription = "租管用户Id")] + public virtual long UserId { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public virtual long OrgId { get; set; } + + /// + /// 域名 + /// + [SugarColumn(ColumnDescription = "域名", Length = 128)] + [MaxLength(128)] + public virtual string? Host { get; set; } + + /// + /// 租户类型 + /// + [SugarColumn(ColumnDescription = "租户类型")] + public virtual TenantTypeEnum TenantType { get; set; } + + /// + /// 数据库类型 + /// + [SugarColumn(ColumnDescription = "数据库类型")] + public virtual SqlSugar.DbType DbType { get; set; } + + /// + /// 数据库连接 + /// + [SugarColumn(ColumnDescription = "数据库连接", Length = 256)] + [MaxLength(256)] + public virtual string? Connection { get; set; } + + /// + /// 数据库标识 + /// + [SugarColumn(ColumnDescription = "数据库标识", Length = 64)] + [MaxLength(64)] + public virtual string? ConfigId { get; set; } + + /// + /// 从库连接/读写分离 + /// + [SugarColumn(ColumnDescription = "从库连接/读写分离", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string? SlaveConnections { get; set; } + + /// + /// 启用注册功能 + /// + [SugarColumn(ColumnDescription = "启用注册功能")] + public virtual YesNoEnum? EnableReg { get; set; } = YesNoEnum.N; + + /// + /// 默认注册方案Id + /// + [SugarColumn(ColumnDescription = "默认注册方案")] + public virtual long? RegWayId { get; set; } + + /// + /// 图标 + /// + [SugarColumn(ColumnDescription = "图标", Length = 256), MaxLength(256)] + public virtual string? Logo { get; set; } + + /// + /// 标题 + /// + [SugarColumn(ColumnDescription = "标题", Length = 32), MaxLength(32)] + public virtual string? Title { get; set; } + + /// + /// 副标题 + /// + [SugarColumn(ColumnDescription = "副标题", Length = 32), MaxLength(32)] + public virtual string? ViceTitle { get; set; } + + /// + /// 副描述 + /// + [SugarColumn(ColumnDescription = "副描述", Length = 64), MaxLength(64)] + public virtual string? ViceDesc { get; set; } + + /// + /// 水印 + /// + [SugarColumn(ColumnDescription = "水印", Length = 32), MaxLength(32)] + public virtual string? Watermark { get; set; } + + /// + /// 版权信息 + /// + [SugarColumn(ColumnDescription = "版权信息", Length = 64), MaxLength(64)] + public virtual string? Copyright { get; set; } + + /// + /// ICP备案号 + /// + [SugarColumn(ColumnDescription = "ICP备案号", Length = 32), MaxLength(32)] + public virtual string? Icp { get; set; } + + /// + /// ICP地址 + /// + [SugarColumn(ColumnDescription = "ICP地址", Length = 32), MaxLength(32)] + public virtual string? IcpUrl { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public virtual int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public virtual string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public virtual StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysTenantConfig.cs b/Admin.NET.Core/Entity/SysTenantConfig.cs new file mode 100644 index 0000000..2cfd676 --- /dev/null +++ b/Admin.NET.Core/Entity/SysTenantConfig.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户配置参数表 +/// +[SugarTable(null, "系统租户配置参数表")] +[SysTable] +public partial class SysTenantConfig : SysConfig +{ + /// + /// 无效字段,用于忽略实体类的Value字段 + /// + [SugarColumn(IsIgnore = true)] + private new string? Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysTenantConfigData.cs b/Admin.NET.Core/Entity/SysTenantConfigData.cs new file mode 100644 index 0000000..0741c31 --- /dev/null +++ b/Admin.NET.Core/Entity/SysTenantConfigData.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户配置参数值表 +/// +[SugarTable(null, "系统租户配置参数值表")] +[SysTable] +[SugarIndex("index_{table}_TC", nameof(TenantId), OrderByType.Asc, nameof(ConfigId), OrderByType.Asc)] +public class SysTenantConfigData : EntityBaseTenantId +{ + /// + /// 配置项Id + /// + [SugarColumn(ColumnDescription = "配置项Id")] + public long ConfigId { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + public string? Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysTenantMenu.cs b/Admin.NET.Core/Entity/SysTenantMenu.cs new file mode 100644 index 0000000..74ed1dc --- /dev/null +++ b/Admin.NET.Core/Entity/SysTenantMenu.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户菜单表 +/// +[SysTable] +[SugarTable(null, "系统租户菜单表")] +public class SysTenantMenu : EntityBaseId +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id")] + public long TenantId { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysUser.cs b/Admin.NET.Core/Entity/SysUser.cs new file mode 100644 index 0000000..335753e --- /dev/null +++ b/Admin.NET.Core/Entity/SysUser.cs @@ -0,0 +1,353 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户表 +/// +[SugarTable(null, "系统用户表")] +[SysTable] +[SugarIndex("index_{table}_A", nameof(Account), OrderByType.Asc)] +[SugarIndex("index_{table}_P", nameof(Phone), OrderByType.Asc)] +public partial class SysUser : EntityBaseTenantOrg +{ + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [Required, MaxLength(32)] + public virtual string Account { get; set; } + + /// + /// 密码 + /// + [SugarColumn(ColumnDescription = "密码", Length = 512)] + [MaxLength(512)] + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string Password { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public virtual string RealName { get; set; } + + /// + /// 昵称 + /// + [SugarColumn(ColumnDescription = "昵称", Length = 32)] + [MaxLength(32)] + public string? NickName { get; set; } + + /// + /// 头像 + /// + [SugarColumn(ColumnDescription = "头像", Length = 512)] + [MaxLength(512)] + public string? Avatar { get; set; } + + /// + /// 性别-男_1、女_2 + /// + [SugarColumn(ColumnDescription = "性别")] + public GenderEnum Sex { get; set; } = GenderEnum.Male; + + /// + /// 年龄 + /// + [SugarColumn(ColumnDescription = "年龄")] + public int Age { get; set; } + + /// + /// 出生日期 + /// + [SugarColumn(ColumnDescription = "出生日期")] + public DateTime? Birthday { get; set; } + + /// + /// 民族 + /// + [SugarColumn(ColumnDescription = "民族", Length = 32)] + [MaxLength(32)] + public string? Nation { get; set; } + + /// + /// 手机号码 + /// + [SugarColumn(ColumnDescription = "手机号码", Length = 16)] + [MaxLength(16)] + public string? Phone { get; set; } + + /// + /// 证件类型 + /// + [SugarColumn(ColumnDescription = "证件类型")] + public CardTypeEnum CardType { get; set; } + + /// + /// 身份证号 + /// + [SugarColumn(ColumnDescription = "身份证号", Length = 32)] + [MaxLength(32)] + public string? IdCardNum { get; set; } + + /// + /// 邮箱 + /// + [SugarColumn(ColumnDescription = "邮箱", Length = 64)] + [MaxLength(64)] + public string? Email { get; set; } + + /// + /// 地址 + /// + [SugarColumn(ColumnDescription = "地址", Length = 256)] + [MaxLength(256)] + public string? Address { get; set; } + + /// + /// 文化程度 + /// + [SugarColumn(ColumnDescription = "文化程度")] + public CultureLevelEnum CultureLevel { get; set; } + + /// + /// 政治面貌 + /// + [SugarColumn(ColumnDescription = "政治面貌", Length = 16)] + [MaxLength(16)] + public string? PoliticalOutlook { get; set; } + + /// + /// 毕业院校 + /// + [SugarColumn(ColumnDescription = "毕业院校", Length = 128)] + [MaxLength(128)] + public string? College { get; set; } + + /// + /// 办公电话 + /// + [SugarColumn(ColumnDescription = "办公电话", Length = 16)] + [MaxLength(16)] + public string? OfficePhone { get; set; } + + /// + /// 紧急联系人 + /// + [SugarColumn(ColumnDescription = "紧急联系人", Length = 32)] + [MaxLength(32)] + public string? EmergencyContact { get; set; } + + /// + /// 紧急联系人电话 + /// + [SugarColumn(ColumnDescription = "紧急联系人电话", Length = 16)] + [MaxLength(16)] + public string? EmergencyPhone { get; set; } + + /// + /// 紧急联系人地址 + /// + [SugarColumn(ColumnDescription = "紧急联系人地址", Length = 256)] + [MaxLength(256)] + public string? EmergencyAddress { get; set; } + + /// + /// 个人简介 + /// + [SugarColumn(ColumnDescription = "个人简介", Length = 512)] + [MaxLength(512)] + public string? Introduction { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 账号类型 + /// + [SugarColumn(ColumnDescription = "账号类型")] + public AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser; + + ///// + ///// 直属机构Id + ///// + //[SugarColumn(ColumnDescription = "直属机构Id")] + //public long OrgId { get; set; } + + /// + /// 直属机构 + /// + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } + + /// + /// 直属主管Id + /// + [SugarColumn(ColumnDescription = "直属主管Id")] + public long? ManagerUserId { get; set; } + + /// + /// 直属主管 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(ManagerUserId))] + public SysUser ManagerUser { get; set; } + + /// + /// 职位Id + /// + [SugarColumn(ColumnDescription = "职位Id")] + public long PosId { get; set; } + + /// + /// 职位 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(PosId))] + public SysPos SysPos { get; set; } + + /// + /// 工号 + /// + [SugarColumn(ColumnDescription = "工号", Length = 32)] + [MaxLength(32)] + public string? JobNum { get; set; } + + /// + /// 职级 + /// + [SugarColumn(ColumnDescription = "职级", Length = 32)] + [MaxLength(32)] + public string? PosLevel { get; set; } + + /// + /// 职称 + /// + [SugarColumn(ColumnDescription = "职称", Length = 32)] + [MaxLength(32)] + public string? PosTitle { get; set; } + + /// + /// 擅长领域 + /// + [SugarColumn(ColumnDescription = "擅长领域", Length = 32)] + [MaxLength(32)] + public string? Expertise { get; set; } + + /// + /// 办公区域 + /// + [SugarColumn(ColumnDescription = "办公区域", Length = 32)] + [MaxLength(32)] + public string? OfficeZone { get; set; } + + /// + /// 办公室 + /// + [SugarColumn(ColumnDescription = "办公室", Length = 32)] + [MaxLength(32)] + public string? Office { get; set; } + + /// + /// 入职日期 + /// + [SugarColumn(ColumnDescription = "入职日期")] + public DateTime? JoinDate { get; set; } + + /// + /// 最新登录Ip + /// + [SugarColumn(ColumnDescription = "最新登录Ip", Length = 256)] + [MaxLength(256)] + public string? LastLoginIp { get; set; } + + /// + /// 最新登录地点 + /// + [SugarColumn(ColumnDescription = "最新登录地点", Length = 128)] + [MaxLength(128)] + public string? LastLoginAddress { get; set; } + + /// + /// 最新登录时间 + /// + [SugarColumn(ColumnDescription = "最新登录时间")] + public DateTime? LastLoginTime { get; set; } + + /// + /// 最新登录设备 + /// + [SugarColumn(ColumnDescription = "最新登录设备", Length = 128)] + [MaxLength(128)] + public string? LastLoginDevice { get; set; } + + /// + /// 电子签名 + /// + [SugarColumn(ColumnDescription = "电子签名", Length = 512)] + [MaxLength(512)] + public string? Signature { get; set; } + + /// + /// 语言代码(如 zh-CN) + /// + [SugarColumn(ColumnDescription = "语言代码")] + public string LangCode { get; set; } = App.GetOptions().DefaultCulture; + + /// + /// 个性化首页地址 + /// + [SugarColumn(ColumnDescription = "个性化首页地址", Length = 512)] + [MaxLength(512)] + public string? Homepage { get; set; } + + /// + /// 验证超级管理员类型,若账号类型为超级管理员则报错 + /// + /// 自定义错误消息 + public void ValidateIsSuperAdminAccountType(ErrorCodeEnum? errorMsg = ErrorCodeEnum.D1014) + { + if (AccountType == AccountTypeEnum.SuperAdmin) + { + throw Oops.Oh(errorMsg); + } + } + + /// + /// 验证用户Id是否相同,若用户Id相同则报错 + /// + /// 用户Id + /// 自定义错误消息 + public void ValidateIsUserId(long userId, ErrorCodeEnum? errorMsg = ErrorCodeEnum.D1001) + { + if (Id == userId) + { + throw Oops.Oh(errorMsg); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysUserConfig.cs b/Admin.NET.Core/Entity/SysUserConfig.cs new file mode 100644 index 0000000..50e5100 --- /dev/null +++ b/Admin.NET.Core/Entity/SysUserConfig.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户配置参数表 +/// +[SugarTable(null, "系统用户配置参数表")] +[SysTable] +public partial class SysUserConfig : SysConfig +{ + /// + /// 无效字段,用于忽略实体类的Value字段 + /// + [SugarColumn(IsIgnore = true)] + private new string? Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysUserConfigData.cs b/Admin.NET.Core/Entity/SysUserConfigData.cs new file mode 100644 index 0000000..f1bf4f9 --- /dev/null +++ b/Admin.NET.Core/Entity/SysUserConfigData.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户配置参数值表 +/// +[SugarTable(null, "系统租户配置参数值表")] +[SysTable] +[SugarIndex("index_{table}_UC", nameof(UserId), OrderByType.Asc, nameof(ConfigId), OrderByType.Asc)] +public class SysUserConfigData : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 配置项Id + /// + [SugarColumn(ColumnDescription = "配置项Id")] + public long ConfigId { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + public string? Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysUserExtOrg.cs b/Admin.NET.Core/Entity/SysUserExtOrg.cs new file mode 100644 index 0000000..cc8c3cb --- /dev/null +++ b/Admin.NET.Core/Entity/SysUserExtOrg.cs @@ -0,0 +1,77 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户扩展机构表 +/// +[SugarTable(null, "系统用户扩展机构表")] +[SysTable] +public partial class SysUserExtOrg : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public long OrgId { get; set; } + + /// + /// 机构 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } + + /// + /// 职位Id + /// + [SugarColumn(ColumnDescription = "职位Id")] + public long PosId { get; set; } + + /// + /// 职位 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(PosId))] + public SysPos SysPos { get; set; } + + /// + /// 工号 + /// + [SugarColumn(ColumnDescription = "工号", Length = 32)] + [MaxLength(32)] + public string? JobNum { get; set; } + + /// + /// 职级 + /// + [SugarColumn(ColumnDescription = "职级", Length = 32)] + [MaxLength(32)] + public string? PosLevel { get; set; } + + /// + /// 入职日期 + /// + [SugarColumn(ColumnDescription = "入职日期")] + public DateTime? JoinDate { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysUserLdap.cs b/Admin.NET.Core/Entity/SysUserLdap.cs new file mode 100644 index 0000000..723c6ca --- /dev/null +++ b/Admin.NET.Core/Entity/SysUserLdap.cs @@ -0,0 +1,80 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户域配置表 +/// +[SugarTable(null, "系统用户域配置表")] +[SysTable] +[SugarIndex("index_{table}_A", nameof(Account), OrderByType.Asc)] +[SugarIndex("index_{table}_U", nameof(UserId), OrderByType.Asc)] +public class SysUserLdap : EntityBaseTenantId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 域账号 + /// AD域对应sAMAccountName + /// Ldap对应uid + /// + [SugarColumn(ColumnDescription = "域账号", Length = 32)] + [Required] + public string Account { get; set; } + + /// + /// 域用户名 + /// + [SugarColumn(ColumnDescription = "域用户名", Length = 32)] + public string UserName { get; set; } + + /// + /// 对应EmployeeId(用于数据导入对照) + /// + [SugarColumn(ColumnDescription = "对应EmployeeId", Length = 32)] + public string? EmployeeId { get; set; } + + /// + /// 组织代码 + /// + [SugarColumn(ColumnDescription = "组织代码", Length = 64)] + public string? DeptCode { get; set; } + + /// + /// 最后设置密码时间 + /// + [SugarColumn(ColumnDescription = "最后设置密码时间")] + public DateTime? PwdLastSetTime { get; set; } + + /// + /// 邮箱 + /// + [SugarColumn(ColumnDescription = "组织代码", Length = 64)] + public string? Mail { get; set; } + + /// + /// 检查账户是否已过期 + /// + [SugarColumn(ColumnDescription = "检查账户是否已过期")] + public bool AccountExpiresFlag { get; set; } = false; + + /// + /// 密码设置是否永不过期 + /// + [SugarColumn(ColumnDescription = "密码设置是否永不过期")] + public bool DontExpiresFlag { get; set; } = false; + + /// + /// DN + /// + [SugarColumn(ColumnDescription = "DN", Length = 512)] + public string Dn { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysUserMenu.cs b/Admin.NET.Core/Entity/SysUserMenu.cs new file mode 100644 index 0000000..ee2a683 --- /dev/null +++ b/Admin.NET.Core/Entity/SysUserMenu.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户菜单快捷导航表 +/// +[SugarTable(null, "系统用户菜单快捷导航表")] +[SysTable] +public partial class SysUserMenu : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } + + /// + /// 菜单 + /// + [Navigate(NavigateType.OneToOne, nameof(MenuId))] + public SysMenu SysMenu { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysUserRegWay.cs b/Admin.NET.Core/Entity/SysUserRegWay.cs new file mode 100644 index 0000000..6ae445b --- /dev/null +++ b/Admin.NET.Core/Entity/SysUserRegWay.cs @@ -0,0 +1,59 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户注册方案表 +/// +[SugarTable(null, "系统用户注册方案表")] +[SysTable] +public partial class SysUserRegWay : EntityBaseTenant +{ + /// + /// 方案名称 + /// + [MaxLength(32)] + [SugarColumn(ColumnDescription = "方案名称", Length = 32)] + public virtual string Name { get; set; } + + /// + /// 账号类型 + /// + [SugarColumn(ColumnDescription = "账号类型")] + public virtual AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser; + + /// + /// 注册用户默认角色 + /// + [SugarColumn(ColumnDescription = "角色")] + public virtual long RoleId { get; set; } + + /// + /// 注册用户默认机构 + /// + [SugarColumn(ColumnDescription = "机构")] + public virtual long OrgId { get; set; } + + /// + /// 注册用户默认职位 + /// + [SugarColumn(ColumnDescription = "职位")] + public virtual long PosId { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "备注", Length = 128)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysUserRole.cs b/Admin.NET.Core/Entity/SysUserRole.cs new file mode 100644 index 0000000..8d5cff1 --- /dev/null +++ b/Admin.NET.Core/Entity/SysUserRole.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户角色表 +/// +[SugarTable(null, "系统用户角色表")] +[SysTable] +public class SysUserRole : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 角色 + /// + [Navigate(NavigateType.OneToOne, nameof(RoleId))] + public SysRole SysRole { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysWechatPay.cs b/Admin.NET.Core/Entity/SysWechatPay.cs new file mode 100644 index 0000000..0676295 --- /dev/null +++ b/Admin.NET.Core/Entity/SysWechatPay.cs @@ -0,0 +1,191 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统微信支付表 +/// +[SugarTable(null, "系统微信支付表")] +[SysTable] +[SugarIndex("index_{table}_BU", nameof(BusinessId), OrderByType.Asc)] +[SugarIndex("index_{table}_TR", nameof(TradeState), OrderByType.Asc)] +[SugarIndex("index_{table}_TA", nameof(Tags), OrderByType.Asc)] +public partial class SysWechatPay : EntityBase +{ + /// + /// 微信商户号 + /// + [SugarColumn(ColumnDescription = "微信商户号")] + [Required] + public virtual string MerchantId { get; set; } + + /// + /// 服务商AppId + /// + [SugarColumn(ColumnDescription = "服务商AppId")] + [Required] + public virtual string AppId { get; set; } + + /// + /// 商户订单号 + /// + [SugarColumn(ColumnDescription = "商户订单号")] + [Required] + public virtual string OutTradeNumber { get; set; } + + /// + /// 支付订单号 + /// + [SugarColumn(ColumnDescription = "支付订单号")] + [Required] + public virtual string TransactionId { get; set; } + + /// + /// 交易类型 + /// + [SugarColumn(ColumnDescription = "交易类型")] + public string? TradeType { get; set; } + + /// + /// 交易状态 + /// + [SugarColumn(ColumnDescription = "交易状态")] + public string? TradeState { get; set; } + + /// + /// 交易状态描述 + /// + [SugarColumn(ColumnDescription = "交易状态描述")] + public string? TradeStateDescription { get; set; } + + /// + /// 付款银行类型 + /// + [SugarColumn(ColumnDescription = "付款银行类型")] + public string? BankType { get; set; } + + /// + /// 订单总金额 + /// + [SugarColumn(ColumnDescription = "订单总金额")] + public int Total { get; set; } + + /// + /// 用户支付金额 + /// + [SugarColumn(ColumnDescription = "用户支付金额")] + public int? PayerTotal { get; set; } + + /// + /// 支付完成时间 + /// + [SugarColumn(ColumnDescription = "支付完成时间")] + public DateTime? SuccessTime { get; set; } + + /// + /// 交易结束时间 + /// + [SugarColumn(ColumnDescription = "交易结束时间")] + public DateTime? ExpireTime { get; set; } + + /// + /// 商品描述 + /// + [SugarColumn(ColumnDescription = "商品描述")] + public string? Description { get; set; } + + /// + /// 场景信息 + /// + [SugarColumn(ColumnDescription = "场景信息")] + public string? Scene { get; set; } + + /// + /// 附加数据 + /// + [SugarColumn(ColumnDescription = "附加数据")] + public string? Attachment { get; set; } + + /// + /// 优惠标记 + /// + [SugarColumn(ColumnDescription = "优惠标记")] + public string? GoodsTag { get; set; } + + /// + /// 结算信息 + /// + [SugarColumn(ColumnDescription = "结算信息")] + public string? Settlement { get; set; } + + /// + /// 回调通知地址 + /// + [SugarColumn(ColumnDescription = "回调通知地址")] + public string? NotifyUrl { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注")] + public string? Remark { get; set; } + + /// + /// 微信OpenId标识 + /// + [SugarColumn(ColumnDescription = "微信OpenId标识")] + public string? OpenId { get; set; } + + /// + /// 业务标签,用来区分做什么业务 + /// + /// + /// Tags标识用来区分这个支付记录对应什么业务从而确定相关联的表名, + /// 再结合BusinessId保存了对应的业务数据的ID,就可以确定这个支付 + /// 记录与哪一条业务数据相关联 + /// + [SugarColumn(ColumnDescription = "业务标签,用来区分做什么业务", Length = 64)] + public string? Tags { get; set; } + + /// + /// 对应业务的主键 + /// + [SugarColumn(ColumnDescription = "对应业务的主键")] + public long BusinessId { get; set; } + + /// + /// 付款二维码内容 + /// + [SugarColumn(ColumnDescription = "付款二维码内容")] + public string? QrcodeContent { get; set; } + + /// + /// 关联微信用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(OpenId))] + public SysWechatUser SysWechatUser { get; set; } + + /// + /// 子商户号 + /// + [SugarColumn(ColumnDescription = "子商户号")] + public string? SubMerchantId { get; set; } + + /// + /// 子商户AppId + /// + [SugarColumn(ColumnDescription = "回调通知地址")] + public string? SubAppId { get; set; } + + /// + /// 子商户唯一标识 + /// + [SugarColumn(ColumnDescription = "子商户唯一标识")] + public string? SubOpenId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysWechatRefund.cs b/Admin.NET.Core/Entity/SysWechatRefund.cs new file mode 100644 index 0000000..cc9b938 --- /dev/null +++ b/Admin.NET.Core/Entity/SysWechatRefund.cs @@ -0,0 +1,97 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统微信支付退款表 +/// +[SugarTable(null, "系统微信支付退款表")] +[SysTable] +[SugarIndex("index_{table}_W", nameof(WechatPayId), OrderByType.Asc)] +public partial class SysWechatRefund : EntityBase +{ + /// + /// 定单主键 + /// + [SugarColumn(ColumnDescription = "定单主键")] + public long WechatPayId { get; set; } + + /// + /// 商户退款号 + /// + [SugarColumn(ColumnDescription = "商户退款号")] + [Required] + public virtual string OutRefundNumber { get; set; } + + /// + /// 退款订单号 + /// + [SugarColumn(ColumnDescription = "退款订单号")] + [Required] + public virtual string TransactionId { get; set; } + + /// + /// 退款原因 + /// + [SugarColumn(ColumnDescription = "退款原因")] + public string? Reason { get; set; } + + /// + /// 退款渠道 + /// + [SugarColumn(ColumnDescription = "退款渠道")] + public string? Channel { get; set; } + + /// + /// 退款入账账户 + /// + /// + /// 取当前退款单的退款入账方,有以下几种情况: + /// 1)退回银行卡:{银行名称}{卡类型}{ 卡尾号} + /// 2)退回支付用户零钱: 支付用户零钱 + /// 3)退还商户: 商户基本账户商户结算银行账户 + /// 4)退回支付用户零钱通: 支付用户零钱通 + /// + [SugarColumn(ColumnDescription = "退款入账账户")] + public string? UserReceivedAccount { get; set; } + + /// + /// 退款状态 + /// + [SugarColumn(ColumnDescription = "退款状态")] + public string? TradeState { get; set; } + + /// + /// 交易状态描述 + /// + [SugarColumn(ColumnDescription = "交易状态描述")] + public string? TradeStateDescription { get; set; } + + /// + /// 订单总金额 + /// + [SugarColumn(ColumnDescription = "退款金额")] + public int Refund { get; set; } + + /// + /// 支完成时间 + /// + [SugarColumn(ColumnDescription = "完成时间")] + public DateTime? SuccessTime { get; set; } + + /// + /// 回调通知地址 + /// + [SugarColumn(ColumnDescription = "回调通知地址")] + public string? NotifyUrl { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注")] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Entity/SysWechatUser.cs b/Admin.NET.Core/Entity/SysWechatUser.cs new file mode 100644 index 0000000..8aa2e58 --- /dev/null +++ b/Admin.NET.Core/Entity/SysWechatUser.cs @@ -0,0 +1,138 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统微信用户表 +/// +[SugarTable(null, "系统微信用户表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(NickName), OrderByType.Asc)] +[SugarIndex("index_{table}_M", nameof(Mobile), OrderByType.Asc)] +public partial class SysWechatUser : EntityBase +{ + /// + /// 系统用户Id + /// + [SugarColumn(ColumnDescription = "系统用户Id")] + public long UserId { get; set; } + + /// + /// 系统用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 平台类型 + /// + [SugarColumn(ColumnDescription = "平台类型")] + public PlatformTypeEnum PlatformType { get; set; } = PlatformTypeEnum.微信公众号; + + /// + /// OpenId + /// + [SugarColumn(ColumnDescription = "OpenId", Length = 64)] + [Required, MaxLength(64)] + public virtual string OpenId { get; set; } + + /// + /// 会话密钥 + /// + [SugarColumn(ColumnDescription = "会话密钥", Length = 256)] + [MaxLength(256)] + public string? SessionKey { get; set; } + + /// + /// UnionId + /// + [SugarColumn(ColumnDescription = "UnionId", Length = 64)] + [MaxLength(64)] + public string? UnionId { get; set; } + + /// + /// 昵称 + /// + [SugarColumn(ColumnDescription = "昵称", Length = 64)] + [MaxLength(64)] + public string? NickName { get; set; } + + /// + /// 头像 + /// + [SugarColumn(ColumnDescription = "头像", Length = 256)] + [MaxLength(256)] + public string? Avatar { get; set; } + + /// + /// 手机号码 + /// + [SugarColumn(ColumnDescription = "手机号码", Length = 16)] + [MaxLength(16)] + public string? Mobile { get; set; } + + /// + /// 性别 + /// + [SugarColumn(ColumnDescription = "性别")] + public int? Sex { get; set; } + + /// + /// 语言 + /// + [SugarColumn(ColumnDescription = "语言", Length = 64)] + [MaxLength(64)] + public string? Language { get; set; } + + /// + /// 城市 + /// + [SugarColumn(ColumnDescription = "城市", Length = 64)] + [MaxLength(64)] + public string? City { get; set; } + + /// + /// 省 + /// + [SugarColumn(ColumnDescription = "省", Length = 64)] + [MaxLength(64)] + public string? Province { get; set; } + + /// + /// 国家 + /// + [SugarColumn(ColumnDescription = "国家", Length = 64)] + [MaxLength(64)] + public string? Country { get; set; } + + /// + /// AccessToken + /// + [SugarColumn(ColumnDescription = "AccessToken", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? AccessToken { get; set; } + + /// + /// RefreshToken + /// + [SugarColumn(ColumnDescription = "RefreshToken", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RefreshToken { get; set; } + + /// + /// 过期时间 + /// + [SugarColumn(ColumnDescription = "ExpiresIn")] + public int? ExpiresIn { get; set; } + + /// + /// 用户授权的作用域,使用逗号分隔 + /// + [SugarColumn(ColumnDescription = "授权作用域", Length = 64)] + [MaxLength(64)] + public string? Scope { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/AccountTypeEnum.cs b/Admin.NET.Core/Enum/AccountTypeEnum.cs new file mode 100644 index 0000000..bedbec6 --- /dev/null +++ b/Admin.NET.Core/Enum/AccountTypeEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 账号类型枚举 +/// +[Description("账号类型枚举")] +public enum AccountTypeEnum +{ + /// + /// 超级管理员 + /// + [Description("超级管理员")] + SuperAdmin = 999, + + /// + /// 系统管理员 + /// + [Description("系统管理员")] + SysAdmin = 888, + + /// + /// 普通账号 + /// + [Description("普通账号")] + NormalUser = 777, + + /// + /// 会员 + /// + [Description("会员")] + Member = 666, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/AlipayCertTypeEnum.cs b/Admin.NET.Core/Enum/AlipayCertTypeEnum.cs new file mode 100644 index 0000000..8bc1c34 --- /dev/null +++ b/Admin.NET.Core/Enum/AlipayCertTypeEnum.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 参与方的证件类型枚举 +/// +[SuppressSniffer] +[Description("参与方的证件类型枚举")] +public enum AlipayCertTypeEnum +{ + [Description("身份证")] + IDENTITY_CARD, + + [Description("护照")] + PASSPORT +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/AlipayIdentityTypeEnum.cs b/Admin.NET.Core/Enum/AlipayIdentityTypeEnum.cs new file mode 100644 index 0000000..4b2d864 --- /dev/null +++ b/Admin.NET.Core/Enum/AlipayIdentityTypeEnum.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 参与方的标识类型枚举 +/// +[SuppressSniffer] +[Description("参与方的标识类型枚举")] +public enum AlipayIdentityTypeEnum +{ + [Description("支付宝用户UID")] + ALIPAY_USER_ID, + + [Description("支付宝登录号")] + ALIPAY_LOGON_ID +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/CacheTypeEnum.cs b/Admin.NET.Core/Enum/CacheTypeEnum.cs new file mode 100644 index 0000000..e6832ed --- /dev/null +++ b/Admin.NET.Core/Enum/CacheTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 缓存类型枚举 +/// +[Description("缓存类型枚举")] +public enum CacheTypeEnum +{ + /// + /// 内存缓存 + /// + [Description("内存缓存")] + Memory, + + /// + /// Redis缓存 + /// + [Description("Redis缓存")] + Redis +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/CardTypeEnum.cs b/Admin.NET.Core/Enum/CardTypeEnum.cs new file mode 100644 index 0000000..774359b --- /dev/null +++ b/Admin.NET.Core/Enum/CardTypeEnum.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 证件类型枚举 +/// +[Description("证件类型枚举")] +public enum CardTypeEnum +{ + /// + /// 身份证 + /// + [Description("身份证")] + IdCard = 0, + + /// + /// 护照 + /// + [Description("护照")] + PassportCard = 1, + + /// + /// 出生证 + /// + [Description("出生证")] + BirthCard = 2, + + /// + /// 港澳台通行证 + /// + [Description("港澳台通行证")] + GatCard = 3, + + /// + /// 外国人居留证 + /// + [Description("外国人居留证")] + ForeignCard = 4, + + /// + /// 营业执照 + /// + [Description("营业执照")] + License = 5, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/CryptogramEnum.cs b/Admin.NET.Core/Enum/CryptogramEnum.cs new file mode 100644 index 0000000..e259bea --- /dev/null +++ b/Admin.NET.Core/Enum/CryptogramEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 密码加密枚举 +/// +[Description("密码加密枚举")] +public enum CryptogramEnum +{ + /// + /// MD5 + /// + [Description("MD5")] + MD5 = 0, + + /// + /// SM2(国密) + /// + [Description("SM2")] + SM2 = 1, + + /// + /// SM4(国密) + /// + [Description("SM4")] + SM4 = 2 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/CultureLevelEnum.cs b/Admin.NET.Core/Enum/CultureLevelEnum.cs new file mode 100644 index 0000000..ce54e33 --- /dev/null +++ b/Admin.NET.Core/Enum/CultureLevelEnum.cs @@ -0,0 +1,92 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 文化程度枚举 +/// +[Description("文化程度枚举")] +public enum CultureLevelEnum +{ + /// + /// 其他 + /// + [Description("其他"), Theme("info")] + Level0 = 0, + + /// + /// 文盲 + /// + [Description("文盲")] + Level1 = 1, + + /// + /// 小学 + /// + [Description("小学")] + Level2 = 2, + + /// + /// 初中 + /// + [Description("初中")] + Level3 = 3, + + /// + /// 普通高中 + /// + [Description("普通高中")] + Level4 = 4, + + /// + /// 技工学校 + /// + [Description("技工学校")] + Level5 = 5, + + /// + /// 职业教育 + /// + [Description("职业教育")] + Level6 = 6, + + /// + /// 职业高中 + /// + [Description("职业高中")] + Level7 = 7, + + /// + /// 中等专科 + /// + [Description("中等专科")] + Level8 = 8, + + /// + /// 大学专科 + /// + [Description("大学专科")] + Level9 = 9, + + /// + /// 大学本科 + /// + [Description("大学本科")] + Level10 = 10, + + /// + /// 硕士研究生 + /// + [Description("硕士研究生")] + Level11 = 11, + + /// + /// 博士研究生 + /// + [Description("博士研究生")] + Level12 = 12, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/DataOpTypeEnum.cs b/Admin.NET.Core/Enum/DataOpTypeEnum.cs new file mode 100644 index 0000000..71ae9c0 --- /dev/null +++ b/Admin.NET.Core/Enum/DataOpTypeEnum.cs @@ -0,0 +1,92 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据操作类型枚举 +/// +[Description("数据操作类型枚举")] +public enum DataOpTypeEnum +{ + /// + /// 其它 + /// + [Description("其它"), Theme("info")] + Other, + + /// + /// 增加 + /// + [Description("增加")] + Add, + + /// + /// 删除 + /// + [Description("删除")] + Delete, + + /// + /// 编辑 + /// + [Description("编辑")] + Edit, + + /// + /// 更新 + /// + [Description("更新")] + Update, + + /// + /// 查询 + /// + [Description("查询")] + Query, + + /// + /// 详情 + /// + [Description("详情")] + Detail, + + /// + /// 树 + /// + [Description("树")] + Tree, + + /// + /// 导入 + /// + [Description("导入")] + Import, + + /// + /// 导出 + /// + [Description("导出")] + Export, + + /// + /// 授权 + /// + [Description("授权")] + Grant, + + /// + /// 强退 + /// + [Description("强退")] + Force, + + /// + /// 清空 + /// + [Description("清空")] + Clean +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/DataScopeEnum.cs b/Admin.NET.Core/Enum/DataScopeEnum.cs new file mode 100644 index 0000000..d6fc8bd --- /dev/null +++ b/Admin.NET.Core/Enum/DataScopeEnum.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 角色数据范围枚举 +/// +[Description("角色数据范围枚举")] +public enum DataScopeEnum +{ + /// + /// 全部数据 + /// + [Description("全部数据")] + All = 1, + + /// + /// 本部门及以下数据 + /// + [Description("本部门及以下数据")] + DeptChild = 2, + + /// + /// 本部门数据 + /// + [Description("本部门数据")] + Dept = 3, + + /// + /// 仅本人数据 + /// + [Description("仅本人数据")] + Self = 4, + + /// + /// 自定义数据 + /// + [Description("自定义数据")] + Define = 5 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/DirectionEnum.cs b/Admin.NET.Core/Enum/DirectionEnum.cs new file mode 100644 index 0000000..caa57ad --- /dev/null +++ b/Admin.NET.Core/Enum/DirectionEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 书写方向枚举 +/// +[Description("书写方向枚举")] +public enum DirectionEnum +{ + /// + /// 从左到右 + /// + [Description("从左到右")] + Ltr = 1, + + /// + /// 从右到左 + /// + [Description("从右到左")] + Rtl = 2 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/ElasticSearchAuthTypeEnum.cs b/Admin.NET.Core/Enum/ElasticSearchAuthTypeEnum.cs new file mode 100644 index 0000000..e11fb00 --- /dev/null +++ b/Admin.NET.Core/Enum/ElasticSearchAuthTypeEnum.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// ES认证类型枚举 +/// https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/_options_on_elasticsearchclientsettings.html +/// +[Description("ES认证类型枚举")] +public enum ElasticSearchAuthTypeEnum +{ + /// + /// BasicAuthentication + /// + [Description("BasicAuthentication")] + Basic = 1, + + /// + /// ApiKey + /// + [Description("ApiKey")] + ApiKey = 2, + + /// + /// Base64ApiKey + /// + [Description("Base64ApiKey")] + Base64ApiKey = 3, + + /// + /// 不验证 + /// + [Description("None")] + None = 4 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/ErrorCodeEnum.cs b/Admin.NET.Core/Enum/ErrorCodeEnum.cs new file mode 100644 index 0000000..dff72be --- /dev/null +++ b/Admin.NET.Core/Enum/ErrorCodeEnum.cs @@ -0,0 +1,909 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统错误码 +/// +[ErrorCodeType] +[Description("系统错误码")] +public enum ErrorCodeEnum +{ + /// + /// 验证码错误 + /// + [ErrorCodeItemMetadata("验证码错误")] + D0008, + + /// + /// 账号不存在 + /// + [ErrorCodeItemMetadata("账号不存在")] + D0009, + + /// + /// 密匙不匹配 + /// + [ErrorCodeItemMetadata("密匙不匹配")] + D0010, + + /// + /// 账号或密码不正确 + /// + [ErrorCodeItemMetadata("账号或密码不正确")] + D1000, + + /// + /// 非法操作!禁止删除自己 + /// + [ErrorCodeItemMetadata("非法操作,禁止删除自己")] + D1001, + + /// + /// 记录不存在 + /// + [ErrorCodeItemMetadata("记录不存在")] + D1002, + + /// + /// 账号已存在 + /// + [ErrorCodeItemMetadata("账号已存在")] + D1003, + + /// + /// 旧密码不匹配 + /// + [ErrorCodeItemMetadata("旧密码输入错误")] + D1004, + + ///// + ///// 测试数据禁止更改admin密码 + ///// + //[ErrorCodeItemMetadata("测试数据禁止更改用户【admin】密码")] + //D1005, + + /// + /// 数据已存在 + /// + [ErrorCodeItemMetadata("数据已存在")] + D1006, + + /// + /// 数据不存在或含有关联引用,禁止删除 + /// + [ErrorCodeItemMetadata("数据不存在或含有关联引用,禁止删除")] + D1007, + + /// + /// 禁止为管理员分配角色 + /// + [ErrorCodeItemMetadata("禁止为管理员分配角色")] + D1008, + + /// + /// 重复数据或记录含有不存在数据 + /// + [ErrorCodeItemMetadata("重复数据或记录含有不存在数据")] + D1009, + + /// + /// 禁止为超级管理员角色分配权限 + /// + [ErrorCodeItemMetadata("禁止为超级管理员角色分配权限")] + D1010, + + /// + /// 非法操作,未登录 + /// + [ErrorCodeItemMetadata("非法操作,未登录")] + D1011, + + /// + /// Id不能为空 + /// + [ErrorCodeItemMetadata("Id不能为空")] + D1012, + + /// + /// 所属机构不在自己的数据范围内 + /// + [ErrorCodeItemMetadata("没有权限操作该数据")] + D1013, + + /// + /// 禁止删除超级管理员 + /// + [ErrorCodeItemMetadata("禁止删除超级管理员")] + D1014, + + /// + /// 禁止修改超级管理员状态 + /// + [ErrorCodeItemMetadata("禁止修改超级管理员状态")] + D1015, + + /// + /// 没有权限 + /// + [ErrorCodeItemMetadata("没有权限")] + D1016, + + /// + /// 账号已冻结 + /// + [ErrorCodeItemMetadata("账号已冻结")] + D1017, + + /// + /// 该租户下角色菜单权限集为空 + /// + [ErrorCodeItemMetadata("该租户下角色菜单权限集为空")] + D1019, + + /// + /// 禁止删除默认租户 + /// + [ErrorCodeItemMetadata("禁止删除默认租户")] + D1023, + + /// + /// 已将其他地方登录账号下线 + /// + [ErrorCodeItemMetadata("已将其他地方登录账号下线")] + D1024, + + /// + /// 此角色下面存在账号禁止删除 + /// + [ErrorCodeItemMetadata("此角色下面存在账号禁止删除")] + D1025, + + /// + /// 禁止修改本人账号状态 + /// + [ErrorCodeItemMetadata("禁止修改本人账号状态")] + D1026, + + /// + /// 密码错误次数过多,账号已锁定,请半小时后重试! + /// + [ErrorCodeItemMetadata("密码错误次数过多,账号已锁定,请半小时后重试!")] + D1027, + + /// + /// 新密码不能与旧密码相同 + /// + [ErrorCodeItemMetadata("新密码不能与旧密码相同")] + D1028, + + /// + /// 系统默认账号禁止删除 + /// + [ErrorCodeItemMetadata("系统默认账号禁止删除")] + D1029, + + /// + /// 开放接口绑定账号禁止删除 + /// + [ErrorCodeItemMetadata("开放接口绑定账号禁止删除")] + D1030, + + /// + /// 开放接口绑定租户禁止删除 + /// + [ErrorCodeItemMetadata("开放接口绑定租户禁止删除")] + D1031, + + /// + /// 手机号已存在 + /// + [ErrorCodeItemMetadata("手机号已存在")] + D1032, + + /// + /// 此角色下存在注册方案禁止删除 + /// + [ErrorCodeItemMetadata("此角色下存在注册方案禁止删除")] + D1033, + + /// + /// 注册功能未开启禁止注册 + /// + [ErrorCodeItemMetadata("注册功能未开启禁止注册")] + D1034, + + /// + /// 注册方案不存在 + /// + [ErrorCodeItemMetadata("注册方案不存在")] + D1035, + + /// + /// 角色不存在 + /// + [ErrorCodeItemMetadata("角色不存在")] + D1036, + + /// + /// 禁止注册超级管理员和系统管理员 + /// + [ErrorCodeItemMetadata("禁止注册超级管理员和系统管理员")] + D1037, + + /// + /// 禁止越权操作系统账户 + /// + [ErrorCodeItemMetadata("禁止越权操作系统账户")] + D1038, + + /// + /// 父机构不存在 + /// + [ErrorCodeItemMetadata("父机构不存在")] + D2000, + + /// + /// 当前机构Id不能与父机构Id相同 + /// + [ErrorCodeItemMetadata("当前机构Id不能与父机构Id相同")] + D2001, + + /// + /// 已有相同组织机构,编码或名称相同 + /// + [ErrorCodeItemMetadata("已有相同组织机构,编码或名称相同")] + D2002, + + /// + /// 没有权限操作机构 + /// + [ErrorCodeItemMetadata("没有权限操作机构")] + D2003, + + /// + /// 该机构下有用户禁止删除 + /// + [ErrorCodeItemMetadata("该机构下有用户禁止删除")] + D2004, + + /// + /// 附属机构下有用户禁止删除 + /// + [ErrorCodeItemMetadata("附属机构下有用户禁止删除")] + D2005, + + /// + /// 只能增加下级机构 + /// + [ErrorCodeItemMetadata("只能增加下级机构")] + D2006, + + /// + /// 下级机构下有用户禁止删除 + /// + [ErrorCodeItemMetadata("下级机构下有用户禁止删除")] + D2007, + + /// + /// 系统默认机构禁止删除 + /// + [ErrorCodeItemMetadata("系统默认机构禁止删除")] + D2008, + + /// + /// 禁止增加根节点机构 + /// + [ErrorCodeItemMetadata("禁止增加根节点机构")] + D2009, + + /// + /// 此机构下存在注册方案禁止删除 + /// + [ErrorCodeItemMetadata("此机构下存在注册方案禁止删除")] + D2010, + + /// + /// 机构不存在 + /// + [ErrorCodeItemMetadata("机构不存在")] + D2011, + + /// + /// 系统默认机构禁止修改 + /// + [ErrorCodeItemMetadata("系统默认机构禁止修改")] + D2012, + + /// + /// 字典类型不存在 + /// + [ErrorCodeItemMetadata("字典类型不存在")] + D3000, + + /// + /// 字典类型已存在 + /// + [ErrorCodeItemMetadata("字典类型已存在,名称或编码重复")] + D3001, + + /// + /// 字典类型下面有字典值禁止删除 + /// + [ErrorCodeItemMetadata("字典类型下面有字典值禁止删除")] + D3002, + + /// + /// 字典值已存在 + /// + [ErrorCodeItemMetadata("字典值已存在")] + D3003, + + /// + /// 字典值不存在 + /// + [ErrorCodeItemMetadata("字典值不存在")] + D3004, + + /// + /// 字典状态错误 + /// + [ErrorCodeItemMetadata("字典状态错误")] + D3005, + + /// + /// 字典编码不能以Enum结尾 + /// + [ErrorCodeItemMetadata("字典编码不能以Enum结尾")] + D3006, + + /// + /// 禁止修改枚举类型的字典编码 + /// + [ErrorCodeItemMetadata("禁止修改枚举类型的字典编码")] + D3007, + + /// + /// 禁止迁移枚举字典 + /// + [ErrorCodeItemMetadata("禁止迁移枚举字典")] + D3008, + + /// + /// 字典已在该租户禁止迁移 + /// + [ErrorCodeItemMetadata("字典已在该租户禁止迁移")] + D3009, + + /// + /// 非超管用户禁止操作系统字典 + /// + [ErrorCodeItemMetadata("非超管用户禁止操作系统字典")] + D3010, + + /// + /// 获取字典值集合入参有误 + /// + [ErrorCodeItemMetadata("获取字典值集合入参有误")] + D3011, + + /// + /// 禁止修改租户字典状态 + /// + [ErrorCodeItemMetadata("禁止修改租户字典状态")] + D3012, + + /// + /// 菜单已存在 + /// + [ErrorCodeItemMetadata("菜单已存在")] + D4000, + + /// + /// 路由地址为空 + /// + [ErrorCodeItemMetadata("路由地址为空")] + D4001, + + /// + /// 打开方式为空 + /// + [ErrorCodeItemMetadata("打开方式为空")] + D4002, + + /// + /// 权限标识格式为空 + /// + [ErrorCodeItemMetadata("权限标识格式为空")] + D4003, + + /// + /// 权限标识格式错误 + /// + [ErrorCodeItemMetadata("权限标识格式错误 如xxx:xxx")] + D4004, + + /// + /// 权限不存在 + /// + [ErrorCodeItemMetadata("权限不存在")] + D4005, + + /// + /// 父级菜单不能为当前节点,请重新选择父级菜单 + /// + [ErrorCodeItemMetadata("父级菜单不能为当前节点,请重新选择父级菜单")] + D4006, + + /// + /// 不能移动根节点 + /// + [ErrorCodeItemMetadata("不能移动根节点")] + D4007, + + /// + /// 禁止本节点与父节点相同 + /// + [ErrorCodeItemMetadata("禁止本节点与父节点相同")] + D4008, + + /// + /// 路由名称重复 + /// + [ErrorCodeItemMetadata("路由名称重复")] + D4009, + + /// + /// 父节点不能为按钮类型 + /// + [ErrorCodeItemMetadata("父节点不能为按钮类型")] + D4010, + + /// + /// 租户不能为空 + /// + [ErrorCodeItemMetadata("租户不能为空")] + D4011, + + /// + /// 系统菜单禁止修改 + /// + [ErrorCodeItemMetadata("系统菜单禁止修改")] + D4012, + + /// + /// 系统菜单禁止删除 + /// + [ErrorCodeItemMetadata("系统菜单禁止删除")] + D4013, + + /// + /// 已存在同名或同编码应用 + /// + [ErrorCodeItemMetadata("已存在同名或同编码应用")] + D5000, + + /// + /// 默认激活系统只能有一个 + /// + [ErrorCodeItemMetadata("默认激活系统只能有一个")] + D5001, + + /// + /// 该应用下有菜单禁止删除 + /// + [ErrorCodeItemMetadata("该应用下有菜单禁止删除")] + D5002, + + /// + /// 已存在同名或同编码应用 + /// + [ErrorCodeItemMetadata("已存在同名或同编码应用")] + D5003, + + /// + /// 已存在同名或同编码职位 + /// + [ErrorCodeItemMetadata("已存在同名或同编码职位")] + D6000, + + /// + /// 该职位下有用户禁止删除 + /// + [ErrorCodeItemMetadata("该职位下有用户禁止删除")] + D6001, + + /// + /// 无权修改本职位 + /// + [ErrorCodeItemMetadata("无权修改本职位")] + D6002, + + /// + /// 职位不存在 + /// + [ErrorCodeItemMetadata("职位不存在")] + D6003, + + /// + /// 此职位下存在注册方案禁止删除 + /// + [ErrorCodeItemMetadata("此职位下存在注册方案禁止删除")] + D6004, + + /// + /// 通知公告状态错误 + /// + [ErrorCodeItemMetadata("通知公告状态错误")] + D7000, + + /// + /// 通知公告删除失败 + /// + [ErrorCodeItemMetadata("通知公告删除失败")] + D7001, + + /// + /// 通知公告编辑失败 + /// + [ErrorCodeItemMetadata("通知公告编辑失败,类型必须为草稿")] + D7002, + + /// + /// 通知公告操作失败,非发布者不能进行操作 + /// + [ErrorCodeItemMetadata("通知公告操作失败,非发布者不能进行操作")] + D7003, + + /// + /// 文件不存在 + /// + [ErrorCodeItemMetadata("文件不存在")] + D8000, + + /// + /// 不允许的文件类型 + /// + [ErrorCodeItemMetadata("不允许的文件类型")] + D8001, + + /// + /// 文件超过允许大小 + /// + [ErrorCodeItemMetadata("文件超过允许大小")] + D8002, + + /// + /// 文件后缀错误 + /// + [ErrorCodeItemMetadata("文件后缀错误")] + D8003, + + /// + /// 文件已存在 + /// + [ErrorCodeItemMetadata("文件已存在")] + D8004, + + /// + /// 无效的文件名 + /// + [ErrorCodeItemMetadata("无效的文件名")] + D8005, + + /// + /// 已存在同名或同编码参数配置 + /// + [ErrorCodeItemMetadata("已存在同名或同编码参数配置")] + D9000, + + /// + /// 禁止删除系统参数 + /// + [ErrorCodeItemMetadata("禁止删除系统参数")] + D9001, + + /// + /// 已存在同名任务调度 + /// + [ErrorCodeItemMetadata("已存在同名任务调度")] + D1100, + + /// + /// 任务调度不存在 + /// + [ErrorCodeItemMetadata("任务调度不存在")] + D1101, + + /// + /// 演示环境禁止修改数据 + /// + [ErrorCodeItemMetadata("演示环境禁止修改数据")] + D1200, + + /// + /// 已存在同名的租户 + /// + [ErrorCodeItemMetadata("已存在同名的租户")] + D1300, + + /// + /// 已存在同名的租户管理员 + /// + [ErrorCodeItemMetadata("已存在同名的租户管理员")] + D1301, + + /// + /// 租户从库配置错误 + /// + [ErrorCodeItemMetadata("租户从库配置错误")] + D1302, + + /// + /// 已存在同名的租户域名 + /// + [ErrorCodeItemMetadata("已存在同名的租户域名")] + D1303, + + /// + /// 授权菜单存在重复项 + /// + [ErrorCodeItemMetadata("授权菜单存在重复项")] + D1304, + + /// + /// 该表代码模板已经生成过 + /// + [ErrorCodeItemMetadata("该表代码模板已经生成过")] + D1400, + + /// + /// 数据库配置不存在 + /// + [ErrorCodeItemMetadata("数据库配置不存在")] + D1401, + + /// + /// 该类型不存在 + /// + [ErrorCodeItemMetadata("该类型不存在")] + D1501, + + /// + /// 该字段不存在 + /// + [ErrorCodeItemMetadata("该字段不存在")] + D1502, + + /// + /// 该类型不是枚举类型 + /// + [ErrorCodeItemMetadata("该类型不是枚举类型")] + D1503, + + /// + /// 该实体不存在 + /// + [ErrorCodeItemMetadata("该实体不存在")] + D1504, + + /// + /// 父菜单不存在 + /// + [ErrorCodeItemMetadata("父菜单不存在")] + D1505, + + /// + /// 父资源不存在 + /// + [ErrorCodeItemMetadata("父资源不存在")] + D1600, + + /// + /// 当前资源Id不能与父资源Id相同 + /// + [ErrorCodeItemMetadata("当前资源Id不能与父资源Id相同")] + D1601, + + /// + /// 已有相同编码或名称 + /// + [ErrorCodeItemMetadata("已有相同编码或名称")] + D1602, + + /// + /// 脚本代码不能为空 + /// + [ErrorCodeItemMetadata("脚本代码不能为空")] + D1701, + + /// + /// 脚本代码中的作业类,需要定义 [JobDetail] 特性 + /// + [ErrorCodeItemMetadata("脚本代码中的作业类,需要定义 [JobDetail] 特性")] + D1702, + + /// + /// 作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致 + /// + [ErrorCodeItemMetadata("作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致")] + D1703, + + /// + /// 禁止修改作业编号 + /// + [ErrorCodeItemMetadata("禁止修改作业编号")] + D1704, + + /// + /// 执行作业失败 + /// + [ErrorCodeItemMetadata("执行作业失败")] + D1705, + + /// + /// 已存在同名打印模板 + /// + [ErrorCodeItemMetadata("已存在同名打印模板")] + D1800, + + /// + /// 已存在同名功能或同名程序及插件 + /// + [ErrorCodeItemMetadata("已存在同名功能或同名程序及插件")] + D1900, + + /// + /// 注册方案名称已存在 + /// + [ErrorCodeItemMetadata("注册方案名称已存在")] + D2101, + + /// + /// 已存在同名模板 + /// + [ErrorCodeItemMetadata("已存在同名模板")] + T1000, + + /// + /// 已存在相同编码模板 + /// + [ErrorCodeItemMetadata("已存在相同编码模板")] + T1001, + + /// + /// 禁止删除存在关联租户的应用 + /// + [ErrorCodeItemMetadata("禁止删除存在关联租户的应用")] + A1001, + + /// + /// 禁止删除存在关联菜单的应用 + /// + [ErrorCodeItemMetadata("禁止删除存在关联菜单的应用")] + A1002, + + /// + /// 找不到系统应用 + /// + [ErrorCodeItemMetadata("找不到系统应用")] + A1000, + + /// + /// 已存在同名或同编码项目 + /// + [ErrorCodeItemMetadata("已存在同名或同编码项目")] + xg1000, + + /// + /// 已存在相同证件号码人员 + /// + [ErrorCodeItemMetadata("已存在相同证件号码人员")] + xg1001, + + /// + /// 检测数据不存在 + /// + [ErrorCodeItemMetadata("检测数据不存在")] + xg1002, + + /// + /// 请添加数据列 + /// + [ErrorCodeItemMetadata("请添加数据列")] + db1000, + + /// + /// 数据表不存在 + /// + [ErrorCodeItemMetadata("数据表不存在")] + db1001, + + /// + /// 数据表不存在 + /// + [ErrorCodeItemMetadata("不允许添加相同字段名")] + db1002, + + /// + /// 实体文件不存在或匹配不到。如果是刚刚生成的实体,请重启服务后再试 + /// + [ErrorCodeItemMetadata("实体文件不存在或匹配不到。如果是刚刚生成的实体,请重启服务后再试")] + db1003, + + /// + /// 父节点不存在 + /// + [ErrorCodeItemMetadata("父节点不存在")] + R2000, + + /// + /// 当前节点Id不能与父节点Id相同 + /// + [ErrorCodeItemMetadata("当前节点Id不能与父节点Id相同")] + R2001, + + /// + /// 已有相同编码或名称 + /// + [ErrorCodeItemMetadata("已有相同编码或名称")] + R2002, + + /// + /// 行政区代码只能为6、9或12位 + /// + [ErrorCodeItemMetadata("行政区代码只能为6、9或12位")] + R2003, + + /// + /// 父节点不能为自己的子节点 + /// + [ErrorCodeItemMetadata("父节点不能为自己的子节点")] + R2004, + + /// + /// 同步国家统计局数据异常,请稍后重试 + /// + [ErrorCodeItemMetadata("同步国家统计局数据异常,请稍后重试")] + R2005, + + /// + /// 默认租户状态禁止修改 + /// + [ErrorCodeItemMetadata("默认租户状态禁止修改")] + Z1001, + + /// + /// 禁止创建此类型的数据库 + /// + [ErrorCodeItemMetadata("禁止创建此类型的数据库")] + Z1002, + + /// + /// 租户不存在或已禁用 + /// + [ErrorCodeItemMetadata("租户不存在或已禁用")] + Z1003, + + /// + /// 租户库连接不能为空 + /// + [ErrorCodeItemMetadata("租户库连接不能为空")] + Z1004, + + /// + /// 身份标识已存在 + /// + [ErrorCodeItemMetadata("身份标识已存在")] + O1000, + + /// + /// 禁止非超级管理员操作 + /// + [ErrorCodeItemMetadata("禁止非超级管理员操作")] + SA001 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/EsClientTypeEnum.cs b/Admin.NET.Core/Enum/EsClientTypeEnum.cs new file mode 100644 index 0000000..14d55e7 --- /dev/null +++ b/Admin.NET.Core/Enum/EsClientTypeEnum.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// ES客户端类型(标识不同场景) +/// +public enum EsClientTypeEnum +{ + /// + /// 日志专用 + /// + Logging, + + /// + /// 业务数据 + /// + Business +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/FilterLogicEnum.cs b/Admin.NET.Core/Enum/FilterLogicEnum.cs new file mode 100644 index 0000000..5e898f2 --- /dev/null +++ b/Admin.NET.Core/Enum/FilterLogicEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 过滤条件 +/// +[Description("过滤条件")] +public enum FilterLogicEnum +{ + /// + /// 并且 + /// + [Description("并且")] + And, + + /// + /// 或者 + /// + [Description("或者")] + Or, + + /// + /// 异或 + /// + [Description("异或")] + Xor +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/FilterOperatorEnum.cs b/Admin.NET.Core/Enum/FilterOperatorEnum.cs new file mode 100644 index 0000000..0de28dd --- /dev/null +++ b/Admin.NET.Core/Enum/FilterOperatorEnum.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 过滤逻辑运算符 +/// +[Description("过滤逻辑运算符")] +public enum FilterOperatorEnum +{ + /// + /// 等于(=) + /// + [Description("等于")] + EQ, + + /// + /// 不等于(!=) + /// + [Description("不等于")] + NEQ, + + /// + /// 小于 + /// + [Description("小于")] + LT, + + /// + /// 小于等于 + /// + [Description("小于等于")] + LTE, + + /// + /// 大于(>) + /// + [Description("大于")] + GT, + + /// + /// 大于等于(>=) + /// + [Description("大于等于")] + GTE, + + /// + /// 开始包含 + /// + [Description("开始包含")] + StartsWith, + + /// + /// 末尾包含 + /// + [Description("末尾包含")] + EndsWith, + + /// + /// 包含 + /// + [Description("包含")] + Contains +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/FinishStatusEnum.cs b/Admin.NET.Core/Enum/FinishStatusEnum.cs new file mode 100644 index 0000000..90e188f --- /dev/null +++ b/Admin.NET.Core/Enum/FinishStatusEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 完成状态枚举 +/// +[Description("完成状态枚举")] +public enum FinishStatusEnum +{ + /// + /// 已完成 + /// + [Description("已完成"), Theme("success")] + Finish = 1, + + /// + /// 未完成 + /// + [Description("未完成"), Theme("danger")] + UnFinish = 0, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/GenderEnum.cs b/Admin.NET.Core/Enum/GenderEnum.cs new file mode 100644 index 0000000..ceea591 --- /dev/null +++ b/Admin.NET.Core/Enum/GenderEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 性别枚举(GB/T 2261.1-2003) +/// +[Description("性别枚举")] +public enum GenderEnum +{ + /// + /// 未知 + /// + [Description("未知"), Theme("info")] + Unknown = 0, + + /// + /// 男性 + /// + [Description("男性"), Theme("success")] + Male = 1, + + /// + /// 女性 + /// + [Description("女性"), Theme("danger")] + Female = 2, + + ///// + ///// 未说明的性别 + ///// + //[Description("未说明的性别"), Theme("warning")] + //Unspecified = 9 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/HttpMethodEnum.cs b/Admin.NET.Core/Enum/HttpMethodEnum.cs new file mode 100644 index 0000000..f930893 --- /dev/null +++ b/Admin.NET.Core/Enum/HttpMethodEnum.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// HTTP请求方法枚举 +/// +[Description("HTTP请求方法枚举")] +public enum HttpMethodEnum +{ + /// + /// HTTP "GET" method. + /// + [Description("HTTP \"GET\" method.")] + Get, + + /// + /// HTTP "POST" method. + /// + [Description("HTTP \"POST\" method.")] + Post, + + /// + /// HTTP "PUT" method. + /// + [Description(" HTTP \"PUT\" method.")] + Put, + + /// + /// HTTP "DELETE" method. + /// + [Description("HTTP \"DELETE\" method.")] + Delete, + + /// + /// HTTP "PATCH" method. + /// + [Description("HTTP \"PATCH\" method. ")] + Patch, + + /// + /// HTTP "HEAD" method. + /// + [Description("HTTP \"HEAD\" method.")] + Head, + + /// + /// HTTP "OPTIONS" method. + /// + [Description("HTTP \"OPTIONS\" method.")] + Options, + + /// + /// HTTP "TRACE" method. + /// + [Description(" HTTP \"TRACE\" method.")] + Trace, + + /// + /// HTTP "CONNECT" method. + /// + [Description("HTTP \"CONNECT\" method.")] + Connect +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/JobCreateTypeEnum.cs b/Admin.NET.Core/Enum/JobCreateTypeEnum.cs new file mode 100644 index 0000000..2d90b69 --- /dev/null +++ b/Admin.NET.Core/Enum/JobCreateTypeEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 作业创建类型枚举 +/// +[Description("作业创建类型枚举")] +public enum JobCreateTypeEnum +{ + /// + /// 内置 + /// + [Description("内置"), Theme("info")] + BuiltIn = 0, + + /// + /// 脚本 + /// + [Description("脚本")] + Script = 1, + + /// + /// HTTP请求 + /// + [Description("HTTP请求")] + Http = 2, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/JobStatusEnum.cs b/Admin.NET.Core/Enum/JobStatusEnum.cs new file mode 100644 index 0000000..b06c65c --- /dev/null +++ b/Admin.NET.Core/Enum/JobStatusEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 岗位状态枚举 +/// +[Description("岗位状态枚举")] +public enum JobStatusEnum +{ + /// + /// 在职 + /// + [Description("在职")] + On = 1, + + /// + /// 离职 + /// + [Description("离职")] + Off = 2, + + /// + /// 请假 + /// + [Description("请假")] + Leave = 3, + + /// + /// 其他 + /// + [Description("其他")] + Other = 4, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/LoginModeEnum.cs b/Admin.NET.Core/Enum/LoginModeEnum.cs new file mode 100644 index 0000000..55e8ddb --- /dev/null +++ b/Admin.NET.Core/Enum/LoginModeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 登录模式枚举 +/// +[Description("登录模式枚举")] +public enum LoginModeEnum +{ + /// + /// PC模式 + /// + [Description("PC模式")] + PC = 1, + + /// + /// APP + /// + [Description("APP")] + APP = 2 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/LoginTypeEnum.cs b/Admin.NET.Core/Enum/LoginTypeEnum.cs new file mode 100644 index 0000000..9fa0950 --- /dev/null +++ b/Admin.NET.Core/Enum/LoginTypeEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 登录类型枚举 +/// +[Description("登录类型枚举")] +public enum LoginTypeEnum +{ + /// + /// PC登录 + /// + [Description("PC登录")] + Login = 1, + + /// + /// PC退出 + /// + [Description("PC退出")] + Logout = 2, + + /// + /// PC注册 + /// + [Description("PC注册")] + Register = 3 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/MaritalStatusEnum.cs b/Admin.NET.Core/Enum/MaritalStatusEnum.cs new file mode 100644 index 0000000..b3787b2 --- /dev/null +++ b/Admin.NET.Core/Enum/MaritalStatusEnum.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 婚姻状况枚举 +/// +[Description("婚姻状况枚举")] +public enum MaritalStatusEnum +{ + /// + /// 未婚 + /// + [Description("未婚")] + UnMarried = 1, + + /// + /// 已婚 + /// + [Description("已婚")] + Married = 2, + + /// + /// 离异 + /// + [Description("离异")] + Divorce = 3, + + /// + /// 再婚 + /// + [Description("再婚")] + Remarry = 4, + + /// + /// 丧偶 + /// + [Description("丧偶")] + Widowed = 5, + + /// + /// 未知 + /// + [Description("未知")] + None = 6, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/MenuTypeEnum.cs b/Admin.NET.Core/Enum/MenuTypeEnum.cs new file mode 100644 index 0000000..0912635 --- /dev/null +++ b/Admin.NET.Core/Enum/MenuTypeEnum.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统菜单类型枚举 +/// +[Description("系统菜单类型枚举")] +public enum MenuTypeEnum +{ + /// + /// 目录 + /// + + [Description("目录"), Theme("warning")] + Dir = 1, + + /// + /// 菜单 + /// + [Description("菜单")] + Menu = 2, + + /// + /// 按钮 + /// + [Description("按钮"), Theme("info")] + Btn = 3 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/MessageTypeEnum.cs b/Admin.NET.Core/Enum/MessageTypeEnum.cs new file mode 100644 index 0000000..1ac3bfd --- /dev/null +++ b/Admin.NET.Core/Enum/MessageTypeEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 消息类型枚举 +/// +[Description("消息类型枚举")] +public enum MessageTypeEnum +{ + /// + /// 普通信息 + /// + [Description("消息"), Theme("info")] + Info = 0, + + /// + /// 成功提示 + /// + [Description("成功"), Theme("success")] + Success = 1, + + /// + /// 警告提示 + /// + [Description("警告"), Theme("warning")] + Warning = 2, + + /// + /// 错误提示 + /// + [Description("错误"), Theme("danger")] + Error = 3 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/NationEnum.cs b/Admin.NET.Core/Enum/NationEnum.cs new file mode 100644 index 0000000..25dd2ed --- /dev/null +++ b/Admin.NET.Core/Enum/NationEnum.cs @@ -0,0 +1,350 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 民族枚举 +/// +[Description("民族枚举")] +public enum NationEnum +{ + /// + /// 汉族 + /// + [Description("汉族")] + HanZu = 1, + + /// + /// 壮族 + /// + [Description("壮族")] + ZhuangZu = 2, + + /// + /// 满族 + /// + [Description("满族")] + ManZu = 3, + + /// + /// 回族 + /// + [Description("回族")] + HuiZu = 4, + + /// + /// 苗族 + /// + [Description("苗族")] + MiaoZu = 5, + + /// + /// 维吾尔族 + /// + [Description("维吾尔族")] + WeiWuErZu = 6, + + /// + /// 土家族 + /// + [Description("土家族")] + TuJiaZu = 7, + + /// + /// 彝族 + /// + [Description("彝族")] + YiZu = 8, + + /// + /// 蒙古族 + /// + [Description("蒙古族")] + MengGuZu = 9, + + /// + /// 藏族 + /// + [Description("藏族")] + ZangZu = 10, + + /// + /// 布依族 + /// + [Description("布依族")] + BuYiZu = 11, + + /// + /// 侗族 + /// + [Description("侗族")] + DongZu = 12, + + /// + /// 瑶族 + /// + [Description("瑶族")] + YaoZu = 13, + + /// + /// 朝鲜族 + /// + [Description("朝鲜族")] + ChaoXianZu = 14, + + /// + /// 白族 + /// + [Description("白族")] + BaiZu = 15, + + /// + /// 哈尼族 + /// + [Description("哈尼族")] + HaNiZu = 16, + + /// + /// 哈萨克族 + /// + [Description("哈萨克族")] + HaSaKeZu = 17, + + /// + /// 黎族 + /// + [Description("黎族")] + LiZu = 18, + + /// + /// 傣族 + /// + [Description("傣族")] + DaiZu = 19, + + /// + /// 畲族 + /// + [Description("畲族")] + SheZu = 20, + + /// + /// 傈僳族 + /// + [Description("傈僳族")] + LiSuZu = 21, + + /// + /// 仡佬族 + /// + [Description("仡佬族")] + GeLaoZu = 22, + + /// + /// 拉祜族 + /// + [Description("拉祜族")] + LaHuZu = 23, + + /// + /// 东乡族 + /// + [Description("东乡族")] + DongXiangZu = 24, + + /// + /// 纳西族 + /// + [Description("纳西族")] + NaXiZu = 25, + + /// + /// 景颇族 + /// + [Description("景颇族")] + JingPoZu = 26, + + /// + /// 柯尔克孜族 + /// + [Description("柯尔克孜族")] + KeErKeZiZu = 27, + + /// + /// 土族 + /// + [Description("土族")] + TuZu = 28, + + /// + /// 达斡尔族 + /// + [Description("达斡尔族")] + DaWoErZu = 29, + + /// + /// 仫佬族 + /// + [Description("仫佬族")] + MuLaoZu = 30, + + /// + /// 羌族 + /// + [Description("羌族")] + QiangZu = 31, + + /// + /// 布朗族 + /// + [Description("布朗族")] + BuLangZu = 32, + + /// + /// 撒拉族 + /// + [Description("撒拉族")] + SaLaZu = 33, + + /// + /// 毛南族 + /// + [Description("毛南族")] + MaoNanZu = 34, + + /// + /// 仡族 + /// + [Description("仡族")] + GeZu = 35, + + /// + /// 锡伯族 + /// + [Description("锡伯族")] + XiBoZu = 36, + + /// + /// 阿昌族 + /// + [Description("阿昌族")] + AChangZu = 37, + + /// + /// 普米族 + /// + [Description("普米族")] + PuMiZu = 38, + + /// + /// 塔吉克族 + /// + [Description("塔吉克族")] + TaJiKeZu = 39, + + /// + /// 怒族 + /// + [Description("怒族")] + NuZu = 40, + + /// + /// 乌孜别克族 + /// + [Description("乌孜别克族")] + WuZiBieKeZu = 41, + + /// + /// 俄罗斯族 + /// + [Description("俄罗斯族")] + ELuoSiZu = 42, + + /// + /// 鄂温克族 + /// + [Description("鄂温克族")] + EwenKeZu = 43, + + /// + /// 德昂族 + /// + [Description("德昂族")] + DeAngZu = 44, + + /// + /// 保安族 + /// + [Description("保安族")] + BaoAnZu = 45, + + /// + /// 裕固族 + /// + [Description("裕固族")] + YuGuZu = 46, + + /// + /// 京族 + /// + [Description("京族")] + JingZu = 47, + + /// + /// 塔塔尔族 + /// + [Description("塔塔尔族")] + TaTaErZu = 48, + + /// + /// 独龙族 + /// + [Description("独龙族")] + DuLongZu = 49, + + /// + /// 鄂伦春族 + /// + [Description("鄂伦春族")] + ELunChunZu = 50, + + /// + /// 赫哲族 + /// + [Description("赫哲族")] + HeZheZu = 51, + + /// + /// 门巴族 + /// + [Description("门巴族")] + MenBaZu = 52, + + /// + /// 珞巴族 + /// + [Description("珞巴族")] + LuoBaZu = 53, + + /// + /// 高山族 + /// + [Description("高山族")] + GaoShanZu = 54, + + /// + /// 佤族 + /// + [Description("佤族")] + WaZu = 55, + + /// + /// 基诺族 + /// + [Description("基诺族")] + JiNuoZu = 56 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/NoticeStatusEnum.cs b/Admin.NET.Core/Enum/NoticeStatusEnum.cs new file mode 100644 index 0000000..0bd68e8 --- /dev/null +++ b/Admin.NET.Core/Enum/NoticeStatusEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通知公告状态枚举 +/// +[Description("通知公告状态枚举")] +public enum NoticeStatusEnum +{ + /// + /// 草稿 + /// + [Description("草稿"), Theme("info")] + DRAFT = 0, + + /// + /// 发布 + /// + [Description("发布")] + PUBLIC = 1, + + /// + /// 撤回 + /// + [Description("撤回")] + CANCEL = 2, + + /// + /// 删除 + /// + [Description("删除")] + DELETED = 3 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/NoticeTypeEnum.cs b/Admin.NET.Core/Enum/NoticeTypeEnum.cs new file mode 100644 index 0000000..387bf29 --- /dev/null +++ b/Admin.NET.Core/Enum/NoticeTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通知公告状类型枚举 +/// +[Description("通知公告状类型枚举")] +public enum NoticeTypeEnum +{ + /// + /// 通知 + /// + [Description("通知")] + NOTICE = 1, + + /// + /// 公告 + /// + [Description("公告")] + ANNOUNCEMENT = 2, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/NoticeUserStatusEnum.cs b/Admin.NET.Core/Enum/NoticeUserStatusEnum.cs new file mode 100644 index 0000000..a22fc26 --- /dev/null +++ b/Admin.NET.Core/Enum/NoticeUserStatusEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通知公告用户状态枚举 +/// +[Description("通知公告用户状态枚举")] +public enum NoticeUserStatusEnum +{ + /// + /// 未读 + /// + [Description("未读")] + UNREAD = 0, + + /// + /// 已读 + /// + [Description("已读"), Theme("info")] + READ = 1 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/PlatformTypeEnum.cs b/Admin.NET.Core/Enum/PlatformTypeEnum.cs new file mode 100644 index 0000000..38f985b --- /dev/null +++ b/Admin.NET.Core/Enum/PlatformTypeEnum.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 平台类型枚举 +/// +[Description("平台类型枚举")] +public enum PlatformTypeEnum +{ + /// + /// 微信公众号 + /// + [Description("微信公众号")] + 微信公众号 = 1, + + /// + /// 微信小程序 + /// + [Description("微信小程序")] + 微信小程序 = 2, + + /// + /// QQ + /// + [Description("QQ")] + QQ = 3, + + /// + /// 支付宝 + /// + [Description("支付宝")] + Alipay = 4, + + /// + /// Gitee + /// + [Description("Gitee")] + Gitee = 5, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/PrintTypeEnum.cs b/Admin.NET.Core/Enum/PrintTypeEnum.cs new file mode 100644 index 0000000..422972f --- /dev/null +++ b/Admin.NET.Core/Enum/PrintTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 打印类型枚举 +/// +[Description("打印类型枚举")] +public enum PrintTypeEnum +{ + /// + /// 浏览器打印 + /// + [Description("浏览器打印")] + Browser = 1, + + /// + /// 浏览器打印 + /// + [Description("客户端打印")] + Client = 2, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/RequestTypeEnum.cs b/Admin.NET.Core/Enum/RequestTypeEnum.cs new file mode 100644 index 0000000..f1f4aee --- /dev/null +++ b/Admin.NET.Core/Enum/RequestTypeEnum.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// HTTP请求类型 +/// +[Description("HTTP请求类型")] +public enum RequestTypeEnum +{ + /// + /// 执行内部方法 + /// + Run = 0, + + /// + /// GET + /// + Get = 1, + + /// + /// POST + /// + Post = 2, + + /// + /// PUT + /// + Put = 3, + + /// + /// DELETE + /// + Delete = 4 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/StatusEnum.cs b/Admin.NET.Core/Enum/StatusEnum.cs new file mode 100644 index 0000000..246f6dc --- /dev/null +++ b/Admin.NET.Core/Enum/StatusEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通用状态枚举 +/// +[Description("通用状态枚举")] +public enum StatusEnum +{ + /// + /// 启用 + /// + [Description("启用"), Theme("success")] + Enable = 1, + + /// + /// 停用 + /// + [Description("停用"), Theme("danger")] + Disable = 2, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs b/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs new file mode 100644 index 0000000..7245626 --- /dev/null +++ b/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件类型-系统用户操作枚举 +/// +[SuppressSniffer] +[Description("事件类型-系统用户操作枚举")] +public enum SysUserEventTypeEnum +{ + /// + /// 增加用户 + /// + [Description("增加用户")] + Add = 111, + + /// + /// 更新用户 + /// + [Description("更新用户")] + Update = 222, + + /// + /// 授权用户角色 + /// + [Description("授权用户角色")] + UpdateRole = 333, + + /// + /// 删除用户 + /// + [Description("删除用户")] + Delete = 444, + + /// + /// 设置用户状态 + /// + [Description("设置用户状态")] + SetStatus = 555, + + /// + /// 修改密码 + /// + [Description("修改密码")] + ChangePwd = 666, + + /// + /// 重置密码 + /// + [Description("重置密码")] + ResetPwd = 777, + + /// + /// 解除登录锁定 + /// + [Description("解除登录锁定")] + UnlockLogin = 888, + + /// + /// 注册用户 + /// + [Description("注册用户")] + Register = 999, + + /// + /// 用户登录 + /// + [Description("用户登录")] + Login = 1000, + + /// + /// 用户退出 + /// + [Description("用户退出")] + LoginOut = 1001, + + /// + /// RefreshToken + /// + [Description("刷新Token")] + RefreshToken = 1002, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/TemplateTypeEnum.cs b/Admin.NET.Core/Enum/TemplateTypeEnum.cs new file mode 100644 index 0000000..9519a7b --- /dev/null +++ b/Admin.NET.Core/Enum/TemplateTypeEnum.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 消息模板类型枚举 +/// +[Description("消息模板类型枚举")] +public enum TemplateTypeEnum +{ + /// + /// 通知公告 + /// + [Description("通知")] + Notice = 1, + + /// + /// 短信 + /// + [Description("短信")] + SMS = 2, + + /// + /// 邮件 + /// + [Description("邮件")] + Email = 3, + + /// + /// 微信 + /// + [Description("微信")] + Wechat = 4, + + /// + /// 钉钉 + /// + [Description("钉钉")] + DingTalk = 5, + + /// + /// 企业微信 + /// + [Description("企业微信")] + WeChatWork = 7 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/TenantTypeEnum.cs b/Admin.NET.Core/Enum/TenantTypeEnum.cs new file mode 100644 index 0000000..c9eeb90 --- /dev/null +++ b/Admin.NET.Core/Enum/TenantTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 租户类型枚举 +/// +[Description("租户类型枚举")] +public enum TenantTypeEnum +{ + /// + /// ID隔离 + /// + [Description("ID隔离")] + Id = 0, + + /// + /// 库隔离 + /// + [Description("库隔离")] + Db = 1, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/WechatReturnCodeEnum.cs b/Admin.NET.Core/Enum/WechatReturnCodeEnum.cs new file mode 100644 index 0000000..ba30a3e --- /dev/null +++ b/Admin.NET.Core/Enum/WechatReturnCodeEnum.cs @@ -0,0 +1,289 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 微信开发返回码 +/// +[Description("微信开发返回码")] +public enum WechatReturnCodeEnum +{ + SenparcWeixinSDK配置错误 = -99, // 0xFFFFFF9D + 系统繁忙此时请开发者稍候再试 = -1, // 0xFFFFFFFF + 请求成功 = 0, + 工商数据返回_企业已注销 = 101, // 0x00000065 + 工商数据返回_企业不存在或企业信息未更新 = 102, // 0x00000066 + 工商数据返回_企业法定代表人姓名不一致 = 103, // 0x00000067 + 工商数据返回_企业法定代表人身份证号码不一致 = 104, // 0x00000068 + 法定代表人身份证号码_工商数据未更新_请5_15个工作日之后尝试 = 105, // 0x00000069 + 工商数据返回_企业信息或法定代表人信息不一致 = 1000, // 0x000003E8 + 对方不是粉丝 = 10700, // 0x000029CC + 发送消息失败_对方关闭了接收消息 = 10703, // 0x000029CF + 发送消息失败_48小时内用户未互动 = 10706, // 0x000029D2 + POST参数非法 = 20002, // 0x00004E22 + 获取access_token时AppSecret错误或者access_token无效 = 40001, // 0x00009C41 + + /// + /// 公众号:不合法的凭证类型 + /// 小程序:暂无生成权限 + /// + 不合法的凭证类型 = 40002, // 0x00009C42 + + 不合法的OpenID = 40003, // 0x00009C43 + 不合法的媒体文件类型 = 40004, // 0x00009C44 + 不合法的文件类型 = 40005, // 0x00009C45 + 不合法的文件大小 = 40006, // 0x00009C46 + 不合法的媒体文件id = 40007, // 0x00009C47 + 不合法的消息类型_40008 = 40008, // 0x00009C48 + 不合法的图片文件大小 = 40009, // 0x00009C49 + 不合法的语音文件大小 = 40010, // 0x00009C4A + 不合法的视频文件大小 = 40011, // 0x00009C4B + 不合法的缩略图文件大小 = 40012, // 0x00009C4C + + /// + /// 微信:不合法的APPID + /// 小程序:生成权限被封禁 + /// + 不合法的APPID = 40013, // 0x00009C4D + + 不合法的access_token = 40014, // 0x00009C4E + 不合法的菜单类型 = 40015, // 0x00009C4F + 不合法的按钮个数1 = 40016, // 0x00009C50 + 不合法的按钮个数2 = 40017, // 0x00009C51 + 不合法的按钮名字长度 = 40018, // 0x00009C52 + 不合法的按钮KEY长度 = 40019, // 0x00009C53 + 不合法的按钮URL长度 = 40020, // 0x00009C54 + 不合法的菜单版本号 = 40021, // 0x00009C55 + 不合法的子菜单级数 = 40022, // 0x00009C56 + 不合法的子菜单按钮个数 = 40023, // 0x00009C57 + 不合法的子菜单按钮类型 = 40024, // 0x00009C58 + 不合法的子菜单按钮名字长度 = 40025, // 0x00009C59 + 不合法的子菜单按钮KEY长度 = 40026, // 0x00009C5A + 不合法的子菜单按钮URL长度 = 40027, // 0x00009C5B + 不合法的自定义菜单使用用户 = 40028, // 0x00009C5C + 不合法的oauth_code = 40029, // 0x00009C5D + 不合法的refresh_token = 40030, // 0x00009C5E + 不合法的openid列表 = 40031, // 0x00009C5F + 不合法的openid列表长度 = 40032, // 0x00009C60 + 不合法的请求字符不能包含uxxxx格式的字符 = 40033, // 0x00009C61 + 不合法的参数 = 40035, // 0x00009C63 + template_id不正确 = 40037, // 0x00009C65 + 不合法的请求格式 = 40038, // 0x00009C66 + 不合法的URL长度 = 40039, // 0x00009C67 + 不合法的分组id = 40050, // 0x00009C72 + 分组名字不合法 = 40051, // 0x00009C73 + + /// + /// 公众号:输入参数有误 + /// 小程序:参数expire_time填写错误 + /// + 输入参数有误 = 40097, // 0x00009CA1 + + appsecret不正确 = 40125, // 0x00009CBD + 调用接口的IP地址不在白名单中 = 40164, // 0x00009CE4 + 参数path填写错误 = 40165, // 0x00009CE5 + 小程序Appid不存在 = 40166, // 0x00009CE6 + 参数query填写错误 = 40212, // 0x00009D14 + 缺少access_token参数 = 41001, // 0x0000A029 + 缺少appid参数 = 41002, // 0x0000A02A + 缺少refresh_token参数 = 41003, // 0x0000A02B + 缺少secret参数 = 41004, // 0x0000A02C + 缺少多媒体文件数据 = 41005, // 0x0000A02D + 缺少media_id参数 = 41006, // 0x0000A02E + 缺少子菜单数据 = 41007, // 0x0000A02F + 缺少oauth_code = 41008, // 0x0000A030 + 缺少openid = 41009, // 0x0000A031 + form_id不正确_或者过期 = 41028, // 0x0000A044 + form_id已被使用 = 41029, // 0x0000A045 + page不正确 = 41030, // 0x0000A046 + access_token超时 = 42001, // 0x0000A411 + refresh_token超时 = 42002, // 0x0000A412 + oauth_code超时 = 42003, // 0x0000A413 + 需要GET请求 = 43001, // 0x0000A7F9 + 需要POST请求 = 43002, // 0x0000A7FA + 需要HTTPS请求 = 43003, // 0x0000A7FB + 需要接收者关注 = 43004, // 0x0000A7FC + 需要好友关系 = 43005, // 0x0000A7FD + + /// [小程序订阅消息]用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系 + 用户拒绝接受消息 = 43101, // 0x0000A85D + + 没有权限 = 43104, // 0x0000A860 + 多媒体文件为空 = 44001, // 0x0000ABE1 + POST的数据包为空 = 44002, // 0x0000ABE2 + 图文消息内容为空 = 44003, // 0x0000ABE3 + 文本消息内容为空 = 44004, // 0x0000ABE4 + 多媒体文件大小超过限制 = 45001, // 0x0000AFC9 + 消息内容超过限制 = 45002, // 0x0000AFCA + 标题字段超过限制 = 45003, // 0x0000AFCB + 描述字段超过限制 = 45004, // 0x0000AFCC + 链接字段超过限制 = 45005, // 0x0000AFCD + 图片链接字段超过限制 = 45006, // 0x0000AFCE + 语音播放时间超过限制 = 45007, // 0x0000AFCF + 图文消息超过限制 = 45008, // 0x0000AFD0 + 接口调用超过限制 = 45009, // 0x0000AFD1 + 创建菜单个数超过限制 = 45010, // 0x0000AFD2 + 回复时间超过限制 = 45015, // 0x0000AFD7 + 系统分组不允许修改 = 45016, // 0x0000AFD8 + 分组名字过长 = 45017, // 0x0000AFD9 + 分组数量超过上限 = 45018, // 0x0000AFDA + 超出响应数量限制 = 45047, // 0x0000AFF7 + 创建的标签数过多请注意不能超过100个 = 45056, // 0x0000B000 + 标签名非法请注意不能和其他标签重名 = 45157, // 0x0000B065 + 标签名长度超过30个字节 = 45158, // 0x0000B066 + 不存在媒体数据 = 46001, // 0x0000B3B1 + 不存在的菜单版本 = 46002, // 0x0000B3B2 + 不存在的菜单数据 = 46003, // 0x0000B3B3 + 解析JSON_XML内容错误 = 47001, // 0x0000B799 + + /// [小程序订阅消息]模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错 + 模板参数不准确 = 47003, // 0x0000B79B + + api功能未授权 = 48001, // 0x0000BB81 + 用户未授权该api = 50001, // 0x0000C351 + 名称格式不合法 = 53010, // 0x0000CF12 + 名称检测命中频率限制 = 53011, // 0x0000CF13 + 禁止使用该名称 = 53012, // 0x0000CF14 + 公众号_名称与已有公众号名称重复_小程序_该名称与已有小程序名称重复 = 53013, // 0x0000CF15 + 公众号_公众号已有_名称A_时_需与该帐号相同主体才可申请_名称A_小程序_小程序已有_名称A_时_需与该帐号相同主体才可申请_名称A_ = 53014, // 0x0000CF16 + 公众号_该名称与已有小程序名称重复_需与该小程序帐号相同主体才可申请_小程序_该名称与已有公众号名称重复_需与该公众号帐号相同主体才可申请 = 53015, // 0x0000CF17 + 公众号_该名称与已有多个小程序名称重复_暂不支持申请_小程序_该名称与已有多个公众号名称重复_暂不支持申请 = 53016, // 0x0000CF18 + 公众号_小程序已有_名称A_时_需与该帐号相同主体才可申请_名称A_小程序_公众号已有_名称A_时_需与该帐号相同主体才可申请_名称A = 53017, // 0x0000CF19 + 名称命中微信号 = 53018, // 0x0000CF1A + 名称在保护期内 = 53019, // 0x0000CF1B + 法人姓名与微信号不一致 = 61070, // 0x0000EE8E + 系统错误system_error = 61450, // 0x0000F00A + 参数错误invalid_parameter = 61451, // 0x0000F00B + 无效客服账号invalid_kf_account = 61452, // 0x0000F00C + 客服帐号已存在kf_account_exsited = 61453, // 0x0000F00D + + /// + /// 客服帐号名长度超过限制(仅允许10个英文字符,不包括@及@后的公众号的微信号)(invalid kf_acount length) + /// + 客服帐号名长度超过限制 = 61454, // 0x0000F00E + + /// + /// 客服帐号名包含非法字符(仅允许英文+数字)(illegal character in kf_account) + /// + 客服帐号名包含非法字符 = 61455, // 0x0000F00F + + /// 客服帐号个数超过限制(10个客服账号)(kf_account count exceeded) + 客服帐号个数超过限制 = 61456, // 0x0000F010 + + 无效头像文件类型invalid_file_type = 61457, // 0x0000F011 + 日期格式错误 = 61500, // 0x0000F03C + 日期范围错误 = 61501, // 0x0000F03D + 发送消息失败_该用户已被加入黑名单_无法向此发送消息 = 62751, // 0x0000F51F + 门店不存在 = 65115, // 0x0000FE5B + 该门店状态不允许更新 = 65118, // 0x0000FE5E + 标签格式错误 = 85006, // 0x00014C0E + 页面路径错误 = 85007, // 0x00014C0F + 类目填写错误 = 85008, // 0x00014C10 + 已经有正在审核的版本 = 85009, // 0x00014C11 + item_list有项目为空 = 85010, // 0x00014C12 + 标题填写错误 = 85011, // 0x00014C13 + 无效的审核id = 85012, // 0x00014C14 + 版本输入错误 = 85015, // 0x00014C17 + 没有审核版本 = 85019, // 0x00014C1B + 审核状态未满足发布 = 85020, // 0x00014C1C + 状态不可变 = 85021, // 0x00014C1D + action非法 = 85022, // 0x00014C1E + 审核列表填写的项目数不在1到5以内 = 85023, // 0x00014C1F + 需要补充相应资料_填写org_code和other_files参数 = 85024, // 0x00014C20 + 管理员手机登记数量已超过上限 = 85025, // 0x00014C21 + 该微信号已绑定5个管理员 = 85026, // 0x00014C22 + 管理员身份证已登记过5次 = 85027, // 0x00014C23 + 该主体登记数量已超过上限 = 85028, // 0x00014C24 + 商家名称已被占用 = 85029, // 0x00014C25 + 不能使用该名称 = 85031, // 0x00014C27 + 该名称在侵权投诉保护期 = 85032, // 0x00014C28 + 名称包含违规内容或微信等保留字 = 85033, // 0x00014C29 + 商家名称在改名15天保护期内 = 85034, // 0x00014C2A + 需与该帐号相同主体才可申请 = 85035, // 0x00014C2B + 介绍中含有虚假混淆内容 = 85036, // 0x00014C2C + 头像或者简介修改达到每个月上限 = 85049, // 0x00014C39 + 正在审核中_请勿重复提交 = 85050, // 0x00014C3A + 请先成功创建门店后再调用 = 85053, // 0x00014C3D + 临时mediaid无效 = 85056, // 0x00014C40 + 链接错误 = 85066, // 0x00014C4A + 测试链接不是子链接 = 85068, // 0x00014C4C + 校验文件失败 = 85069, // 0x00014C4D + 个人类型小程序无法设置二维码规则 = 85070, // 0x00014C4E + 已添加该链接_请勿重复添加 = 85071, // 0x00014C4F + 该链接已被占用 = 85072, // 0x00014C50 + 二维码规则已满 = 85073, // 0x00014C51 + 小程序未发布_小程序必须先发布代码才可以发布二维码跳转规则 = 85074, // 0x00014C52 + 个人类型小程序无法设置二维码规则1 = 85075, // 0x00014C53 + 小程序没有线上版本_不能进行灰度 = 85079, // 0x00014C57 + 小程序提交的审核未审核通过 = 85080, // 0x00014C58 + 无效的发布比例 = 85081, // 0x00014C59 + 当前的发布比例需要比之前设置的高 = 85082, // 0x00014C5A + 小程序提审数量已达本月上限 = 85085, // 0x00014C5D + 提交代码审核之前需提前上传代码 = 85086, // 0x00014C5E + 小程序已使用_api_navigateToMiniProgram_请声明跳转_appid_列表后再次提交 = 85087, // 0x00014C5F + 不是由第三方代小程序进行调用 = 86000, // 0x00014FF0 + 不存在第三方的已经提交的代码 = 86001, // 0x00014FF1 + 小程序还未设置昵称_头像_简介_请先设置完后再重新提交 = 86002, // 0x00014FF2 + 无效微信号 = 86004, // 0x00014FF4 + + /// + /// 小程序为“签名错误”。对应公众号: 87009, “errmsg” : “reply is not exists” //该回复不存在 + /// + 签名错误 = 87009, // 0x000153E1 + + 现网已经在灰度发布_不能进行版本回退 = 87011, // 0x000153E3 + 该版本不能回退_可能的原因_1_无上一个线上版用于回退_2_此版本为已回退版本_不能回退_3_此版本为回退功能上线之前的版本_不能回退 = 87012, // 0x000153E4 + 内容含有违法违规内容 = 87014, // 0x000153E6 + 没有留言权限 = 88000, // 0x000157C0 + 该图文不存在 = 88001, // 0x000157C1 + 文章存在敏感信息 = 88002, // 0x000157C2 + 精选评论数已达上限 = 88003, // 0x000157C3 + 已被用户删除_无法精选 = 88004, // 0x000157C4 + 已经回复过了 = 88005, // 0x000157C5 + 回复超过长度限制或为0 = 88007, // 0x000157C7 + 该评论不存在 = 88008, // 0x000157C8 + 获取评论数目不合法 = 88010, // 0x000157CA + 该公众号_小程序已经绑定了开放平台帐号 = 89000, // 0x00015BA8 + 业务域名无更改_无需重复设置 = 89019, // 0x00015BBB + 尚未设置小程序业务域名_请先在第三方平台中设置小程序业务域名后在调用本接口 = 89020, // 0x00015BBC + 请求保存的域名不是第三方平台中已设置的小程序业务域名或子域名 = 89021, // 0x00015BBD + 业务域名数量超过限制_最多可以添加100个业务域名 = 89029, // 0x00015BC5 + 个人小程序不支持调用_setwebviewdomain_接口 = 89231, // 0x00015C8F + 内部错误 = 89247, // 0x00015C9F + 企业代码类型无效_请选择正确类型填写 = 89248, // 0x00015CA0 + 该主体已有任务执行中_距上次任务24h后再试 = 89249, // 0x00015CA1 + 未找到该任务 = 89250, // 0x00015CA2 + 待法人人脸核身校验 = 89251, // 0x00015CA3 + 法人_企业信息一致性校验中 = 89252, // 0x00015CA4 + 缺少参数 = 89253, // 0x00015CA5 + 第三方权限集不全_补全权限集全网发布后生效 = 89254, // 0x00015CA6 + 系统不稳定_请稍后再试_如多次失败请通过社区反馈 = 89401, // 0x00015D39 + 该审核单不在待审核队列_请检查是否已提交审核或已审完 = 89402, // 0x00015D3A + 本单属于平台不支持加急种类_请等待正常审核流程 = 89403, // 0x00015D3B + 本单已加速成功_请勿重复提交 = 89404, // 0x00015D3C + 本月加急额度不足_请提升提审质量以获取更多额度 = 89405, // 0x00015D3D + 该经营资质已添加_请勿重复添加 = 92000, // 0x00016760 + 附近地点添加数量达到上线_无法继续添加 = 92002, // 0x00016762 + 地点已被其它小程序占用 = 92003, // 0x00016763 + 附近功能被封禁 = 92004, // 0x00016764 + 地点正在审核中 = 92005, // 0x00016765 + 地点正在展示小程序 = 92006, // 0x00016766 + 地点审核失败 = 92007, // 0x00016767 + 程序未展示在该地点 = 92008, // 0x00016768 + 小程序未上架或不可见 = 92009, // 0x00016769 + 地点不存在 = 93010, // 0x00016B52 + 个人类型小程序不可用 = 93011, // 0x00016B53 + 已下发的模板消息法人并未确认且已超时_24h_未进行身份证校验 = 100001, // 0x000186A1 + 已下发的模板消息法人并未确认且已超时_24h_未进行人脸识别校验 = 100002, // 0x000186A2 + 已下发的模板消息法人并未确认且已超时_24h = 100003, // 0x000186A3 + 此账号已被封禁_无法操作 = 200011, // 0x00030D4B + 私有模板数已达上限_上限_50_个 = 200012, // 0x00030D4C + 此模版已被封禁_无法选用 = 200013, // 0x00030D4D + 模版tid参数错误 = 200014, // 0x00030D4E + 关键词列表kidList参数错误 = 200020, // 0x00030D54 + 场景描述sceneDesc参数错误 = 200021, // 0x00030D55 +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/WeekEnum.cs b/Admin.NET.Core/Enum/WeekEnum.cs new file mode 100644 index 0000000..279c1c6 --- /dev/null +++ b/Admin.NET.Core/Enum/WeekEnum.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 周枚举 +/// +[Description("周枚举")] +public enum WeekEnum +{ + /// + /// 周一 + /// + [Description("周一")] + Monday = 1, + + /// + /// 周二 + /// + [Description("周二")] + Tuesday = 2, + + /// + /// 周三 + /// + [Description("周三")] + Wednesday = 3, + + /// + /// 周四 + /// + [Description("周四")] + Thursday = 4, + + /// + /// 周五 + /// + [Description("周五")] + Friday = 5, + + /// + /// 周六 + /// + [Description("周六")] + Saturday = 6, + + /// + /// 周日 + /// + [Description("周日")] + Sunday = 7, +} \ No newline at end of file diff --git a/Admin.NET.Core/Enum/YesNoEnum.cs b/Admin.NET.Core/Enum/YesNoEnum.cs new file mode 100644 index 0000000..d26d141 --- /dev/null +++ b/Admin.NET.Core/Enum/YesNoEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 是否枚举 +/// +[Description("是否枚举")] +public enum YesNoEnum +{ + /// + /// 是 + /// + [Description("是"), Theme("success")] + Y = 1, + + /// + /// 否 + /// + [Description("否"), Theme("danger")] + N = 2 +} \ No newline at end of file diff --git a/Admin.NET.Core/EventBus/AppEventSubscriber.cs b/Admin.NET.Core/EventBus/AppEventSubscriber.cs new file mode 100644 index 0000000..e44ffd5 --- /dev/null +++ b/Admin.NET.Core/EventBus/AppEventSubscriber.cs @@ -0,0 +1,58 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件订阅 +/// +public class AppEventSubscriber : IEventSubscriber, ISingleton, IDisposable +{ + private static readonly ISugarQueryable SysTenantQueryable = App.GetService().Queryable(); + private readonly IServiceScope _serviceScope; + + public AppEventSubscriber(IServiceScopeFactory scopeFactory) + { + _serviceScope = scopeFactory.CreateScope(); + } + + /// + /// 增加异常日志 + /// + /// + /// + [EventSubscribe(CommonConst.AddExLog)] + public async Task CreateExLog(EventHandlerExecutingContext context) + { + // 切换日志独立数据库 + var db = SqlSugarSetup.ITenant.IsAnyConnection(SqlSugarConst.LogConfigId) + ? SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.LogConfigId) + : SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.MainConfigId); + await db.CopyNew().Insertable((SysLogEx)context.Source.Payload).ExecuteCommandAsync(); + } + + /// + /// 发送异常邮件 + /// + /// + /// + [EventSubscribe(CommonConst.SendErrorMail)] + public async Task SendOrderErrorMail(EventHandlerExecutingContext context) + { + long.TryParse(App.HttpContext?.User.FindFirst(ClaimConst.TenantId)?.Value, out var tenantId); + var tenant = await SysTenantQueryable.FirstAsync(t => t.Id == tenantId); + var title = $"{tenant?.Title} 系统异常"; + await _serviceScope.ServiceProvider.GetRequiredService().SendEmail(JSON.Serialize(context.Source.Payload), title); + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + _serviceScope.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/EventBus/EventConsumer.cs b/Admin.NET.Core/EventBus/EventConsumer.cs new file mode 100644 index 0000000..971d2e7 --- /dev/null +++ b/Admin.NET.Core/EventBus/EventConsumer.cs @@ -0,0 +1,118 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Redis 消息扩展 +/// +/// +public class EventConsumer : IDisposable +{ + /// + /// + /// + private Task _consumerTask; + + /// + /// + /// + private CancellationTokenSource _consumerCts; + + /// + /// 消费者 + /// + public IProducerConsumer Consumer { get; } + + /// + /// 消息回调 + /// + public event EventHandler Received; + + /// + /// 构造函数 + /// + /// + public EventConsumer(IProducerConsumer consumer) => Consumer = consumer; + + /// + /// 启动 + /// + /// + public void Start() + { + if (Consumer is null) + { + throw new InvalidOperationException("Subscribe first using the Consumer.Subscribe() function"); + } + if (_consumerTask != null) + { + return; + } + _consumerCts = new CancellationTokenSource(); + var ct = _consumerCts.Token; + _consumerTask = Task.Factory.StartNew(async () => + { + while (!ct.IsCancellationRequested) + { + try + { + var cr = Consumer.TakeOne(10); + if (cr == null) continue; + Received?.Invoke(this, cr); + } + catch (Exception ex) + { + Console.WriteLine($"消息消费异常: {ex.Message}"); + await Task.Delay(1000); // 短暂等待后继续尝试 + } + } + }, ct, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + /// + /// 停止 + /// + /// + public async Task Stop() + { + if (_consumerCts == null || _consumerTask == null) return; + _consumerCts.Cancel(); + try + { + await _consumerTask; + } + finally + { + _consumerTask = null; + _consumerCts = null; + } + } + + /// + /// 释放 + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// 释放 + /// + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_consumerTask != null) + { + Stop().Wait(); + } + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/EventBus/EventHandlerMonitor.cs b/Admin.NET.Core/EventBus/EventHandlerMonitor.cs new file mode 100644 index 0000000..7f229ed --- /dev/null +++ b/Admin.NET.Core/EventBus/EventHandlerMonitor.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class EventHandlerMonitor : IEventHandlerMonitor +{ + public Task OnExecutingAsync(EventHandlerExecutingContext context) + { + //_logger.LogInformation("执行之前:{EventId}", context.Source.EventId); + return Task.CompletedTask; + } + + public Task OnExecutedAsync(EventHandlerExecutedContext context) + { + //_logger.LogInformation("执行之后:{EventId}", context.Source.EventId); + + if (context.Exception != null) + { + Log.Error($"EventHandlerMonitor.OnExecutedAsync 执行出错啦:{context.Source.EventId}", context.Exception); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/EventBus/RabbitMQEventSourceStore.cs b/Admin.NET.Core/EventBus/RabbitMQEventSourceStore.cs new file mode 100644 index 0000000..5c28f6c --- /dev/null +++ b/Admin.NET.Core/EventBus/RabbitMQEventSourceStore.cs @@ -0,0 +1,146 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Threading.Channels; + +namespace Admin.NET.Core; + +/// +/// RabbitMQ自定义事件源存储器 +/// +public class RabbitMQEventSourceStore : IEventSourceStorer, IDisposable +{ + /// + /// 内存通道事件源存储器 + /// + private Channel _channelEventSource; + + /// + /// 路由键 + /// + private string _routeKey; + + /// + /// 连接对象 + /// + private IConnection _connection; + + /// + /// 通道对象 + /// + private IChannel _channel; + + /// + /// 构造函数 + /// + /// 连接工厂 + /// 路由键 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + public RabbitMQEventSourceStore(ConnectionFactory factory, string routeKey, int capacity) + { + InitEventSourceStore(factory, routeKey, capacity).GetAwaiter().GetResult(); + } + + /// + /// 初始化事件源存储器 + /// + /// 连接工厂 + /// 路由键 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + private async Task InitEventSourceStore(ConnectionFactory factory, string routeKey, int capacity) + { + // 配置通道(超出默认容量后进入等待) + var boundedChannelOptions = new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }; + // 创建有限容量通道 + _channelEventSource = Channel.CreateBounded(boundedChannelOptions); + + // 创建连接 + _connection = await factory.CreateConnectionAsync(); + // 路由键名 + _routeKey = routeKey; + + // 创建通道 + _channel = await _connection.CreateChannelAsync(); + + // 声明路由队列 + await _channel.QueueDeclareAsync(routeKey, false, false, false, null); + + // 创建消息订阅者 + var consumer = new AsyncEventingBasicConsumer(_channel); + + // 订阅消息并写入内存 Channel + consumer.ReceivedAsync += async (ch, ea) => + { + // 读取原始消息 + var stringEventSource = Encoding.UTF8.GetString(ea.Body.ToArray()); + + // 转换为 IEventSource,如果自定义了 EventSource,注意属性是可读可写 + var eventSource = JSON.Deserialize(stringEventSource); + + // 写入内存管道存储器 + await _channelEventSource.Writer.WriteAsync(eventSource); + + // 确认该消息已被消费 + await _channel.BasicAckAsync(ea.DeliveryTag, false); + }; + + // 启动消费者且设置为手动应答消息 + await _channel.BasicConsumeAsync(routeKey, false, consumer); + } + + /// + /// 将事件源写入存储器 + /// + /// 事件源对象 + /// 取消任务 Token + /// + public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken) + { + if (eventSource == default) + throw new ArgumentNullException(nameof(eventSource)); + + // 判断是否是 ChannelEventSource 或自定义的 EventSource + if (eventSource is ChannelEventSource source) + { + // 序列化及发布 + var data = Encoding.UTF8.GetBytes(JSON.Serialize(source)); + var props = new BasicProperties(); + props.ContentType = "text/plain"; + props.DeliveryMode = DeliveryModes.Persistent; + await _channel.BasicPublishAsync("", _routeKey, false, props, data); + } + else + { + // 处理动态订阅 + await _channelEventSource.Writer.WriteAsync(eventSource, cancellationToken); + } + } + + /// + /// 从存储器中读取一条事件源 + /// + /// 取消任务 Token + /// 事件源对象 + public async ValueTask ReadAsync(CancellationToken cancellationToken) + { + var eventSource = await _channelEventSource.Reader.ReadAsync(cancellationToken); + return eventSource; + } + + /// + /// 释放非托管资源 + /// + public void Dispose() + { + _channel.Dispose(); + _connection.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs b/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs new file mode 100644 index 0000000..f507e7b --- /dev/null +++ b/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs @@ -0,0 +1,165 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using NewLife.Caching.Queues; +using Newtonsoft.Json; +using System.Threading.Channels; + +namespace Admin.NET.Core; + +/// +/// Redis自定义事件源存储器 +/// +/// +/// 在集群部署时,一般每一个消息只由一个服务节点消费一次。 +/// 有些特殊情情要通知到服务器群中的每一个节点(比如需要强制加载某些配置、重点服务等), +/// 在这种情况下就要以“broadcast:”开头来定义EventId, +/// 本系统会把“broadcast:”开头的事件视为“广播消息”保证集群中的每一个服务节点都能消费得到这个消息 +/// +public sealed class RedisEventSourceStorer : IEventSourceStorer, IDisposable +{ + /// + /// 消费者 + /// + private readonly EventConsumer _eventConsumer; + + /// + /// 内存通道事件源存储器 + /// + private readonly Channel _channel; + + private IProducerConsumer _queueSingle; + + private RedisStream _queueBroadcast; + + private ILogger _logger; + + /// + /// 构造函数 + /// + /// Redis 连接对象 + /// 路由键 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + public RedisEventSourceStorer(ICacheProvider cacheProvider, string routeKey, int capacity) + { + _logger = App.GetRequiredService>(); + + // 配置通道,设置超出默认容量后进入等待 + var boundedChannelOptions = new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }; + + // 创建有限容量通道 + _channel = Channel.CreateBounded(boundedChannelOptions); + + //_redis = redis as FullRedis; + + // 创建广播消息订阅者,即所有服务器节点都能收到消息(用来发布重启、Reload配置等消息) + FullRedis redis = (FullRedis)cacheProvider.Cache; + var clusterOpt = App.GetConfig("Cluster", true); + _queueBroadcast = redis.GetStream(routeKey + ":broadcast"); + _queueBroadcast.Group = clusterOpt.ServerId;//根据服务器标识分配到不同的分组里 + _queueBroadcast.Expire = TimeSpan.FromSeconds(10);//消息10秒过期() + _queueBroadcast.ConsumeAsync(OnConsumeBroadcast); + + // 创建队列消息订阅者,只要有一个服务节点消费了消息即可 + _queueSingle = redis.GetQueue(routeKey + ":single"); + _eventConsumer = new EventConsumer(_queueSingle); + + // 订阅消息写入 Channel + _eventConsumer.Received += async (send, cr) => + { + // var oriColor = Console.ForegroundColor; + try + { + ChannelEventSource ces = (ChannelEventSource)cr; + await ConsumeChannelEventSourceAsync(ces, ces.CancellationToken); + } + catch (Exception e) + { + _logger.LogError(e, "处理Received中的消息产生错误!"); + } + }; + _eventConsumer.Start(); + } + + private async Task OnConsumeBroadcast(string source, Message message, CancellationToken token) + { + ChannelEventSource ces = JsonConvert.DeserializeObject(source); + await ConsumeChannelEventSourceAsync(ces, token); + } + + private async Task ConsumeChannelEventSourceAsync(ChannelEventSource ces, CancellationToken cancel = default) + { + // 打印测试事件 + if (ces.EventId != null && ces.EventId.IndexOf(":Test") > 0) + { + var oriColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"有消息要处理{ces.EventId},{ces.Payload}"); + Console.ForegroundColor = oriColor; + } + await _channel.Writer.WriteAsync(ces, cancel); + } + + /// + /// 将事件源写入存储器 + /// + /// 事件源对象 + /// 取消任务 Token + /// + public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken) + { + // 空检查 + if (eventSource == default) + throw new ArgumentNullException(nameof(eventSource)); + + // 这里判断是否是 ChannelEventSource 或者 自定义的 EventSource + if (eventSource is ChannelEventSource source) + { + // 异步发布 + await Task.Factory.StartNew(() => + { + if (source.EventId != null && source.EventId.StartsWith("broadcast:")) + { + string str = JsonConvert.SerializeObject(source); + _queueBroadcast.Add(str); + } + else + { + _queueSingle.Add(source); + } + }, cancellationToken, TaskCreationOptions.LongRunning, System.Threading.Tasks.TaskScheduler.Default); + } + else + { + // 处理动态订阅问题 + await _channel.Writer.WriteAsync(eventSource, cancellationToken); + } + } + + /// + /// 从存储器中读取一条事件源 + /// + /// 取消任务 Token + /// 事件源对象 + public async ValueTask ReadAsync(CancellationToken cancellationToken) + { + // 读取一条事件源 + var eventSource = await _channel.Reader.ReadAsync(cancellationToken); + return eventSource; + } + + /// + /// 释放非托管资源 + /// + public async void Dispose() + { + await _eventConsumer.Stop(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/EventBus/RedisQueue.cs b/Admin.NET.Core/EventBus/RedisQueue.cs new file mode 100644 index 0000000..3478e25 --- /dev/null +++ b/Admin.NET.Core/EventBus/RedisQueue.cs @@ -0,0 +1,202 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using NewLife.Caching.Queues; + +namespace Admin.NET.Core; + +/// +/// Redis 消息队列 +/// +public static class RedisQueue +{ + private static ICacheProvider _cacheProvider = App.GetRequiredService(); + + /// 创建Redis消息队列。默认消费一次,指定消费者group时使用STREAM结构,支持多消费组共享消息 + /// + /// 使用队列时,可根据是否设置消费组来决定使用简单队列还是完整队列。 简单队列(如RedisQueue)可用作命令队列,Topic很多,但几乎没有消息。 完整队列(如RedisStream)可用作消息队列,Topic很少,但消息很多,并且支持多消费组。 + /// + /// + /// 主题 + /// 消费组。未指定消费组时使用简单队列(如RedisQueue),指定消费组时使用完整队列(如RedisStream) + /// + public static IProducerConsumer GetQueue(String topic, String group = null) + { + // 队列需要单列 + var key = $"myStream:{topic}"; + if (_cacheProvider.InnerCache.TryGetValue>(key, out var queue)) return queue; + + queue = _cacheProvider.GetQueue(topic, group); + _cacheProvider.Cache.Set(key, queue); + + return queue; + } + + /// + /// 获取可信队列,需要确认 + /// + /// + /// + /// + public static RedisReliableQueue GetRedisReliableQueue(string topic) + { + // 队列需要单列 + var key = $"myQueue:{topic}"; + if (_cacheProvider.InnerCache.TryGetValue>(key, out var queue)) return queue; + + queue = (_cacheProvider.Cache as FullRedis).GetReliableQueue(topic); + _cacheProvider.Cache.Set(key, queue); + + return queue; + } + + /// + /// 可信队列回滚 + /// + /// + /// + /// + public static int RollbackAllAck(string topic, int retryInterval = 60) + { + var queue = GetRedisReliableQueue(topic); + queue.RetryInterval = retryInterval; + return queue.RollbackAllAck(); + } + + /// + /// 发送一个数据列表到可信队列 + /// + /// + /// + /// + /// + public static int AddReliableQueueList(string topic, List value) + { + var queue = GetRedisReliableQueue(topic); + var count = queue.Count; + var result = queue.Add(value.ToArray()); + return result - count; + } + + /// + /// 发送一条数据到可信队列 + /// + /// + /// + /// + /// + public static int AddReliableQueue(string topic, T value) + { + var queue = GetRedisReliableQueue(topic); + var count = queue.Count; + var result = queue.Add(value); + return result - count; + } + + /// + /// 获取延迟队列 + /// + /// + /// + /// + public static RedisDelayQueue GetDelayQueue(string topic) + { + // 队列需要单列 + var key = $"myDelay:{topic}"; + if (_cacheProvider.InnerCache.TryGetValue>(key, out var queue)) return queue; + + queue = (_cacheProvider.Cache as FullRedis).GetDelayQueue(topic); + _cacheProvider.Cache.Set(key, queue); + + return queue; + } + + /// + /// 发送一条数据到延迟队列 + /// + /// + /// + /// 延迟时间。单位秒 + /// + /// + public static int AddDelayQueue(string topic, T value, int delay) + { + var queue = GetDelayQueue(topic); + return queue.Add(value, delay); + } + + /// + /// 发送数据列表到延迟队列 + /// + /// + /// + /// + /// 延迟时间。单位秒 + /// + public static int AddDelayQueue(string topic, List value, int delay) + { + var queue = GetDelayQueue(topic); + queue.Delay = delay; + return queue.Add(value.ToArray()); + } + + /// + /// 在可信队列获取一条数据 + /// + /// + /// + /// + public static T ReliableTakeOne(string topic) + { + var queue = GetRedisReliableQueue(topic); + return queue.TakeOne(1); + } + + /// + /// 异步在可信队列获取一条数据 + /// + /// + /// + /// + public static async Task ReliableTakeOneAsync(string topic) + { + var queue = GetRedisReliableQueue(topic); + return await queue.TakeOneAsync(1); + } + + /// + /// 在可信队列获取多条数据 + /// + /// + /// + /// + /// + public static List ReliableTake(string topic, int count) + { + var queue = GetRedisReliableQueue(topic); + return queue.Take(count).ToList(); + } + + /// + /// 申请分布式锁 + /// + /// 要锁定的key + /// 申请锁等待的时间,单位毫秒 + /// 锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒 + /// 失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败 + /// + public static 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; + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs b/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs new file mode 100644 index 0000000..59f9cec --- /dev/null +++ b/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件执行器-超时控制、失败重试熔断等等 +/// +public class RetryEventHandlerExecutor : IEventHandlerExecutor +{ + public async Task ExecuteAsync(EventHandlerExecutingContext context, Func handler) + { + var eventSubscribeAttribute = context.Attribute; + // 判断是否自定义了重试失败回调服务 + var fallbackPolicyService = eventSubscribeAttribute?.FallbackPolicy == null + ? null + : App.GetRequiredService(eventSubscribeAttribute.FallbackPolicy) as IEventFallbackPolicy; + + await Retry.InvokeAsync(async () => + { + try + { + await handler(context); + } + catch (Exception ex) + { + Log.Error($"Invoke EventHandler {context.Source.EventId} Error", ex); + throw; + } + } + , eventSubscribeAttribute?.NumRetries ?? 0 + , eventSubscribeAttribute?.RetryTimeout ?? 1000 + , exceptionTypes: eventSubscribeAttribute?.ExceptionTypes + , fallbackPolicy: fallbackPolicyService == null ? null : async (Exception ex) => { await fallbackPolicyService.CallbackAsync(context, ex); } + , retryAction: (total, times) => + { + // 输出重试日志 + Log.Warning($"Retrying {times}/{total} times for EventHandler {context.Source.EventId}"); + }); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/ConsoleLogoSetup.cs b/Admin.NET.Core/Extension/ConsoleLogoSetup.cs new file mode 100644 index 0000000..be20e97 --- /dev/null +++ b/Admin.NET.Core/Extension/ConsoleLogoSetup.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 控制台logo +/// +public static class ConsoleLogoSetup +{ + public static void AddConsoleLogo(this IServiceCollection services) + { + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine(@" + _ _ _ _ ______ _______ + /\ | | (_) | \ | | ____|__ __| + / \ __| |_ __ ___ _ _ __ | \| | |__ | | + / /\ \ / _` | '_ ` _ \| | '_ \ | . ` | __| | | + / ____ \ (_| | | | | | | | | | |_| |\ | |____ | | + /_/ \_\__,_|_| |_| |_|_|_| |_(_)_| \_|______| |_| "); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(@"让.NET更简单、更通用、更流行!"); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/DataTypeExtension.cs b/Admin.NET.Core/Extension/DataTypeExtension.cs new file mode 100644 index 0000000..0f9d4ab --- /dev/null +++ b/Admin.NET.Core/Extension/DataTypeExtension.cs @@ -0,0 +1,109 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 基本数据类型扩展(作为NewLife.Core的Utility的补充) +/// +public static class DataTypeExtension +{ + /// 转为SByte整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static sbyte ToSByte(this object value, sbyte defaultValue = default) + { + if (value is sbyte num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (sbyte.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为Byte整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static byte ToByte(this object value, byte defaultValue = default) + { + if (value is byte num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (byte.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为Int16整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static short ToInt16(this object value, short defaultValue = default) + { + if (value is short num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (short.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为UInt16整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static ushort ToUInt16(this object value, ushort defaultValue = default) + { + if (value is ushort num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (ushort.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为UInt32整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static uint ToUInt32(this object value, uint defaultValue = default) + { + if (value is uint num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (uint.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为UInt64整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static ulong ToUInt64(this object value, ulong defaultValue = default) + { + if (value is ulong num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (ulong.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/EnumExtension.cs b/Admin.NET.Core/Extension/EnumExtension.cs new file mode 100644 index 0000000..6900d76 --- /dev/null +++ b/Admin.NET.Core/Extension/EnumExtension.cs @@ -0,0 +1,239 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举拓展 +/// +public static class EnumExtension +{ + // 枚举显示字典缓存 + private static readonly ConcurrentDictionary> EnumDisplayValueDict = new(); + + // 枚举值字典缓存 + private static readonly ConcurrentDictionary> EnumNameValueDict = new(); + + // 枚举类型缓存 + private static ConcurrentDictionary _enumTypeDict; + + /// + /// 获取枚举对象Key与名称的字典(缓存) + /// + /// + /// + public static Dictionary GetEnumDictionary(this Type enumType) + { + if (!enumType.IsEnum) + throw new ArgumentException("Type '" + enumType.Name + "' is not an enum."); + + // 查询缓存 + var enumDic = EnumNameValueDict.TryGetValue(enumType, out var value) ? value : new Dictionary(); + if (enumDic.Count != 0) + return enumDic; + // 取枚举类型的Key/Value字典集合 + enumDic = GetEnumDictionaryItems(enumType); + + // 缓存 + EnumNameValueDict[enumType] = enumDic; + + return enumDic; + } + + /// + /// 获取枚举对象Key与名称的字典 + /// + /// + /// + private static Dictionary GetEnumDictionaryItems(this Type enumType) + { + // 获取类型的字段,初始化一个有限长度的字典 + var enumFields = enumType.GetFields(BindingFlags.Public | BindingFlags.Static); + Dictionary enumDic = new(enumFields.Length); + + // 遍历字段数组获取key和name + foreach (var enumField in enumFields) + { + var intValue = (int)enumField.GetValue(enumType)!; + enumDic[intValue] = enumField.Name; + } + + return enumDic; + } + + /// + /// 获取枚举类型key与描述的字典(缓存) + /// + /// + /// + /// + public static Dictionary GetEnumDescDictionary(this Type enumType) + { + if (!enumType.IsEnum) + throw new ArgumentException("Type '" + enumType.Name + "' is not an enum."); + + // 查询缓存 + var enumDic = EnumDisplayValueDict.TryGetValue(enumType, out var value) + ? value + : new Dictionary(); + if (enumDic.Count != 0) + return enumDic; + // 取枚举类型的Key/Value字典集合 + enumDic = GetEnumDescDictionaryItems(enumType); + + // 缓存 + EnumDisplayValueDict[enumType] = enumDic; + + return enumDic; + } + + /// + /// 获取枚举类型key与描述的字典(没有描述则获取name) + /// + /// + /// + /// + private static Dictionary GetEnumDescDictionaryItems(this Type enumType) + { + // 获取类型的字段,初始化一个有限长度的字典 + var enumFields = enumType.GetFields(BindingFlags.Public | BindingFlags.Static); + Dictionary enumDic = new(enumFields.Length); + + // 遍历字段数组获取key和name + foreach (var enumField in enumFields) + { + var intValue = (int)enumField.GetValue(enumType)!; + var desc = enumField.GetDescriptionValue(); + enumDic[intValue] = desc != null && !string.IsNullOrEmpty(desc.Description) ? desc.Description : enumField.Name; + } + + return enumDic; + } + + /// + /// 从程序集中查找指定枚举类型 + /// + /// + /// + /// + public static Type TryToGetEnumType(Assembly assembly, string typeName) + { + // 枚举缓存为空则重新加载枚举类型字典 + _enumTypeDict ??= LoadEnumTypeDict(assembly); + + // 按名称查找 + return _enumTypeDict.TryGetValue(typeName, out var value) ? value : null; + } + + /// + /// 从程序集中加载所有枚举类型 + /// + /// + /// + private static ConcurrentDictionary LoadEnumTypeDict(Assembly assembly) + { + // 取程序集中所有类型 + var typeArray = assembly.GetTypes(); + + // 过滤非枚举类型,转成字典格式并返回 + var dict = typeArray.Where(o => o.IsEnum).ToDictionary(o => o.Name, o => o); + ConcurrentDictionary enumTypeDict = new(dict); + return enumTypeDict; + } + + /// + /// 获取枚举的Description + /// + /// + /// + public static string GetEnumDescription(this Enum value) + { + return value.GetType().GetField(value.ToString())?.GetCustomAttribute()?.Description; + } + + /// + /// 获取枚举的Description + /// + /// + /// + public static string GetEnumDescription(this object value) + { + return value.GetType().GetField(value.ToString()!)?.GetCustomAttribute()?.Description; + } + + /// + /// 获取枚举的Theme + /// + /// + /// + public static string GetTheme(this object value) + { + return value.GetType().GetField(value.ToString()!)?.GetCustomAttribute()?.Theme; + } + + /// + /// 将枚举转成枚举信息集合 + /// + /// + /// + public static List EnumToList(this Type type) + { + if (!type.IsEnum) + throw new ArgumentException("Type '" + type.Name + "' is not an enum."); + var arr = Enum.GetNames(type); + return arr.Select(sl => + { + var item = Enum.Parse(type, sl); + return new EnumEntity + { + Name = item.ToString(), + Describe = item.GetEnumDescription() ?? item.ToString(), + Theme = item.GetTheme() ?? string.Empty, + Value = item.GetHashCode() + }; + }).ToList(); + } + + /// + /// 枚举ToList + /// + /// + /// + /// + public static List EnumToList(this Type type) + { + if (!type.IsEnum) + throw new ArgumentException("Type '" + type.Name + "' is not an enum."); + var arr = Enum.GetNames(type); + return arr.Select(name => (T)Enum.Parse(type, name)).ToList(); + } +} + +/// +/// 枚举实体 +/// +public class EnumEntity +{ + /// + /// 枚举的描述 + /// + public string Describe { get; set; } + + /// + /// 枚举的样式 + /// + public string Theme { get; set; } + + /// + /// 枚举名称 + /// + public string Name { get; set; } + + /// + /// 枚举对象的值 + /// + public int Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/EnumerableExtension.cs b/Admin.NET.Core/Extension/EnumerableExtension.cs new file mode 100644 index 0000000..de6c71e --- /dev/null +++ b/Admin.NET.Core/Extension/EnumerableExtension.cs @@ -0,0 +1,109 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据集合拓展类 +/// +public static class EnumerableExtension +{ + private static readonly ConcurrentDictionary PropertyCache = new(); + + /// + /// 查询有父子关系的数据集 + /// + /// 数据集 + /// 主键ID字段 + /// 父级字段 + /// 顶级节点父级字段值 + /// 是否包含顶级节点本身 + /// + public static IEnumerable ToChildList(this IEnumerable list, + Expression> idExpression, + Expression> parentIdExpression, + object topParentIdValue, + bool isContainOneself = true) + { + if (list == null || !list.Any()) return Enumerable.Empty(); + + var propId = GetPropertyInfo(idExpression); + var propParentId = GetPropertyInfo(parentIdExpression); + + // 查找所有顶级节点 + var topNodes = list.Where(item => Equals(propId.GetValue(item), topParentIdValue)).ToList(); + + return TraverseHierarchy(list, propId, propParentId, topNodes, isContainOneself); + } + + /// + /// 查询有父子关系的数据集 + /// + /// 数据集 + /// 主键ID字段 + /// 父级字段 + /// 顶级节点的选择条件 + /// 是否包含顶级节点本身 + /// + public static IEnumerable ToChildList(this IEnumerable list, + Expression> idExpression, + Expression> parentIdExpression, + Expression> topLevelPredicate, + bool isContainOneself = true) + { + if (list == null || !list.Any()) return Enumerable.Empty(); + + // 获取顶级节点 + var topNodes = list.Where(topLevelPredicate.Compile()).ToList(); + + if (!topNodes.Any()) return Enumerable.Empty(); + + var idPropertyInfo = GetPropertyInfo(idExpression); + var parentPropertyInfo = GetPropertyInfo(parentIdExpression); + + return TraverseHierarchy(list, idPropertyInfo, parentPropertyInfo, topNodes, isContainOneself); + } + + /// + /// 辅助方法,从表达式中提取属性信息并使用临时缓存 + /// + private static PropertyInfo GetPropertyInfo(Expression> expression) + { + // 使用 ConcurrentDictionary 确保线程安全 + return PropertyCache.GetOrAdd(typeof(T).FullName + "." + ((MemberExpression)expression.Body).Member.Name, k => + { + if (expression.Body is UnaryExpression { Operand: MemberExpression member }) return (PropertyInfo)member.Member; + if (expression.Body is MemberExpression memberExpression) return (PropertyInfo)memberExpression.Member; + throw Oops.Oh("表达式必须是一个属性访问: " + expression); + }); + } + + /// + /// 使用队列遍历层级结构 + /// + private static IEnumerable TraverseHierarchy(IEnumerable list, + PropertyInfo idPropertyInfo, + PropertyInfo parentPropertyInfo, + List topNodes, + bool isContainOneself) + { + var queue = new Queue(topNodes); + var result = new HashSet(topNodes); + + while (queue.Count > 0) + { + var currentNode = queue.Dequeue(); + var children = list.Where(item => Equals(parentPropertyInfo.GetValue(item), idPropertyInfo.GetValue(currentNode))).ToList(); + children.Where(child => result.Add(child)).ForEach(child => queue.Enqueue(child)); + } + if (isContainOneself) return result; + + // 如果不需要包含顶级节点本身,则移除它们 + topNodes.ForEach(e => result.Remove(e)); + + return result; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/HttpContextExtension.cs b/Admin.NET.Core/Extension/HttpContextExtension.cs new file mode 100644 index 0000000..88074a7 --- /dev/null +++ b/Admin.NET.Core/Extension/HttpContextExtension.cs @@ -0,0 +1,94 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +public static class HttpContextExtension +{ + public static async Task GetExternalProvidersAsync(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + var schemes = context.RequestServices.GetRequiredService(); + + return (from scheme in await schemes.GetAllSchemesAsync() + where !string.IsNullOrEmpty(scheme.DisplayName) + select scheme).ToArray(); + } + + public static async Task IsProviderSupportedAsync(this HttpContext context, string provider) + { + ArgumentNullException.ThrowIfNull(context); + + return (from scheme in await context.GetExternalProvidersAsync() + where string.Equals(scheme.Name, provider, StringComparison.OrdinalIgnoreCase) + select scheme).Any(); + } + + /// + /// 获取设备信息 + /// + /// + /// + public static string GetClientDeviceInfo(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return CommonUtil.GetClientDeviceInfo(context.Request.Headers.UserAgent); + } + + /// + /// 获取浏览器信息 + /// + /// + /// + public static string GetClientBrowser(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + string userAgent = context.Request.Headers.UserAgent; + try + { + if (userAgent != null) + { + var client = Parser.GetDefault().Parse(userAgent); + if (client.Device.IsSpider) + return "爬虫"; + return $"{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}"; + } + } + catch + { } + return "未知"; + } + + /// + /// 获取操作系统信息 + /// + /// + /// + public static string GetClientOs(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + string userAgent = context.Request.Headers.UserAgent; + try + { + if (userAgent != null) + { + var client = Parser.GetDefault().Parse(userAgent); + if (client.Device.IsSpider) + return "爬虫"; + return $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}"; + } + } + catch + { } + return "未知"; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/ListExtensions.cs b/Admin.NET.Core/Extension/ListExtensions.cs new file mode 100644 index 0000000..dcf9e9c --- /dev/null +++ b/Admin.NET.Core/Extension/ListExtensions.cs @@ -0,0 +1,48 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class ListExtensions +{ + public static async Task ForEachAsync(this List list, Func func) + { + foreach (var value in list) + { + await func(value); + } + } + + public static async Task ForEachAsync(this IEnumerable source, Func action) + { + foreach (var value in source) + { + await action(value); + } + } + + public static void ForEach(this IEnumerable enumerable, Action consumer) + { + foreach (T item in enumerable) + { + consumer(item); + } + } + + public static void AddRange(this IList list, IEnumerable items) + { + if (list is List list2) + { + list2.AddRange(items); + return; + } + + foreach (T item in items) + { + list.Add(item); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/ObjectExtension.cs b/Admin.NET.Core/Extension/ObjectExtension.cs new file mode 100644 index 0000000..21fd46b --- /dev/null +++ b/Admin.NET.Core/Extension/ObjectExtension.cs @@ -0,0 +1,521 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core; + +/// +/// 对象拓展 +/// +[SuppressSniffer] +public static partial class ObjectExtension +{ + /// + /// 类型属性列表映射表 + /// + private static readonly ConcurrentDictionary PropertyCache = new(); + + /// + /// 脱敏特性缓存映射表 + /// + private static readonly ConcurrentDictionary AttributeCache = new(); + + /// + /// 判断类型是否实现某个泛型 + /// + /// 类型 + /// 泛型类型 + /// bool + public static bool HasImplementedRawGeneric(this Type type, Type generic) + { + // 检查接口类型 + var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); + if (isTheRawGenericType) return true; + + // 检查类型 + while (type != null && type != typeof(object)) + { + isTheRawGenericType = IsTheRawGenericType(type); + if (isTheRawGenericType) return true; + type = type.BaseType; + } + + return false; + + // 判断逻辑 + bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); + } + + /// + /// 将字典转化为QueryString格式 + /// + /// + /// + /// + public static string ToQueryString(this Dictionary dict, bool urlEncode = true) + { + return string.Join("&", dict.Select(p => $"{(urlEncode ? p.Key?.UrlEncode() : "")}={(urlEncode ? p.Value?.UrlEncode() : "")}")); + } + + /// + /// 将字符串URL编码 + /// + /// + /// + public static string UrlEncode(this string str) + { + return string.IsNullOrEmpty(str) ? "" : System.Uri.EscapeDataString(str); + } + + /// + /// 对象序列化成Json字符串 + /// + /// + /// + public static string ToJson(this object obj) + { + var jsonSettings = SetNewtonsoftJsonSetting(); + return JSON.GetJsonSerializer().Serialize(obj, jsonSettings); + } + + private static JsonSerializerSettings SetNewtonsoftJsonSetting() + { + JsonSerializerSettings setting = new JsonSerializerSettings(); + setting.DateFormatHandling = DateFormatHandling.IsoDateFormat; + setting.DateTimeZoneHandling = DateTimeZoneHandling.Local; + setting.DateFormatString = "yyyy-MM-dd HH:mm:ss"; // 时间格式化 + setting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // 忽略循环引用 + //setting.ContractResolver = new HelErpContractResolver("StartTime", customName); + return setting; + } + + /// + /// Json字符串反序列化成对象 + /// + /// + /// + /// + public static T ToObject(this string json) + { + return JSON.GetJsonSerializer().Deserialize(json); + } + + /// + /// 将object转换为long,若失败则返回0 + /// + /// + /// + public static long ParseToLong(this object obj) + { + try + { + return long.Parse(obj.ToString()); + } + catch + { + return 0L; + } + } + + /// + /// 将object转换为long,若失败则返回指定值 + /// + /// + /// + /// + public static long ParseToLong(this string str, long defaultValue) + { + try + { + return long.Parse(str); + } + catch + { + return defaultValue; + } + } + + /// + /// 将object转换为double,若失败则返回0 + /// + /// + /// + public static double ParseToDouble(this object obj) + { + try + { + return double.Parse(obj.ToString()); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为double,若失败则返回指定值 + /// + /// + /// + /// + public static double ParseToDouble(this object str, double defaultValue) + { + try + { + return double.Parse(str.ToString()); + } + catch + { + return defaultValue; + } + } + + /// + /// 将string转换为DateTime,若失败则返回日期最小值 + /// + /// + /// + public static DateTime ParseToDateTime(this string str) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return DateTime.MinValue; + } + if (str.Contains('-') || str.Contains('/')) + { + return DateTime.Parse(str); + } + else + { + int length = str.Length; + switch (length) + { + case 4: + return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture); + + case 6: + return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture); + + case 8: + return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture); + + case 10: + return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture); + + case 12: + return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture); + + case 14: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + + default: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + } + } + } + catch + { + return DateTime.MinValue; + } + } + + /// + /// 将string转换为DateTime,若失败则返回默认值 + /// + /// + /// + /// + public static DateTime ParseToDateTime(this string str, DateTime? defaultValue) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return defaultValue.GetValueOrDefault(); + } + if (str.Contains('-') || str.Contains('/')) + { + return DateTime.Parse(str); + } + else + { + int length = str.Length; + switch (length) + { + case 4: + return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture); + + case 6: + return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture); + + case 8: + return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture); + + case 10: + return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture); + + case 12: + return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture); + + case 14: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + + default: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + } + } + } + catch + { + return defaultValue.GetValueOrDefault(); + } + } + + /// + /// 将 string 时间日期格式转换成字符串 如 {yyyy} => 2024 + /// + /// + /// + public static string ParseToDateTimeForRep(this string str) + { + if (string.IsNullOrWhiteSpace(str)) + str = $"{DateTime.Now.Year}/{DateTime.Now.Month}/{DateTime.Now.Day}"; + + var date = DateTime.Now; + var reg = new Regex(@"(\{.+?})"); + var match = reg.Matches(str); + match.ToList().ForEach(u => + { + var temp = date.ToString(u.ToString().Substring(1, u.Length - 2)); + str = str.Replace(u.ToString(), temp); + }); + return str; + } + + /// + /// 是否有值 + /// + /// + /// + public static bool IsNullOrEmpty(this object obj) + { + return obj == null || string.IsNullOrEmpty(obj.ToString()); + } + + /// + /// 字符串掩码 + /// + /// 字符串 + /// 掩码符 + /// + public static string Mask(this string str, char mask = '*') + { + if (string.IsNullOrWhiteSpace(str?.Trim())) + return str; + + str = str.Trim(); + var masks = mask.ToString().PadLeft(4, mask); + return str.Length switch + { + >= 11 => Regex.Replace(str, "(.{3}).*(.{4})", $"$1{masks}$2"), + 10 => Regex.Replace(str, "(.{3}).*(.{3})", $"$1{masks}$2"), + 9 => Regex.Replace(str, "(.{2}).*(.{3})", $"$1{masks}$2"), + 8 => Regex.Replace(str, "(.{2}).*(.{2})", $"$1{masks}$2"), + 7 => Regex.Replace(str, "(.{1}).*(.{2})", $"$1{masks}$2"), + 6 => Regex.Replace(str, "(.{1}).*(.{1})", $"$1{masks}$2"), + _ => Regex.Replace(str, "(.{1}).*", $"$1{masks}") + }; + } + + /// + /// 身份证号掩码 + /// + /// 身份证号 + /// 掩码符 + /// + public static string MaskIdCard(this string idCard, char mask = '*') + { + if (!idCard.TryValidate(ValidationTypes.IDCard).IsValid) return idCard; + + var masks = mask.ToString().PadLeft(8, mask); + return Regex.Replace(idCard, @"^(.{6})(.*)(.{4})$", $"$1{masks}$3"); + } + + /// + /// 邮箱掩码 + /// + /// 邮箱 + /// 掩码符 + /// + public static string MaskEmail(this string email, char mask = '*') + { + if (!email.TryValidate(ValidationTypes.EmailAddress).IsValid) return email; + + var pos = email.IndexOf("@"); + return Mask(email[..pos], mask) + email[pos..]; + } + + /// + /// 将字符串转为值类型,若没有得到或者错误返回为空 + /// + /// 指定值类型 + /// 传入字符串 + /// 可空值 + public static T? ParseTo(this string str) where T : struct + { + try + { + if (!string.IsNullOrWhiteSpace(str)) + { + MethodInfo method = typeof(T).GetMethod("Parse", new Type[] { typeof(string) }); + if (method != null) + { + T result = (T)method.Invoke(null, new string[] { str }); + return result; + } + } + } + catch + { + } + return null; + } + + /// + /// 将字符串转为值类型,若没有得到或者错误返回为空 + /// + /// 传入字符串 + /// 目标类型 + /// 可空值 + public static object ParseTo(this string str, Type type) + { + try + { + if (type.Name == "String") + return str; + + if (!string.IsNullOrWhiteSpace(str)) + { + var _type = type; + if (type.Name.StartsWith("Nullable")) + _type = type.GetGenericArguments()[0]; + + MethodInfo method = _type.GetMethod("Parse", new Type[] { typeof(string) }); + if (method != null) + return method.Invoke(null, new string[] { str }); + } + } + catch + { + } + return null; + } + + /// + /// 将一个对象属性值赋给另一个指定对象属性, 只复制相同属性的 + /// + /// 原数据对象 + /// 目标数据对象 + /// 属性集,键为原属性,值为目标属性 + /// 属性集,目标不修改的属性 + public static void CopyTo(object src, object target, Dictionary changeProperties = null, string[] unChangeProperties = null) + { + if (src == null || target == null) + throw new ArgumentException("src == null || target == null "); + + var SourceType = src.GetType(); + var TargetType = target.GetType(); + + if (changeProperties == null || changeProperties.Count == 0) + { + var fields = TargetType.GetProperties(); + changeProperties = fields.Select(m => m.Name).ToDictionary(m => m); + } + + if (unChangeProperties == null || unChangeProperties.Length == 0) + { + foreach (var item in changeProperties) + { + var srcProperty = SourceType.GetProperty(item.Key); + if (srcProperty != null) + { + var sourceVal = srcProperty.GetValue(src, null); + + var tarProperty = TargetType.GetProperty(item.Value); + tarProperty?.SetValue(target, sourceVal, null); + } + } + } + else + { + foreach (var item in changeProperties) + { + if (!unChangeProperties.Any(m => m == item.Value)) + { + var srcProperty = SourceType.GetProperty(item.Key); + if (srcProperty != null) + { + var sourceVal = srcProperty.GetValue(src, null); + + var tarProperty = TargetType.GetProperty(item.Value); + tarProperty?.SetValue(target, sourceVal, null); + } + } + } + } + } + + /// + /// 深复制 + /// + /// 深复制源对象 + /// 对象 + /// + public static T DeepCopy(this T obj) + { + var jsonSettings = SetNewtonsoftJsonSetting(); + var json = JSON.Serialize(obj, jsonSettings); + return JSON.Deserialize(json); + } + + /// + /// 对带有特性字段进行脱敏处理 + /// + public static T MaskSensitiveData(this T obj) where T : class + { + if (obj == null) return null; + + var type = typeof(T); + + // 获取或缓存属性集合 + var properties = PropertyCache.GetOrAdd(type, t => + t.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.PropertyType == typeof(string) && p.GetCustomAttribute() != null) + .ToArray()); + + // 并行处理可写属性 + Parallel.ForEach(properties, prop => + { + if (!prop.CanWrite) return; + + // 获取或缓存特性 + var maskAttr = AttributeCache.GetOrAdd(prop, p => p.GetCustomAttribute()); + + if (maskAttr == null) return; + + // 处理非空字符串 + if (prop.GetValue(obj) is string { Length: > 0 } value) + { + prop.SetValue(obj, maskAttr.Mask(value)); + } + }); + + return obj; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/RepositoryExtension.cs b/Admin.NET.Core/Extension/RepositoryExtension.cs new file mode 100644 index 0000000..e52918e --- /dev/null +++ b/Admin.NET.Core/Extension/RepositoryExtension.cs @@ -0,0 +1,454 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using MapsterMapper; + +namespace Admin.NET.Core; + +public static class RepositoryExtension +{ + /// + /// 实体假删除 _rep.FakeDelete(entity) + /// + /// + /// + /// + /// + public static int FakeDelete(this ISugarRepository repository, T entity) where T : EntityBaseDel, new() + { + return repository.Context.FakeDelete(entity); + } + + /// + /// 实体假删除 db.FakeDelete(entity) + /// + /// + /// + /// + /// + public static int FakeDelete(this ISqlSugarClient db, T entity) where T : EntityBaseDel, new() + { + return db.Updateable(entity).AS().ReSetValue(x => { x.IsDelete = true; }) + .IgnoreColumns(ignoreAllNullColumns: true) + .EnableDiffLogEvent() // 记录差异日志 + .UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId }) // 允许更新的字段-AOP拦截自动设置UpdateTime、UpdateUserId + .ExecuteCommand(); + } + + /// + /// 实体集合批量假删除 _rep.FakeDelete(entity) + /// + /// + /// + /// + /// + public static int FakeDelete(this ISugarRepository repository, List entity) where T : EntityBaseDel, new() + { + return repository.Context.FakeDelete(entity); + } + + /// + /// 实体集合批量假删除 db.FakeDelete(entity) + /// + /// + /// + /// + /// + public static int FakeDelete(this ISqlSugarClient db, List entity) where T : EntityBaseDel, new() + { + return db.Updateable(entity).AS().ReSetValue(x => { x.IsDelete = true; }) + .IgnoreColumns(ignoreAllNullColumns: true) + .EnableDiffLogEvent() // 记录差异日志 + .UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId }) // 允许更新的字段-AOP拦截自动设置UpdateTime、UpdateUserId + .ExecuteCommand(); + } + + /// + /// 实体假删除异步 _rep.FakeDeleteAsync(entity) + /// + /// + /// + /// + /// + public static Task FakeDeleteAsync(this ISugarRepository repository, T entity) where T : EntityBaseDel, new() + { + return repository.Context.FakeDeleteAsync(entity); + } + + /// + /// 实体假删除 db.FakeDelete(entity) + /// + /// + /// + /// + /// + public static Task FakeDeleteAsync(this ISqlSugarClient db, T entity) where T : EntityBaseDel, new() + { + return db.Updateable(entity).AS().ReSetValue(x => { x.IsDelete = true; }) + .IgnoreColumns(ignoreAllNullColumns: true) + .EnableDiffLogEvent() // 记录差异日志 + .UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId }) // 允许更新的字段-AOP拦截自动设置UpdateTime、UpdateUserId + .ExecuteCommandAsync(); + } + + /// + /// 实体集合批量假删除异步 _rep.FakeDeleteAsync(entity) + /// + /// + /// + /// + /// + public static Task FakeDeleteAsync(this ISugarRepository repository, List entity) where T : EntityBaseDel, new() + { + return repository.Context.FakeDeleteAsync(entity); + } + + /// + /// 实体集合批量假删除 db.FakeDelete(entity) + /// + /// + /// + /// + /// + public static Task FakeDeleteAsync(this ISqlSugarClient db, List entity) where T : EntityBaseDel, new() + { + return db.Updateable(entity).AS().ReSetValue(x => { x.IsDelete = true; }) + .IgnoreColumns(ignoreAllNullColumns: true) + .EnableDiffLogEvent() // 记录差异日志 + .UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId }) // 允许更新的字段-AOP拦截自动设置UpdateTime、UpdateUserId + .ExecuteCommandAsync(); + } + + /// + /// 排序方式(默认降序) + /// + /// + /// + /// + /// 默认排序字段 + /// 是否降序 + /// + public static ISugarQueryable OrderBuilder(this ISugarQueryable queryable, BasePageInput pageInput, string prefix = "", string defaultSortField = "Id", bool descSort = true) + { + var iSqlBuilder = InstanceFactory.GetSqlBuilderWithContext(queryable.Context); + + // 约定默认每张表都有Id排序 + var orderStr = string.IsNullOrWhiteSpace(defaultSortField) ? "" : $"{prefix}{iSqlBuilder.GetTranslationColumnName(defaultSortField)}" + (descSort ? " Desc" : " Asc"); + + TypeAdapterConfig typeAdapterConfig = new(); + typeAdapterConfig.ForType().IgnoreNullValues(true); + Mapper mapper = new(typeAdapterConfig); // 务必将mapper设为单实例 + var nowPagerInput = mapper.Map(pageInput); + // 排序是否可用-排序字段为非空才启用排序,排序顺序默认为倒序 + if (!string.IsNullOrEmpty(nowPagerInput.Field)) + { + nowPagerInput.Field = Regex.Replace(nowPagerInput.Field, @"[\s;()\-'@=/%]", ""); //过滤掉一些关键字符防止构造特殊SQL语句注入 + var orderByDbName = queryable.Context.EntityMaintenance.GetDbColumnName(nowPagerInput.Field);//防止注入,类中只要不存在属性名就会报错 + orderStr = $"{prefix}{iSqlBuilder.GetTranslationColumnName(orderByDbName)} {(string.IsNullOrEmpty(nowPagerInput.Order) || nowPagerInput.Order.Equals(nowPagerInput.DescStr, StringComparison.OrdinalIgnoreCase) ? "Desc" : "Asc")}"; + } + return queryable.OrderByIF(!string.IsNullOrWhiteSpace(orderStr), orderStr); + } + + /// + /// 更新实体并记录差异日志 _rep.UpdateWithDiffLog(entity) + /// + /// + /// + /// + /// + /// + public static int UpdateWithDiffLog(this ISugarRepository repository, T entity, bool ignoreAllNullColumns = true) where T : EntityBase, new() + { + return repository.Context.UpdateWithDiffLog(entity, ignoreAllNullColumns); + } + + /// + /// 更新实体并记录差异日志 _rep.UpdateWithDiffLog(entity) + /// + /// + /// + /// + /// + /// + public static int UpdateWithDiffLog(this ISqlSugarClient db, T entity, bool ignoreAllNullColumns = true) where T : EntityBase, new() + { + return db.Updateable(entity).AS() + .IgnoreColumns(ignoreAllNullColumns: ignoreAllNullColumns) + .EnableDiffLogEvent() + .ExecuteCommand(); + } + + /// + /// 更新实体并记录差异日志 _rep.UpdateWithDiffLogAsync(entity) + /// + /// + /// + /// + /// + /// + public static Task UpdateWithDiffLogAsync(this ISugarRepository repository, T entity, bool ignoreAllNullColumns = true) where T : EntityBase, new() + { + return repository.Context.UpdateWithDiffLogAsync(entity, ignoreAllNullColumns); + } + + /// + /// 更新实体并记录差异日志 _rep.UpdateWithDiffLogAsync(entity) + /// + /// + /// + /// + /// + /// + public static Task UpdateWithDiffLogAsync(this ISqlSugarClient db, T entity, bool ignoreAllNullColumns = true) where T : EntityBase, new() + { + return db.Updateable(entity) + .IgnoreColumns(ignoreAllNullColumns: ignoreAllNullColumns) + .EnableDiffLogEvent() + .ExecuteCommandAsync(); + } + + /// + /// 新增实体并记录差异日志 _rep.InsertWithDiffLog(entity) + /// + /// + /// + /// + /// + public static int InsertWithDiffLog(this ISugarRepository repository, T entity) where T : EntityBase, new() + { + return repository.Context.InsertWithDiffLog(entity); + } + + /// + /// 新增实体并记录差异日志 _rep.InsertWithDiffLog(entity) + /// + /// + /// + /// + /// + public static int InsertWithDiffLog(this ISqlSugarClient db, T entity) where T : EntityBase, new() + { + return db.Insertable(entity).AS().EnableDiffLogEvent().ExecuteCommand(); + } + + /// + /// 新增实体并记录差异日志 _rep.InsertWithDiffLogAsync(entity) + /// + /// + /// + /// + /// + public static Task InsertWithDiffLogAsync(this ISugarRepository repository, T entity) where T : EntityBase, new() + { + return repository.Context.InsertWithDiffLogAsync(entity); + } + + /// + /// 新增实体并记录差异日志 _rep.InsertWithDiffLog(entity) + /// + /// + /// + /// + /// + public static Task InsertWithDiffLogAsync(this ISqlSugarClient db, T entity) where T : EntityBase, new() + { + return db.Insertable(entity).AS().EnableDiffLogEvent().ExecuteCommandAsync(); + } + + /// + /// 多库查询 + /// + /// + /// + public static ISugarQueryable AS(this ISugarQueryable queryable) + { + var info = GetTableInfo(); + return queryable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 多库查询 + /// + /// + /// + /// + /// + public static ISugarQueryable AS(this ISugarQueryable queryable) + { + var info = GetTableInfo(); + return queryable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 多库更新 + /// + /// + /// + public static IUpdateable AS(this IUpdateable updateable) where T : EntityBase, new() + { + var info = GetTableInfo(); + return updateable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 多库新增 + /// + /// + /// + public static IInsertable AS(this IInsertable insertable) where T : EntityBase, new() + { + var info = GetTableInfo(); + return insertable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 多库删除 + /// + /// + /// + public static IDeleteable AS(this IDeleteable deleteable) where T : EntityBase, new() + { + var info = GetTableInfo(); + return deleteable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 根据实体类型获取表信息 + /// + /// + /// + private static Tuple GetTableInfo() + { + var entityType = typeof(T); + var attr = entityType.GetCustomAttribute(); + var configId = attr == null ? SqlSugarConst.MainConfigId : attr.configId.ToString(); + var tableName = entityType.GetCustomAttribute().TableName; + return new Tuple(configId, tableName); + } + + /// + /// 禁用过滤器-适用于更新和删除操作(只对当前请求有效,禁止使用异步) + /// + /// + /// 禁止异步 + /// + public static void RunWithoutFilter(this ISugarRepository repository, Action action) + { + repository.Context.QueryFilter.ClearAndBackup(); // 清空并备份过滤器 + action.Invoke(); + repository.Context.QueryFilter.Restore(); // 还原过滤器 + + // 用例 + //_rep.RunWithoutFilter(() => + //{ + // 执行更新或者删除 + // 禁止使用异步函数 + //}); + } + + /// + /// 忽略租户 + /// + /// + /// 是否忽略 默认true + /// + public static ISugarQueryable IgnoreTenant(this ISugarQueryable queryable, bool ignore = true) + { + return ignore ? queryable.ClearFilter() : queryable; + } + + /// + /// 只更新某些列 + /// + /// + /// + /// + /// + public static IUpdateable OnlyUpdateColumn(this IUpdateable updateable) where T : EntityBase, new() where R : class, new() + { + if (updateable.UpdateBuilder.UpdateColumns == null) + updateable.UpdateBuilder.UpdateColumns = new List(); + + foreach (PropertyInfo info in typeof(R).GetProperties()) + { + // 判断是否是相同属性 + if (typeof(T).GetProperty(info.Name) != null) + updateable.UpdateBuilder.UpdateColumns.Add(info.Name); + } + return updateable; + } + + /// + /// 导航只更新(主表)某些列 + /// + /// + /// + /// + /// + /// + public static UpdateNavRootOptions OnlyNavUpdateColumn(this T t, R r) + { + UpdateNavRootOptions uNOption = new UpdateNavRootOptions(); + var updateColumns = new List(); + + foreach (PropertyInfo info in r.GetType().GetProperties()) + { + //判断是否是相同属性 + PropertyInfo pro = t.GetType().GetProperty(info.Name); + var attr = pro.GetCustomAttribute(); + if (pro != null && attr != null && !attr.IsPrimaryKey) + updateColumns.Add(info.Name); + } + uNOption.UpdateColumns = updateColumns.ToArray(); + return uNOption; + } + + /// + /// 批量列表in查询 + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> BulkListQuery(this ISugarQueryable queryable, + Expression, bool>> exp, + IEnumerable queryList, + CancellationToken stoppingToken) where T1 : class, new() + { + // 创建临时表 (用真表兼容性好,表名随机) + var tableName = "Temp" + SnowFlakeSingle.Instance.NextId(); + try + { + var type = queryable.Context.DynamicBuilder().CreateClass(tableName, new SugarTable()) + .CreateProperty("ColumnName", typeof(string), new SugarColumn() { IsPrimaryKey = true }) // 主键不要自增 + .BuilderType(); + // 创建表 + queryable.Context.CodeFirst.InitTables(type); + var insertData = queryList.Select(it => new SingleColumnEntity() { ColumnName = it }).ToList(); + // 插入临时表 + queryable.Context.Fastest>() + .AS(tableName) + .BulkCopy(insertData); + var queryTemp = queryable.Context.Queryable>() + .AS(tableName); + + var systemData = await queryable + .InnerJoin(queryTemp, exp) + .ToListAsync(stoppingToken); + + queryable.Context.DbMaintenance.DropTable(tableName); + return systemData; + } + catch (Exception error) + { + queryable.Context.DbMaintenance.DropTable(tableName); + throw Oops.Oh(error); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/RequestExtension.cs b/Admin.NET.Core/Extension/RequestExtension.cs new file mode 100644 index 0000000..b7e8068 --- /dev/null +++ b/Admin.NET.Core/Extension/RequestExtension.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class RequestExtension +{ + /// + /// 获取请求地址源 + /// + /// + /// + public static string GetOrigin(this HttpRequest request) + { + string scheme = request.Scheme; + string host = request.Host.Host; + int port = request.Host.Port ?? (-1); + + string url = $"{scheme}://{host}"; + if (port != 80 && port != 443 && port != -1) url += $":{port}"; + + return url; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/SqlSugarExtension.cs b/Admin.NET.Core/Extension/SqlSugarExtension.cs new file mode 100644 index 0000000..fd16ee1 --- /dev/null +++ b/Admin.NET.Core/Extension/SqlSugarExtension.cs @@ -0,0 +1,367 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json; + +namespace Admin.NET.Core; + +/// +/// Sqlsugar 动态查询扩展方法 +/// +public static class SqlSugarExtension +{ + public static ISugarQueryable SearchBy(this ISugarQueryable queryable, BaseFilter filter) + { + return queryable.SearchByKeyword(filter.Keyword) + .AdvancedSearch(filter.Search) + .AdvancedFilter(filter.Filter); + } + + public static ISugarQueryable SearchByKeyword(this ISugarQueryable queryable, string keyword) + { + return queryable.AdvancedSearch(new Search { Keyword = keyword }); + } + + public static ISugarQueryable AdvancedSearch(this ISugarQueryable queryable, Search search) + { + if (!string.IsNullOrWhiteSpace(search?.Keyword)) + { + var paramExpr = Expression.Parameter(typeof(T)); + + Expression right = Expression.Constant(false); + + if (search.Fields?.Any() is true) + { + foreach (string field in search.Fields) + { + MemberExpression propertyExpr = GetPropertyExpression(field, paramExpr); + + var left = AddSearchPropertyByKeyword(propertyExpr, search.Keyword); + + right = Expression.Or(left, right); + } + } + else + { + var properties = typeof(T).GetProperties() + .Where(prop => Nullable.GetUnderlyingType(prop.PropertyType) == null + && !prop.PropertyType.IsEnum + && Type.GetTypeCode(prop.PropertyType) != TypeCode.Object); + + foreach (var property in properties) + { + var propertyExpr = Expression.Property(paramExpr, property); + + var left = AddSearchPropertyByKeyword(propertyExpr, search.Keyword); + + right = Expression.Or(left, right); + } + } + + var lambda = Expression.Lambda>(right, paramExpr); + + return queryable.Where(lambda); + } + + return queryable; + } + + public static ISugarQueryable AdvancedFilter(this ISugarQueryable queryable, Filter filter) + { + if (filter is not null) + { + var parameter = Expression.Parameter(typeof(T)); + + Expression binaryExpresioFilter; + + if (filter.Logic.HasValue) + { + if (filter.Filters is null) throw new ArgumentException("The Filters attribute is required when declaring a logic"); + binaryExpresioFilter = CreateFilterExpression(filter.Logic.Value, filter.Filters, parameter); + } + else + { + var filterValid = GetValidFilter(filter); + binaryExpresioFilter = CreateFilterExpression(filterValid.Field!, filterValid.Operator.Value, filterValid.Value, parameter); + } + + var lambda = Expression.Lambda>(binaryExpresioFilter, parameter); + + return queryable.Where(lambda); + } + return queryable; + } + + private static Expression CombineFilter( + FilterLogicEnum filterLogic, + Expression bExpresionBase, + Expression bExpresion) + { + return filterLogic switch + { + FilterLogicEnum.And => Expression.AndAlso(bExpresionBase, bExpresion), + FilterLogicEnum.Or => Expression.OrElse(bExpresionBase, bExpresion), + FilterLogicEnum.Xor => Expression.ExclusiveOr(bExpresionBase, bExpresion), + _ => throw new ArgumentException("FilterLogic is not valid.", nameof(filterLogic)), + }; + } + + private static Filter GetValidFilter(Filter filter) + { + if (string.IsNullOrEmpty(filter.Field)) throw new ArgumentException("The field attribute is required when declaring a filter"); + if (filter.Operator.IsNullOrEmpty()) throw new ArgumentException("The Operator attribute is required when declaring a filter"); + return filter; + } + + private static Expression CreateFilterExpression( + FilterLogicEnum filterLogic, + IEnumerable filters, + ParameterExpression parameter) + { + Expression filterExpression = default!; + + foreach (var filter in filters) + { + Expression bExpresionFilter; + + if (filter.Logic.HasValue) + { + if (filter.Filters is null) throw new ArgumentException("The Filters attribute is required when declaring a logic"); + bExpresionFilter = CreateFilterExpression(filter.Logic.Value, filter.Filters, parameter); + } + else + { + var filterValid = GetValidFilter(filter); + bExpresionFilter = CreateFilterExpression(filterValid.Field!, filterValid.Operator.Value, filterValid.Value, parameter); + } + + filterExpression = filterExpression is null ? bExpresionFilter : CombineFilter(filterLogic, filterExpression, bExpresionFilter); + } + + return filterExpression; + } + + private static Expression CreateFilterExpression( + string field, + FilterOperatorEnum filterOperator, + object? value, + ParameterExpression parameter) + { + var propertyExpresion = GetPropertyExpression(field, parameter); + var valueExpresion = GeValuetExpression(field, value, propertyExpresion.Type); + return CreateFilterExpression(propertyExpresion, valueExpresion, filterOperator); + } + + private static Expression CreateFilterExpression( + MemberExpression memberExpression, + ConstantExpression constantExpression, + FilterOperatorEnum filterOperator) + { + return filterOperator switch + { + FilterOperatorEnum.EQ => Expression.Equal(memberExpression, constantExpression), + FilterOperatorEnum.NEQ => Expression.NotEqual(memberExpression, constantExpression), + FilterOperatorEnum.LT => Expression.LessThan(memberExpression, constantExpression), + FilterOperatorEnum.LTE => Expression.LessThanOrEqual(memberExpression, constantExpression), + FilterOperatorEnum.GT => Expression.GreaterThan(memberExpression, constantExpression), + FilterOperatorEnum.GTE => Expression.GreaterThanOrEqual(memberExpression, constantExpression), + FilterOperatorEnum.Contains => Expression.Call(memberExpression, nameof(FilterOperatorEnum.Contains), null, constantExpression), + FilterOperatorEnum.StartsWith => Expression.Call(memberExpression, nameof(FilterOperatorEnum.StartsWith), null, constantExpression), + FilterOperatorEnum.EndsWith => Expression.Call(memberExpression, nameof(FilterOperatorEnum.EndsWith), null, constantExpression), + _ => throw new ArgumentException("Filter Operator is not valid."), + }; + } + + private static string GetStringFromJsonElement(object value) + { + if (value is JsonElement) return ((JsonElement)value).GetString()!; + if (value is string) return (string)value; + return value?.ToString(); + } + + private static ConstantExpression GeValuetExpression( + string field, + object? value, + Type propertyType) + { + if (value == null) return Expression.Constant(null, propertyType); + + if (propertyType.IsEnum) + { + string? stringEnum = GetStringFromJsonElement(value); + + if (!Enum.TryParse(propertyType, stringEnum, true, out object? valueparsed)) throw new ArgumentException(string.Format("Value {0} is not valid for {1}", value, field)); + + return Expression.Constant(valueparsed, propertyType); + } + if (propertyType == typeof(long) || propertyType == typeof(long?)) + { + string? stringLong = GetStringFromJsonElement(value); + + if (!long.TryParse(stringLong, out long valueparsed)) throw new ArgumentException(string.Format("Value {0} is not valid for {1}", value, field)); + + return Expression.Constant(valueparsed, propertyType); + } + + if (propertyType == typeof(Guid)) + { + string? stringGuid = GetStringFromJsonElement(value); + + if (!Guid.TryParse(stringGuid, out Guid valueparsed)) throw new ArgumentException(string.Format("Value {0} is not valid for {1}", value, field)); + + return Expression.Constant(valueparsed, propertyType); + } + + if (propertyType == typeof(string)) + { + string? text = GetStringFromJsonElement(value); + + return Expression.Constant(text, propertyType); + } + + if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) + { + string? text = GetStringFromJsonElement(value); + return Expression.Constant(ChangeType(text, propertyType), propertyType); + } + + return Expression.Constant(ChangeType(((JsonElement)value).GetRawText(), propertyType), propertyType); + } + + private static dynamic? ChangeType(object value, Type conversion) + { + var t = conversion; + + if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) + { + if (value == null) + { + return null; + } + + t = Nullable.GetUnderlyingType(t); + } + + return Convert.ChangeType(value, t!); + } + + private static MemberExpression GetPropertyExpression( + string propertyName, + ParameterExpression parameter) + { + Expression propertyExpression = parameter; + foreach (string member in propertyName.Split('.')) + { + propertyExpression = Expression.PropertyOrField(propertyExpression, member); + } + + return (MemberExpression)propertyExpression; + } + + private static Expression AddSearchPropertyByKeyword( + Expression propertyExpr, + string keyword, + FilterOperatorEnum operatorSearch = FilterOperatorEnum.Contains) + { + if (propertyExpr is not MemberExpression memberExpr || memberExpr.Member is not PropertyInfo property) + { + throw new ArgumentException("propertyExpr must be a property expression.", nameof(propertyExpr)); + } + + ConstantExpression constant = Expression.Constant(keyword); + + MethodInfo method = operatorSearch switch + { + FilterOperatorEnum.Contains => typeof(string).GetMethod(nameof(FilterOperatorEnum.Contains), new Type[] { typeof(string) }), + FilterOperatorEnum.StartsWith => typeof(string).GetMethod(nameof(FilterOperatorEnum.StartsWith), new Type[] { typeof(string) }), + FilterOperatorEnum.EndsWith => typeof(string).GetMethod(nameof(FilterOperatorEnum.EndsWith), new Type[] { typeof(string) }), + _ => throw new ArgumentException("Filter Operator is not valid."), + }; + + Expression selectorExpr = + property.PropertyType == typeof(string) + ? propertyExpr + : Expression.Condition( + Expression.Equal(Expression.Convert(propertyExpr, typeof(object)), Expression.Constant(null, typeof(object))), + Expression.Constant(null, typeof(string)), + Expression.Call(propertyExpr, "ToString", null, null)); + + return Expression.Call(selectorExpr, method, constant); + } + + #region 视图操作 + + /// + /// 获取映射SQL语句, 用于创建视图 + /// + /// + /// + /// + public static string ToMappedSqlString(this ISugarQueryable queryable) where T : class + { + ArgumentNullException.ThrowIfNull(queryable); + + // 获取实体映射信息 + var entityInfo = queryable.Context.EntityMaintenance.GetEntityInfo(typeof(T)); + if (entityInfo?.Columns == null || entityInfo.Columns.Count == 0) return queryable.ToSqlString(); + + // 构建需要替换的字段名映射(只处理实际有差异的字段) + var nameMap = entityInfo.Columns + .Where(c => !string.Equals(c.PropertyName, c.DbColumnName, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(k => k.PropertyName.ToLower(), v => v.DbColumnName, StringComparer.OrdinalIgnoreCase); + if (nameMap.Count == 0) return queryable.ToSqlString(); + + // 预编译正则表达式提升性能 + var sql = queryable.ToSqlString(); + foreach (var kv in nameMap) + { + sql = Regex.Replace(sql, $@"\b{kv.Key}\b", kv.Value ?? kv.Key, RegexOptions.IgnoreCase | RegexOptions.Compiled); // 单词边界匹配 + } + return sql; + } + + #endregion 视图操作 + + /// + /// 列表转换为树形结构 + /// + /// + /// 列表数据 + /// 设置子节点列表。例如:item => item.Children + /// 设置元素的父级 Id。例如:item => item.ParentId + /// 根节点的父级 Id,默认为 0 + /// + /// + public static IEnumerable ToTree( + this IEnumerable source, + Func> childrenSelector, + Func parentIdSelector, + int rootParentId) + where T : class + { + var lookup = source.ToLookup(parentIdSelector); + List BuildTree(long parentId) + { + return lookup[parentId].Select(item => + { + var children = BuildTree(GetId(item)); + childrenSelector(item).Clear(); + childrenSelector(item).AddRange(children); + return item; + }).ToList(); + } + + // 需要提供获取Id的方法,可以用反射或者自己传参数 + long GetId(T item) + { + var prop = typeof(T).GetProperty("Id"); + if (prop == null) throw new Exception("没有找到Id属性"); + return (long)prop.GetValue(item); + } + + return BuildTree(rootParentId); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/SqlSugarFilterExtension.cs b/Admin.NET.Core/Extension/SqlSugarFilterExtension.cs new file mode 100644 index 0000000..aba6fb5 --- /dev/null +++ b/Admin.NET.Core/Extension/SqlSugarFilterExtension.cs @@ -0,0 +1,58 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class SqlSugarFilterExtension +{ + /// + /// 根据指定Attribute获取属性 + /// + /// + /// + /// + private static List GetPropertyNames(this Type type) where T : Attribute + { + return type.GetProperties() + .Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(T))) + .Select(x => x.Name).ToList(); + } + + /// + /// 获取过滤表达式 + /// + /// + /// + /// + /// + public static LambdaExpression GetConditionExpression(this Type type, List owners) where T : Attribute + { + var fieldNames = type.GetPropertyNames(); + ParameterExpression parameter = Expression.Parameter(type, "c"); + + Expression right = Expression.Constant(false); + ConstantExpression ownersCollection = Expression.Constant(owners); + foreach (var fieldName in fieldNames) + { + var property = type.GetProperty(fieldName); + Expression memberExp = Expression.Property(parameter, property!); + + // 如果属性是可为空的类型,则转换为其基础类型 + var baseType = Nullable.GetUnderlyingType(property.PropertyType); + if (baseType != null) memberExp = Expression.Convert(memberExp, baseType); + + // 调用ownersCollection.Contains方法,检查是否包含属性值 + right = Expression.OrElse(Expression.Call( + typeof(Enumerable), + nameof(Enumerable.Contains), + new[] { memberExp.Type }, + ownersCollection, + memberExp + ), right); + } + return Expression.Lambda(right, parameter); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/StringExtension.cs b/Admin.NET.Core/Extension/StringExtension.cs new file mode 100644 index 0000000..f0d1e39 --- /dev/null +++ b/Admin.NET.Core/Extension/StringExtension.cs @@ -0,0 +1,172 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 字符串扩展方法 +/// +public static class StringExtension +{ + /// + /// 字符串截断 + /// + public static string Truncate(this string str, int maxLength, string ellipsis = "...") + { + if (string.IsNullOrWhiteSpace(str)) return str; + if (maxLength <= 0) return string.Empty; + if (str.Length <= maxLength) return str; + + // 确保省略号不会导致字符串超出最大长度 + int ellipsisLength = ellipsis?.Length ?? 0; + int truncateLength = Math.Min(maxLength, str.Length - ellipsisLength); + return str[..truncateLength] + ellipsis; + } + + /// + /// 单词首字母全部大写 + /// + public static string ToTitleCase(this string str) + { + return string.IsNullOrWhiteSpace(str) ? str : System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower()); + } + + /// + /// 检查是否包含子串,忽略大小写 + /// + public static bool ContainsIgnoreCase(this string str, string substring) + { + if (string.IsNullOrWhiteSpace(str) || string.IsNullOrWhiteSpace(substring)) return false; + return str.Contains(substring, StringComparison.OrdinalIgnoreCase); + } + + /// + /// 判断是否是 JSON 数据 + /// + public static bool IsJson(this string str) + { + if (string.IsNullOrWhiteSpace(str)) return false; + str = str.Trim(); + return (str.StartsWith("{") && str.EndsWith("}")) || (str.StartsWith("[") && str.EndsWith("]")); + } + + /// + /// 判断是否是 HTML 数据 + /// + public static bool IsHtml(this string str) + { + if (string.IsNullOrWhiteSpace(str)) return false; + str = str.Trim(); + + // 检查是否以 或 开头 + if (str.StartsWith("", StringComparison.OrdinalIgnoreCase) || str.StartsWith("", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // 检查是否包含 HTML 标签 + return Regex.IsMatch(str, @"<\s*[^>]+>.*<\s*/\s*[^>]+>|<\s*[^>]+\s*/>", RegexOptions.Singleline | RegexOptions.IgnoreCase); + } + + /// + /// 字符串反转 + /// + public static string Reverse(this string str) + { + if (string.IsNullOrEmpty(str)) return str; + + // 使用 Span 提高性能 + Span charSpan = stackalloc char[str.Length]; + for (int i = 0; i < str.Length; i++) + { + charSpan[str.Length - 1 - i] = str[i]; + } + return new string(charSpan); + } + + /// + /// 转首字母小写 + /// + public static string ToFirstLetterLowerCase(this string input) + { + if (string.IsNullOrWhiteSpace(input)) return input; + if (input.Length == 1) return input.ToLower(); // 处理单字符字符串 + + return char.ToLower(input[0]) + input[1..]; + } + + /// + /// 渲染字符串,替换占位符 + /// + /// 模板内容 + /// 参数对象 + /// + public static string Render(this string template, object parameters) + { + if (string.IsNullOrWhiteSpace(template)) return template; + + // 将参数转换为字典(忽略大小写) + var paramDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (parameters != null) + { + foreach (var prop in parameters.GetType().GetProperties()) + { + paramDict[prop.Name] = prop.GetValue(parameters)?.ToString() ?? string.Empty; + } + } + + // 使用正则表达式替换占位符 + return Regex.Replace(template, @"\{(\w+)\}", match => + { + string key = match.Groups[1].Value; // 获取占位符中的 key + return paramDict.TryGetValue(key, out string value) ? value : string.Empty; + }); + } + + /// + /// 驼峰转下划线 + /// + /// + /// + /// + public static string ToUnderLine(this string str, bool isToUpper = false) + { + if (string.IsNullOrEmpty(str) || str.Contains("_")) + { + return str; + } + + int length = str.Length; + var result = new System.Text.StringBuilder(length + (length / 3)); + + result.Append(char.ToLowerInvariant(str[0])); + + int lastIndex = length - 1; + + for (int i = 1; i < length; i++) + { + char current = str[i]; + if (!char.IsUpper(current)) + { + result.Append(current); + continue; + } + + bool prevIsLower = char.IsLower(str[i - 1]); + bool nextIsLower = (i < lastIndex) && char.IsLower(str[i + 1]); + + if (prevIsLower || nextIsLower) + { + result.Append('_'); + } + + result.Append((char)(current | 0x20)); + } + + string converted = result.ToString(); + return isToUpper ? converted.ToUpperInvariant() : converted; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Extension/UseApplicationBuilder.cs b/Admin.NET.Core/Extension/UseApplicationBuilder.cs new file mode 100644 index 0000000..f10ca84 --- /dev/null +++ b/Admin.NET.Core/Extension/UseApplicationBuilder.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using AspNetCoreRateLimit; +using Microsoft.AspNetCore.Builder; + +namespace Admin.NET.Core; + +/// +/// 配置中间件扩展 +/// +public static class UseApplicationBuilder +{ + // 配置限流中间件策略 + public static void UsePolicyRateLimit(this IApplicationBuilder app) + { + var ipPolicyStore = app.ApplicationServices.GetRequiredService(); + ipPolicyStore.SeedAsync().GetAwaiter().GetResult(); + + var clientPolicyStore = app.ApplicationServices.GetRequiredService(); + clientPolicyStore.SeedAsync().GetAwaiter().GetResult(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/GlobalUsings.cs b/Admin.NET.Core/GlobalUsings.cs new file mode 100644 index 0000000..a1a6b8b --- /dev/null +++ b/Admin.NET.Core/GlobalUsings.cs @@ -0,0 +1,61 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Admin.NET.Core.Service; +global using Furion; +global using Furion.ConfigurableOptions; +global using Furion.DatabaseAccessor; +global using Furion.DataEncryption; +global using Furion.DataValidation; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Furion.EventBus; +global using Furion.FriendlyException; +global using Furion.HttpRemote; +global using Furion.JsonSerialization; +global using Furion.Logging; +global using Furion.Schedule; +global using Furion.UnifyResult; +global using Furion.ViewEngine; +global using Magicodes.ExporterAndImporter.Core; +global using Magicodes.ExporterAndImporter.Core.Extension; +global using Magicodes.ExporterAndImporter.Excel; +global using Mapster; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.Mvc.Filters; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using NewLife; +global using NewLife.Caching; +global using Newtonsoft.Json.Linq; +global using SKIT.FlurlHttpClient; +global using SKIT.FlurlHttpClient.Wechat.Api; +global using SKIT.FlurlHttpClient.Wechat.Api.Models; +global using SKIT.FlurlHttpClient.Wechat.TenpayV3; +global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Events; +global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models; +global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings; +global using SqlSugar; +global using System.Collections; +global using System.Collections.Concurrent; +global using System.ComponentModel; +global using System.ComponentModel.DataAnnotations; +global using System.Data; +global using System.Diagnostics; +global using System.Linq.Dynamic.Core; +global using System.Linq.Expressions; +global using System.Reflection; +global using System.Runtime.InteropServices; +global using System.Text; +global using System.Text.RegularExpressions; +global using System.Web; +global using UAParser; +global using Yitter.IdGenerator; \ No newline at end of file diff --git a/Admin.NET.Core/Hub/Dto/OnlineUserHubInput.cs b/Admin.NET.Core/Hub/Dto/OnlineUserHubInput.cs new file mode 100644 index 0000000..386ad74 --- /dev/null +++ b/Admin.NET.Core/Hub/Dto/OnlineUserHubInput.cs @@ -0,0 +1,12 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class OnlineUserHubInput +{ + public string ConnectionId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Hub/Dto/OnlineUserHubOutput.cs b/Admin.NET.Core/Hub/Dto/OnlineUserHubOutput.cs new file mode 100644 index 0000000..90c4425 --- /dev/null +++ b/Admin.NET.Core/Hub/Dto/OnlineUserHubOutput.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class OnlineUserList +{ + public string RealName { get; set; } + + public bool Online { get; set; } + + public List UserList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Hub/IOnlineUserHub.cs b/Admin.NET.Core/Hub/IOnlineUserHub.cs new file mode 100644 index 0000000..64fa9a2 --- /dev/null +++ b/Admin.NET.Core/Hub/IOnlineUserHub.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public interface IOnlineUserHub +{ + /// + /// 在线用户列表 + /// + /// + /// + Task OnlineUserList(OnlineUserList context); + + /// + /// 强制下线 + /// + /// + /// + Task ForceOffline(object context); + + /// + /// 发布站内消息 + /// + /// + /// + Task PublicNotice(SysNotice context); + + /// + /// 接收消息 + /// + /// + /// + Task ReceiveMessage(object context); +} \ No newline at end of file diff --git a/Admin.NET.Core/Hub/OnlineUserHub.cs b/Admin.NET.Core/Hub/OnlineUserHub.cs new file mode 100644 index 0000000..3df9cc6 --- /dev/null +++ b/Admin.NET.Core/Hub/OnlineUserHub.cs @@ -0,0 +1,182 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.InstantMessaging; +using Microsoft.AspNetCore.SignalR; + +namespace Admin.NET.Core; + +/// +/// 在线用户集线器 +/// +[MapHub("/hubs/onlineUser")] +public class OnlineUserHub : Hub +{ + private const string GROUP_ONLINE = "GROUP_ONLINE_"; // 租户分组前缀 + + private readonly SqlSugarRepository _sysOnlineUerRep; + private readonly SysMessageService _sysMessageService; + private readonly IHubContext _onlineUserHubContext; + private readonly SysCacheService _sysCacheService; + private readonly SysConfigService _sysConfigService; + + public OnlineUserHub(SqlSugarRepository sysOnlineUerRep, + SysMessageService sysMessageService, + IHubContext onlineUserHubContext, + SysCacheService sysCacheService, + SysConfigService sysConfigService) + { + _sysOnlineUerRep = sysOnlineUerRep; + _sysMessageService = sysMessageService; + _onlineUserHubContext = onlineUserHubContext; + _sysCacheService = sysCacheService; + _sysConfigService = sysConfigService; + } + + /// + /// 连接 + /// + /// + public override async Task OnConnectedAsync() + { + var httpContext = Context.GetHttpContext(); + var userId = (httpContext.User.FindFirst(ClaimConst.UserId)?.Value).ToLong(); + var account = httpContext.User.FindFirst(ClaimConst.Account)?.Value; + var realName = httpContext.User.FindFirst(ClaimConst.RealName)?.Value; + var tenantId = (httpContext.User.FindFirst(ClaimConst.TenantId)?.Value).ToLong(); + + if (userId < 0 || string.IsNullOrWhiteSpace(account)) return; + var user = new SysOnlineUser + { + ConnectionId = Context.ConnectionId, + UserId = userId, + UserName = account, + RealName = realName, + Time = DateTime.Now, + Ip = httpContext.GetRemoteIpAddressToIPv4(true), + Browser = httpContext.GetClientBrowser(), + Os = httpContext.GetClientOs(), + TenantId = tenantId, + }; + await _sysOnlineUerRep.InsertAsync(user); + + // 是否开启单用户登录 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysSingleLogin)) + { + _sysCacheService.HashAddOrUpdate(CacheConst.KeyUserOnline, "" + user.UserId, user); + } + else // 非单用户登录则绑定用户连接Id + { + _sysCacheService.HashAdd(CacheConst.KeyUserOnline, user.UserId + Context.ConnectionId, user); + } + + // 以租户Id进行分组 + var groupName = $"{GROUP_ONLINE}{user.TenantId}"; + await _onlineUserHubContext.Groups.AddToGroupAsync(Context.ConnectionId, groupName); + + var userList = await _sysOnlineUerRep.AsQueryable().Filter("", true) + .Where(u => u.TenantId == user.TenantId).Take(10).ToListAsync(); + + if (await _sysConfigService.GetConfigValue(ConfigConst.SysLoginOutReminder)) + await _onlineUserHubContext.Clients.Groups(groupName).OnlineUserList(new OnlineUserList + { + RealName = user.RealName, + Online = true, + UserList = userList + }); + } + + /// + /// 断开 + /// + /// + /// + public override async Task OnDisconnectedAsync(Exception exception) + { + if (string.IsNullOrEmpty(Context.ConnectionId)) return; + + var httpContext = Context.GetHttpContext(); + + var user = await _sysOnlineUerRep.AsQueryable().Filter("", true).FirstAsync(u => u.ConnectionId == Context.ConnectionId); + if (user == null) return; + + await _sysOnlineUerRep.DeleteByIdAsync(user.Id); + + // 是否开启单用户登录 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysSingleLogin)) + { + _sysCacheService.HashDel(CacheConst.KeyUserOnline, "" + user.UserId); + // _sysCacheService.Remove(CacheConst.KeyUserOnline + user.UserId); + } + else + { + _sysCacheService.HashDel(CacheConst.KeyUserOnline, user.UserId + Context.ConnectionId); + // _sysCacheService.Remove(CacheConst.KeyUserOnline + user.UserId + Context.ConnectionId); + } + + // 通知当前组用户变动 + var userList = await _sysOnlineUerRep.AsQueryable().Filter("", true) + .Where(u => u.TenantId == user.TenantId).Take(10).ToListAsync(); + + if (await _sysConfigService.GetConfigValue(ConfigConst.SysLoginOutReminder)) + await _onlineUserHubContext.Clients.Groups($"{GROUP_ONLINE}{user.TenantId}").OnlineUserList(new OnlineUserList + { + RealName = user.RealName, + Online = false, + UserList = userList + }); + } + + /// + /// 强制下线 + /// + /// + /// + public async Task ForceOffline(OnlineUserHubInput input) + { + await _onlineUserHubContext.Clients.Client(input.ConnectionId).ForceOffline("强制下线"); + } + + /// + /// 发送信息给某个人 + /// + /// + /// + public async Task ClientsSendMessage(MessageInput message) + { + await _sysMessageService.SendUser(message); + } + + /// + /// 发送信息给所有人 + /// + /// + /// + public async Task ClientsSendMessageToAll(MessageInput message) + { + await _sysMessageService.SendAllUser(message); + } + + /// + /// 发送消息给某些人(除了本人) + /// + /// + /// + public async Task ClientsSendMessageToOther(MessageInput message) + { + await _sysMessageService.SendOtherUser(message); + } + + /// + /// 发送消息给某些人 + /// + /// + /// + public async Task ClientsSendMessageToUsers(MessageInput message) + { + await _sysMessageService.SendUsers(message); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Job/DynamicJobCompiler.cs b/Admin.NET.Core/Job/DynamicJobCompiler.cs new file mode 100644 index 0000000..67eb8fa --- /dev/null +++ b/Admin.NET.Core/Job/DynamicJobCompiler.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 动态作业编译 +/// +public class DynamicJobCompiler : ISingleton +{ + /// + /// 编译代码并返回其中实现 IJob 的类型 + /// + /// 动态编译的作业代码 + /// + public Type BuildJob(string script) + { + var jobAssembly = Schedular.CompileCSharpClassCode(script); + var jobType = jobAssembly.GetTypes().FirstOrDefault(u => typeof(IJob).IsAssignableFrom(u)); + return jobType; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Job/EnumToDictJob.cs b/Admin.NET.Core/Job/EnumToDictJob.cs new file mode 100644 index 0000000..4b84903 --- /dev/null +++ b/Admin.NET.Core/Job/EnumToDictJob.cs @@ -0,0 +1,146 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举转字典 +/// +[JobDetail("job_EnumToDictJob", Description = "枚举转字典", GroupName = "default", Concurrent = false)] +[PeriodSeconds(1, TriggerId = "trigger_EnumToDictJob", Description = "枚举转字典", MaxNumberOfRuns = 1, RunOnStart = true)] +public class EnumToDictJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private const string DefaultTagType = null; + private const int OrderOffset = 10; + + public EnumToDictJob(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"【{DateTime.Now}】系统枚举转换字典"); + + using var serviceScope = _scopeFactory.CreateScope(); + var db = serviceScope.ServiceProvider.GetRequiredService().CopyNew(); + + var sysEnumService = serviceScope.ServiceProvider.GetRequiredService(); + var sysDictTypeList = GetDictByEnumType(sysEnumService.GetEnumTypeList()); + + // 校验枚举类命名规范,字典相关功能中需要通过后缀判断是否为枚举类型 + Console.ForegroundColor = ConsoleColor.Red; + foreach (var dictType in sysDictTypeList.Where(x => !x.Code.EndsWith("Enum"))) + Console.WriteLine($"【{DateTime.Now}】系统枚举转换字典的枚举类名称必须以Enum结尾: {dictType.Code} ({dictType.Name})"); + sysDictTypeList = sysDictTypeList.Where(x => x.Code.EndsWith("Enum")).ToList(); + + await SyncEnumToDictInfoAsync(db, sysDictTypeList); + + Console.ForegroundColor = ConsoleColor.Yellow; + try + { + await db.BeginTranAsync(); + var storageable1 = await db.Storageable(sysDictTypeList) + .SplitUpdate(it => it.Any()) + .SplitInsert(_ => true) + .ToStorageAsync(); + await storageable1.AsInsertable.ExecuteCommandAsync(stoppingToken); + await storageable1.AsUpdateable.ExecuteCommandAsync(stoppingToken); + + Console.WriteLine($"【{DateTime.Now}】系统枚举类转字典类型数据: 插入{storageable1.InsertList.Count}条, 更新{storageable1.UpdateList.Count}条, 共{storageable1.TotalList.Count}条。"); + + var storageable2 = await db.Storageable(sysDictTypeList.SelectMany(x => x.Children).ToList()) + .WhereColumns(it => new { it.DictTypeId, it.Value }) + .SplitUpdate(it => it.Any()) + .SplitInsert(_ => true) + .ToStorageAsync(); + await storageable2.AsInsertable.ExecuteCommandAsync(stoppingToken); + await storageable2.AsUpdateable.UpdateColumns(u => new + { + u.Label, + u.Code, + u.Value + }).ExecuteCommandAsync(stoppingToken); + + Console.WriteLine($"【{DateTime.Now}】系统枚举项转字典值数据: 插入{storageable2.InsertList.Count}条, 更新{storageable2.UpdateList.Count}条, 共{storageable2.TotalList.Count}条。"); + + await db.CommitTranAsync(); + } + catch (Exception error) + { + await db.RollbackTranAsync(); + Log.Error($"系统枚举转换字典操作错误:{error.Message}\n堆栈跟踪:{error.StackTrace}", error); + throw; + } + finally + { + Console.ForegroundColor = originColor; + } + } + + /// + /// 用于同步枚举转字典数据 + /// + /// + /// + private async Task SyncEnumToDictInfoAsync(SqlSugarClient db, List list) + { + var codeList = list.Select(x => x.Code).ToList(); + foreach (var dbDictType in await db.Queryable().ClearFilter().Where(x => codeList.Contains(x.Code)).ToListAsync() ?? new()) + { + var enumDictType = list.First(x => x.Code == dbDictType.Code); + if (enumDictType.Id == dbDictType.Id) + { + // 字典值表字段改变后每条字典值记录会多出一条,此处用于删除多余的字典值数据 + var dataValueList = enumDictType.Children.Select(e => e.Value).ToList(); + await db.Deleteable().Where(x => x.DictTypeId == dbDictType.Id && !dataValueList.Contains(x.Value)).ExecuteCommandAsync(); + continue; + } + + // 数据不一致则删除 + await db.Deleteable().Where(x => x.DictTypeId == dbDictType.Id).ExecuteCommandAsync(); + await db.Deleteable().Where(x => x.Id == dbDictType.Id).ExecuteCommandAsync(); + Console.WriteLine($"【{DateTime.Now}】删除字典数据: {dbDictType.Name}-{dbDictType.Code}"); + } + } + + /// + /// 枚举信息转字典 + /// + /// + /// + private List GetDictByEnumType(List enumTypeList) + { + var orderNo = 1; + var list = new List(); + foreach (var type in enumTypeList) + { + var dictType = new SysDictType + { + Id = 900000000000 + CommonUtil.GetFixedHashCode(type.TypeFullName), + SysFlag = YesNoEnum.Y, + Code = type.TypeName, + Name = type.TypeDescribe, + Remark = type.TypeFullName + }; + dictType.Children = type.EnumEntities.Select(x => new SysDictData + { + Id = dictType.Id + orderNo++, + DictTypeId = dictType.Id, + Code = x.Name, + Label = x.Describe, + Value = x.Value.ToString(), + OrderNo = x.Value + OrderOffset, + TagType = x.Theme != "" ? x.Theme : DefaultTagType + }).ToList(); + list.Add(dictType); + } + return list; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Job/LogJob.cs b/Admin.NET.Core/Job/LogJob.cs new file mode 100644 index 0000000..eba51dc --- /dev/null +++ b/Admin.NET.Core/Job/LogJob.cs @@ -0,0 +1,46 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 清理日志作业任务 +/// +[JobDetail("job_log", Description = "清理操作日志", GroupName = "default", Concurrent = false)] +[Daily(TriggerId = "trigger_log", Description = "清理操作日志")] +public class LogJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public LogJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory) + { + _scopeFactory = scopeFactory; + _logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + + var db = serviceScope.ServiceProvider.GetRequiredService().CopyNew(); + var sysConfigService = serviceScope.ServiceProvider.GetRequiredService(); + + var daysAgo = await sysConfigService.GetConfigValue(ConfigConst.SysLogRetentionDays); // 日志保留天数 + await db.Deleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除访问日志 + await db.Deleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除操作日志 + await db.Deleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除差异日志 + + string msg = $"【{DateTime.Now}】清理系统日志成功,删除 {daysAgo} 天前的日志数据!"; + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(msg); + Console.ForegroundColor = originColor; + + // 自定义日志 + _logger.LogInformation(msg); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Job/OnlineUserJob.cs b/Admin.NET.Core/Job/OnlineUserJob.cs new file mode 100644 index 0000000..7e49e69 --- /dev/null +++ b/Admin.NET.Core/Job/OnlineUserJob.cs @@ -0,0 +1,46 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.Logging.Extensions; + +namespace Admin.NET.Core; + +/// +/// 清理在线用户作业任务 +/// +[JobDetail("job_onlineUser", Description = "清理在线用户", GroupName = "default", Concurrent = false)] +[PeriodSeconds(1, TriggerId = "trigger_onlineUser", Description = "清理在线用户", MaxNumberOfRuns = 1, RunOnStart = true)] +public class OnlineUserJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public OnlineUserJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory) + { + _scopeFactory = scopeFactory; + _logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + + var db = serviceScope.ServiceProvider.GetRequiredService().CopyNew(); + await db.Deleteable().ExecuteCommandAsync(stoppingToken); + + string msg = $"【{DateTime.Now}】清理在线用户成功!服务已重启..."; + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(msg); + Console.ForegroundColor = originColor; + + // 自定义日志 + _logger.LogInformation(msg); + + // 缓存租户列表 + await serviceScope.ServiceProvider.GetRequiredService().CacheTenant(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs b/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs new file mode 100644 index 0000000..d8db2c8 --- /dev/null +++ b/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs @@ -0,0 +1,206 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据库日志写入器 +/// +public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable +{ + private readonly IServiceScope _serviceScope; + private readonly ISqlSugarClient _db; + private readonly SysConfigService _sysConfigService; // 参数配置服务 + private readonly ILogger _logger; // 日志组件 + + public DatabaseLoggingWriter(IServiceScopeFactory scopeFactory) + { + _serviceScope = scopeFactory.CreateScope(); + //_db = _serviceScope.ServiceProvider.GetRequiredService(); + _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService(); + _logger = _serviceScope.ServiceProvider.GetRequiredService>(); + + // 切换日志独立数据库 + _db = SqlSugarSetup.ITenant.IsAnyConnection(SqlSugarConst.LogConfigId) + ? SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.LogConfigId) + : SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.MainConfigId); + } + + public async Task WriteAsync(LogMessage logMsg, bool flush) + { + var jsonStr = logMsg.Context?.Get("loggingMonitor")?.ToString(); + if (string.IsNullOrWhiteSpace(jsonStr)) + { + await _db.Insertable(new SysLogOp + { + DisplayTitle = "自定义操作日志", + LogDateTime = logMsg.LogDateTime, + EventId = logMsg.EventId.Id, + ThreadId = logMsg.ThreadId, + TraceId = logMsg.TraceId, + Exception = logMsg.Exception == null ? null : JSON.Serialize(logMsg.Exception), + Message = logMsg.Message, + LogLevel = logMsg.LogLevel, + Status = "200", + }).ExecuteCommandAsync(); + return; + } + + var loggingMonitor = JSON.Deserialize(jsonStr); + // 记录数据校验日志 + if (loggingMonitor.validation != null && !await _sysConfigService.GetConfigValue(ConfigConst.SysValidationLog)) return; + + // 获取当前操作者 + string account = "", realName = "", userId = "", tenantId = ""; + if (loggingMonitor.authorizationClaims != null) + { + var map = (loggingMonitor.authorizationClaims as IEnumerable) + !.ToDictionary(u => u.type.ToString(), u => u.value.ToString()); + account = map.GetValueOrDefault(ClaimConst.Account); + realName = map.GetValueOrDefault(ClaimConst.RealName); + tenantId = map.GetValueOrDefault(ClaimConst.TenantId); + userId = map.GetValueOrDefault(ClaimConst.UserId); + } + + // 优先获取 X-Forwarded-For 头部信息携带的IP地址(如nginx代理配置转发) + var remoteIPv4 = ((JArray)loggingMonitor.requestHeaders).OfType() + .FirstOrDefault(header => (string)header["key"] == "X-Forwarded-For")?["value"]?.ToString(); + + if (string.IsNullOrEmpty(remoteIPv4)) + remoteIPv4 = loggingMonitor.remoteIPv4; + + remoteIPv4 = remoteIPv4?.Split(',')?.FirstOrDefault()?.Trim(); + + (string ipLocation, double? longitude, double? latitude) = CommonUtil.GetIpAddress(remoteIPv4); + + var browser = ""; + var os = ""; + if (loggingMonitor.userAgent != null) + { + var client = Parser.GetDefault().Parse(loggingMonitor.userAgent.ToString()); + browser = $"{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}"; + os = $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}"; + } + + // 捕捉异常,否则会由于 unhandled exception 导致程序崩溃 + try + { + // 记录异常日志-发送邮件 + if (logMsg.Exception != null || loggingMonitor.exception != null) + { + await _db.Insertable(new SysLogEx + { + ControllerName = loggingMonitor.controllerName, + ActionName = loggingMonitor.actionTypeName, + DisplayTitle = loggingMonitor.displayTitle, + Status = loggingMonitor.returnInformation?.httpStatusCode, + RemoteIp = remoteIPv4, + Location = ipLocation, + Longitude = (decimal?)longitude, + Latitude = (decimal?)latitude, + Browser = browser, // loggingMonitor.userAgent, + Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, + Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, + LogDateTime = logMsg.LogDateTime, + Account = account, + RealName = realName, + HttpMethod = loggingMonitor.httpMethod, + RequestUrl = loggingMonitor.requestUrl, + RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value), + ReturnResult = loggingMonitor.returnInformation == null ? null : JSON.Serialize(loggingMonitor.returnInformation), + EventId = logMsg.EventId.Id, + ThreadId = logMsg.ThreadId, + TraceId = logMsg.TraceId, + Exception = JSON.Serialize(loggingMonitor.exception), + Message = logMsg.Message, + CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), + TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId), + LogLevel = logMsg.LogLevel + }).ExecuteCommandAsync(); + + // 将异常日志发送到邮件 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysErrorMail)) + { + await App.GetRequiredService().PublishAsync(CommonConst.SendErrorMail, logMsg.Exception ?? loggingMonitor.exception); + } + + return; + } + + // 记录访问日志-登录退出 + if (loggingMonitor.actionName == "userInfo" || loggingMonitor.actionName == "logout") + { + await _db.Insertable(new SysLogVis + { + ControllerName = loggingMonitor.controllerName, + ActionName = loggingMonitor.actionTypeName, + DisplayTitle = loggingMonitor.displayTitle, + Status = loggingMonitor.returnInformation?.httpStatusCode, + RemoteIp = remoteIPv4, + Location = ipLocation, + Longitude = (decimal?)longitude, + Latitude = (decimal?)latitude, + Browser = browser, // loggingMonitor.userAgent, + Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, + Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, + LogDateTime = logMsg.LogDateTime, + Account = account, + RealName = realName, + CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), + TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId), + LogLevel = logMsg.LogLevel + }).ExecuteCommandAsync(); + return; + } + + // 记录操作日志 + if (!await _sysConfigService.GetConfigValue(ConfigConst.SysOpLog)) return; + await _db.Insertable(new SysLogOp + { + ControllerName = loggingMonitor.controllerName, + ActionName = loggingMonitor.actionTypeName, + DisplayTitle = loggingMonitor.displayTitle, + Status = loggingMonitor.returnInformation?.httpStatusCode, + RemoteIp = remoteIPv4, + Location = ipLocation, + Longitude = (decimal?)longitude, + Latitude = (decimal?)latitude, + Browser = browser, // loggingMonitor.userAgent, + Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, + Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, + LogDateTime = logMsg.LogDateTime, + Account = account, + RealName = realName, + HttpMethod = loggingMonitor.httpMethod, + RequestUrl = loggingMonitor.requestUrl, + RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value), + ReturnResult = loggingMonitor.returnInformation == null ? null : JSON.Serialize(loggingMonitor.returnInformation), + EventId = logMsg.EventId.Id, + ThreadId = logMsg.ThreadId, + TraceId = logMsg.TraceId, + Exception = loggingMonitor.exception == null ? null : JSON.Serialize(loggingMonitor.exception), + Message = logMsg.Message, + CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), + TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId), + LogLevel = logMsg.LogLevel + }).ExecuteCommandAsync(); + + await Task.Delay(50); // 延迟 0.05 秒写入数据库,有效减少高频写入数据库导致死锁问题 + } + catch (Exception ex) + { + _logger.LogError(ex, "操作日志入库"); + } + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + _serviceScope.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs b/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs new file mode 100644 index 0000000..64bd80a --- /dev/null +++ b/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs @@ -0,0 +1,101 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Elastic.Clients.Elasticsearch; + +namespace Admin.NET.Core; + +/// +/// ES日志写入器 +/// +public class ElasticSearchLoggingWriter : IDatabaseLoggingWriter, IDisposable +{ + private readonly IServiceScope _serviceScope; + private readonly ElasticsearchClient _esClient; + private readonly SysConfigService _sysConfigService; + + public ElasticSearchLoggingWriter(IServiceScopeFactory scopeFactory) + { + _serviceScope = scopeFactory.CreateScope(); + _esClient = _serviceScope.ServiceProvider.GetRequiredService()?.Logging; + _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService(); + } + + public async Task WriteAsync(LogMessage logMsg, bool flush) + { + // 是否启用操作日志 + var sysOpLogEnabled = await _sysConfigService.GetConfigValue(ConfigConst.SysOpLog); + if (!sysOpLogEnabled) return; + + var jsonStr = logMsg.Context?.Get("loggingMonitor")?.ToString(); + if (string.IsNullOrWhiteSpace(jsonStr)) return; + + var loggingMonitor = JSON.Deserialize(jsonStr); + + // 不记录登录退出日志 + if (loggingMonitor.actionName == "userInfo" || loggingMonitor.actionName == "logout") + return; + + // 获取当前操作者 + string account = "", realName = "", userId = "", tenantId = ""; + if (loggingMonitor.authorizationClaims != null) + { + foreach (var item in loggingMonitor.authorizationClaims) + { + if (item.type == ClaimConst.Account) + account = item.value; + if (item.type == ClaimConst.RealName) + realName = item.value; + if (item.type == ClaimConst.TenantId) + tenantId = item.value; + if (item.type == ClaimConst.UserId) + userId = item.value; + } + } + + string remoteIPv4 = loggingMonitor.remoteIPv4; + (string ipLocation, double? longitude, double? latitude) = CommonUtil.GetIpAddress(remoteIPv4); + + var sysLogOp = new SysLogOp + { + Id = DateTime.Now.Ticks, + ControllerName = loggingMonitor.controllerName, + ActionName = loggingMonitor.actionTypeName, + DisplayTitle = loggingMonitor.displayTitle, + Status = loggingMonitor.returnInformation.httpStatusCode, + RemoteIp = remoteIPv4, + Location = ipLocation, + Longitude = (decimal?)longitude, + Latitude = (decimal?)latitude, + Browser = loggingMonitor.userAgent, + Os = loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, + Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, + LogDateTime = logMsg.LogDateTime, + Account = account, + RealName = realName, + HttpMethod = loggingMonitor.httpMethod, + RequestUrl = loggingMonitor.requestUrl, + RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value), + ReturnResult = JSON.Serialize(loggingMonitor.returnInformation), + EventId = logMsg.EventId.Id, + ThreadId = logMsg.ThreadId, + TraceId = logMsg.TraceId, + Exception = (loggingMonitor.exception == null) ? null : JSON.Serialize(loggingMonitor.exception), + Message = logMsg.Message, + CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), + TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId) + }; + await _esClient.IndexAsync(sysLogOp); + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + _serviceScope.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Logging/LogExceptionHandler.cs b/Admin.NET.Core/Logging/LogExceptionHandler.cs new file mode 100644 index 0000000..7895563 --- /dev/null +++ b/Admin.NET.Core/Logging/LogExceptionHandler.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +//using Microsoft.AspNetCore.Mvc.Controllers; +//using System.Security.Claims; + +//namespace Admin.NET.Core.Logging; + +///// +///// 全局异常处理 +///// +//public class LogExceptionHandler : IGlobalExceptionHandler, ISingleton +//{ +// private readonly IEventPublisher _eventPublisher; + +// public LogExceptionHandler(IEventPublisher eventPublisher) +// { +// _eventPublisher = eventPublisher; +// } + +// public async Task OnExceptionAsync(ExceptionContext context) +// { +// var actionMethod = (context.ActionDescriptor as ControllerActionDescriptor)?.MethodInfo; +// var displayNameAttribute = actionMethod.IsDefined(typeof(DisplayNameAttribute), true) ? actionMethod.GetCustomAttribute(true) : default; + +// var sysLogEx = new SysLogEx +// { +// Account = App.User?.FindFirstValue(ClaimConst.Account), +// RealName = App.User?.FindFirstValue(ClaimConst.RealName), +// ControllerName = actionMethod.DeclaringType.FullName, +// ActionName = actionMethod.Name, +// DisplayTitle = displayNameAttribute?.DisplayName, +// Exception = $"异常信息:{context.Exception.Message} 异常来源:{context.Exception.Source} 堆栈信息:{context.Exception.StackTrace}", +// Message = "全局异常", +// RequestParam = context.Exception.TargetSite.GetParameters().ToString(), +// HttpMethod = context.HttpContext.Request.Method, +// RequestUrl = context.HttpContext.Request.GetRequestUrlAddress(), +// RemoteIp = context.HttpContext.GetRemoteIpAddressToIPv4(), +// Browser = context.HttpContext.Request.Headers["User-Agent"], +// TraceId = App.GetTraceId(), +// ThreadId = App.GetThreadId(), +// LogDateTime = DateTime.Now, +// LogLevel = LogLevel.Error +// }; + +// await _eventPublisher.PublishAsync(new ChannelEventSource("Add:ExLog", sysLogEx)); + +// await _eventPublisher.PublishAsync("Send:ErrorMail", sysLogEx); +// } +//} \ No newline at end of file diff --git a/Admin.NET.Core/Logging/LoggingSetup.cs b/Admin.NET.Core/Logging/LoggingSetup.cs new file mode 100644 index 0000000..17cc785 --- /dev/null +++ b/Admin.NET.Core/Logging/LoggingSetup.cs @@ -0,0 +1,104 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class LoggingSetup +{ + /// + /// 日志注册 + /// + /// + public static void AddLoggingSetup(this IServiceCollection services) + { + // 日志监听 + services.AddMonitorLogging(options => + { + options.IgnorePropertyNames = new[] { "Byte" }; + options.IgnorePropertyTypes = new[] { typeof(byte[]) }; + }); + + // 控制台日志 + var consoleLog = App.GetConfig("Logging:Monitor:ConsoleLog", true); + services.AddConsoleFormatter(options => + { + options.DateFormat = "yyyy-MM-dd HH:mm:ss(zzz) dddd"; + //options.WithTraceId = true; // 显示线程Id + //options.WithStackFrame = true; // 显示程序集 + options.WriteFilter = (logMsg) => + { + return consoleLog; + }; + }); + + // 日志写入文件 + if (App.GetConfig("Logging:File:Enabled", true)) + { + var loggingMonitorSettings = App.GetConfig("Logging:Monitor", true); + Array.ForEach(new[] { LogLevel.Information, LogLevel.Warning, LogLevel.Error }, logLevel => + { + services.AddFileLogging(options => + { + options.WithTraceId = true; // 显示线程Id + options.WithStackFrame = true; // 显示程序集 + options.FileNameRule = fileName => string.Format(fileName, DateTime.Now, logLevel.ToString()); // 每天创建一个文件 + options.WriteFilter = logMsg => logMsg.LogLevel == logLevel; // 日志级别 + options.HandleWriteError = (writeError) => // 写入失败时启用备用文件 + { + writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName)); + }; + if (loggingMonitorSettings.JsonBehavior == JsonBehavior.OnlyJson) + { + options.MessageFormat = LoggerFormatter.Json; + // options.MessageFormat = LoggerFormatter.JsonIndented; + options.MessageFormat = (logMsg) => + { + var jsonString = logMsg.Context.Get("loggingMonitor"); + return jsonString?.ToString(); + }; + } + }); + }); + } + + // 日志写入ElasticSearch + if (App.GetConfig("ElasticSearch:Logging:Enabled", true)) + { + services.AddDatabaseLogging(options => + { + options.WithTraceId = true; // 显示线程Id + options.WithStackFrame = true; // 显示程序集 + options.IgnoreReferenceLoop = false; // 忽略循环检测 + options.MessageFormat = LoggerFormatter.Json; + options.WriteFilter = (logMsg) => + { + return logMsg.LogName == CommonConst.SysLogCategoryName; // 只写LoggingMonitor日志 + }; + }); + } + + // 日志写入数据库 + if (App.GetConfig("Logging:Database:Enabled", true)) + { + services.AddDatabaseLogging(options => + { + options.WithTraceId = true; // 显示线程Id + options.WithStackFrame = true; // 显示程序集 + options.IgnoreReferenceLoop = false; // 忽略循环检测 + options.MessageFormat = (logMsg) => + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(logMsg.Message); + return stringBuilder.ToString(); + }; + options.WriteFilter = (logMsg) => + { + return logMsg.LogName == CommonConst.SysLogCategoryName; // 只写LoggingMonitor日志 + }; + }); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/APIJSONOptions.cs b/Admin.NET.Core/Option/APIJSONOptions.cs new file mode 100644 index 0000000..c12625a --- /dev/null +++ b/Admin.NET.Core/Option/APIJSONOptions.cs @@ -0,0 +1,70 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// APIJSON配置选项 +/// +public sealed class APIJSONOptions : IConfigurableOptions +{ + /// + /// 角色集合 + /// + public List Roles { get; set; } +} + +/// +/// APIJSON角色权限 +/// +public class APIJSON_Role +{ + /// + /// 角色名称 + /// + public string RoleName { get; set; } + + /// + /// 查询 + /// + public APIJSON_RoleItem Select { get; set; } + + /// + /// 增加 + /// + public APIJSON_RoleItem Insert { get; set; } + + /// + /// 更新 + /// + public APIJSON_RoleItem Update { get; set; } + + /// + /// 删除 + /// + public APIJSON_RoleItem Delete { get; set; } +} + +/// +/// APIJSON角色权限内容 +/// +public class APIJSON_RoleItem +{ + /// + /// 表集合 + /// + public string[] Table { get; set; } + + /// + /// 列集合 + /// + public string[] Column { get; set; } + + /// + /// 过滤器 + /// + public string[] Filter { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/AlipayOptions.cs b/Admin.NET.Core/Option/AlipayOptions.cs new file mode 100644 index 0000000..ddd1bb0 --- /dev/null +++ b/Admin.NET.Core/Option/AlipayOptions.cs @@ -0,0 +1,119 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Aop.Api; + +namespace Admin.NET.Core; + +/// +/// 支付宝支付配置选项 +/// +public sealed class AlipayOptions : IConfigurableOptions +{ + /// + /// 支付宝网关地址 + /// + public string ServerUrl { get; init; } + + /// + /// 支付宝授权回调地址 + /// + public string AuthUrl { get; init; } + + /// + /// 应用授权回调地址 + /// + public string AppAuthUrl { get; init; } + + /// + /// 支付宝 websocket 服务地址 + /// + public string WebsocketUrl { get; init; } + + /// + /// 应用回调地址 + /// + public string NotifyUrl { get; init; } + + /// + /// 支付宝根证书存放路径 + /// + public string RootCertPath { get; init; } + + /// + /// 支付宝商户账号列表 + /// + public List AccountList { get; init; } + + /// + /// 获取支付宝客户端 + /// + /// + public DefaultAopClient GetClient(AlipayMerchantAccount account) + { + account = account ?? throw new Exception("未找到支付宝商户账号"); + string path = App.WebHostEnvironment.ContentRootPath; + return new DefaultAopClient(new AlipayConfig + { + Format = "json", + Charset = "UTF-8", + ServerUrl = ServerUrl, + AppId = account.AppId, + SignType = account.SignType, + PrivateKey = account.PrivateKey, + EncryptKey = account.EncryptKey, + RootCertPath = Path.Combine(path, RootCertPath), + AppCertPath = Path.Combine(path, account.AppCertPath), + AlipayPublicCertPath = Path.Combine(path, account.AlipayPublicCertPath) + }); + } +} + +/// +/// 支付宝商户账号信息 +/// +public class AlipayMerchantAccount +{ + /// + /// 配置Id + /// + public long Id { get; init; } + + /// + /// 商户名称 + /// + public string Name { get; init; } + + /// + /// 商户AppId + /// + public string AppId { get; init; } + + /// + /// 应用私钥 + /// + public string PrivateKey { get; init; } + + /// + /// 从支付宝获取敏感信息时的加密密钥(可选) + /// + public string EncryptKey { get; init; } + + /// + /// 加密算法 + /// + public string SignType { get; init; } + + /// + /// 应用公钥证书路径 + /// + public string AppCertPath { get; init; } + + /// + /// 支付宝公钥证书路径 + /// + public string AlipayPublicCertPath { get; init; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/CDConfigOptions.cs b/Admin.NET.Core/Option/CDConfigOptions.cs new file mode 100644 index 0000000..5ed01e9 --- /dev/null +++ b/Admin.NET.Core/Option/CDConfigOptions.cs @@ -0,0 +1,84 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// CI/CD 配置选项 +/// +public class CDConfigOptions : IConfigurableOptions +{ + /// + /// 是否启用 + /// + public bool Enabled { get; set; } + + /// + /// 用户名 + /// + public string Owner { get; set; } + + /// + /// 仓库名 + /// + public string Repo { get; set; } + + /// + /// 分支名 + /// + public string Branch { get; set; } + + /// + /// 用户授权码 + /// + public string AccessToken { get; set; } + + /// + /// 更新间隔限制(分钟)0 不限制 + /// + public int UpdateInterval { get; set; } + + /// + /// 保留备份文件的数量, 0 不限制 + /// + public int BackupCount { get; set; } + + /// + /// 输出目录配置 + /// + public string BackendOutput { get; set; } + + /// + /// 发布配置选项 + /// + public PublishOptions Publish { get; set; } + + /// + /// 排除文件列表 + /// + public List ExcludeFiles { get; set; } +} + +/// +/// 编译发布配置选项 +/// +public class PublishOptions +{ + /// + /// 发布环境配置 + /// + public string Configuration { get; set; } + + /// + /// 目标框架 + /// + public string TargetFramework { get; set; } + + /// + /// 运行环境 + /// + public string RuntimeIdentifier { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/CacheOptions.cs b/Admin.NET.Core/Option/CacheOptions.cs new file mode 100644 index 0000000..b9ee81b --- /dev/null +++ b/Admin.NET.Core/Option/CacheOptions.cs @@ -0,0 +1,142 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 缓存配置选项 +/// +public sealed class CacheOptions : IConfigurableOptions +{ + /// + /// 缓存前缀 + /// + public string Prefix { get; set; } + + /// + /// 缓存类型 + /// + public string CacheType { get; set; } + + /// + /// Redis缓存 + /// + public RedisOption Redis { get; set; } + + public void PostConfigure(CacheOptions options, IConfiguration configuration) + { + options.Prefix = string.IsNullOrWhiteSpace(options.Prefix) ? "" : options.Prefix.Trim(); + } +} + +/// +/// Redis缓存 +/// +public sealed class RedisOption : RedisOptions +{ + /// + /// 最大消息大小 + /// + public int MaxMessageSize { get; set; } +} + +/// +/// 集群配置选项 +/// +public sealed class ClusterOptions : IConfigurableOptions +{ + /// + /// 是否启用 + /// + public bool Enabled { get; set; } + + /// + /// 服务器标识 + /// + public string ServerId { get; set; } + + /// + /// 服务器IP + /// + public string ServerIp { get; set; } + + /// + /// SignalR配置 + /// + public ClusterSignalR SignalR { get; set; } + + /// + /// 数据保护key + /// + public string DataProtecteKey { get; set; } + + /// + /// 是否哨兵模式 + /// + public bool IsSentinel { get; set; } + + /// + /// 哨兵配置 + /// + public StackExchangeSentinelConfig SentinelConfig { get; set; } +} + +/// +/// 集群SignalR配置 +/// +public sealed class ClusterSignalR +{ + /// + /// Redis连接字符串 + /// + public string RedisConfiguration { get; set; } + + /// + /// 缓存前缀 + /// + public string ChannelPrefix { get; set; } +} + +/// +/// 哨兵配置 +/// +public sealed class StackExchangeSentinelConfig +{ + /// + /// master名称 + /// + public string ServiceName { get; set; } + + /// + /// master访问密码 + /// + public string Password { get; set; } + + /// + /// 哨兵访问密码 + /// + public string SentinelPassword { get; set; } + + /// + /// 哨兵端口 + /// + public List EndPoints { get; set; } + + /// + /// 默认库 + /// + public int DefaultDb { get; set; } + + /// + /// 主前缀 + /// + public string MainPrefix { get; set; } + + /// + /// SignalR前缀 + /// + public string SignalRChannelPrefix { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/CodeGenOptions.cs b/Admin.NET.Core/Option/CodeGenOptions.cs new file mode 100644 index 0000000..66735f7 --- /dev/null +++ b/Admin.NET.Core/Option/CodeGenOptions.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 代码生成配置选项 +/// +public sealed class CodeGenOptions : IConfigurableOptions +{ + /// + /// 数据库实体程序集名称集合 + /// + public List EntityAssemblyNames { get; set; } + + /// + /// 数据库实体基类名称集合 + /// + public List BaseEntityNames { get; set; } + + /// + /// 前端文件根目录 + /// + public string FrontRootPath { get; set; } + + /// + /// 后端生成到的项目 + /// + public List BackendApplicationNamespaces { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/CryptogramOptions.cs b/Admin.NET.Core/Option/CryptogramOptions.cs new file mode 100644 index 0000000..44a43ec --- /dev/null +++ b/Admin.NET.Core/Option/CryptogramOptions.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 密码配置选项 +/// +public sealed class CryptogramOptions : IConfigurableOptions +{ + /// + /// 是否开启密码强度验证 + /// + public bool StrongPassword { get; set; } + + /// + /// 密码强度验证正则表达式 + /// + public string PasswordStrengthValidation { get; set; } + + /// + /// 密码强度验证提示 + /// + public string PasswordStrengthValidationMsg { get; set; } + + /// + /// 密码类型 + /// + public string CryptoType { get; set; } + + /// + /// 公钥 + /// + public string PublicKey { get; set; } + + /// + /// 私钥 + /// + public string PrivateKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/DbConnectionOptions.cs b/Admin.NET.Core/Option/DbConnectionOptions.cs new file mode 100644 index 0000000..f1c046f --- /dev/null +++ b/Admin.NET.Core/Option/DbConnectionOptions.cs @@ -0,0 +1,136 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据库配置选项 +/// +public sealed class DbConnectionOptions : IConfigurableOptions +{ + /// + /// 启用控制台打印SQL + /// + public bool EnableConsoleSql { get; set; } + + /// + /// 超级管理员是否忽略逻辑删除过滤器 + /// + public bool SuperAdminIgnoreIDeletedFilter { get; set; } + + /// + /// 数据库集合 + /// + public List ConnectionConfigs { get; set; } + + public void PostConfigure(DbConnectionOptions options, IConfiguration configuration) + { + foreach (var dbConfig in options.ConnectionConfigs) + { + if (dbConfig.ConfigId == null || string.IsNullOrWhiteSpace(dbConfig.ConfigId.ToString())) + dbConfig.ConfigId = SqlSugarConst.MainConfigId; + } + } +} + +/// +/// 数据库连接配置 +/// +public sealed class DbConnectionConfig : ConnectionConfig +{ + /// + /// 数据库名称 + /// + public string DbNickName { get; set; } + + /// + /// 数据库配置 + /// + public DbSettings DbSettings { get; set; } + + /// + /// 表配置 + /// + public TableSettings TableSettings { get; set; } + + /// + /// 种子配置 + /// + public SeedSettings SeedSettings { get; set; } + + /// + /// 隔离方式 + /// + public TenantTypeEnum TenantType { get; set; } = TenantTypeEnum.Id; + + /// + /// 数据库存储目录(仅SqlServer支持指定目录创建) + /// + public string DatabaseDirectory { get; set; } +} + +/// +/// 数据库配置 +/// +public sealed class DbSettings +{ + /// + /// 启用库表初始化 + /// + public bool EnableInitDb { get; set; } + + /// + /// 启用视图初始化 + /// + public bool EnableInitView { get; set; } + + /// + /// 启用库表差异日志 + /// + public bool EnableDiffLog { get; set; } + + /// + /// 启用驼峰转下划线 + /// + public bool EnableUnderLine { get; set; } + + /// + /// 启用数据库连接串加密策略 + /// + public bool EnableConnStringEncrypt { get; set; } +} + +/// +/// 表配置 +/// +public sealed class TableSettings +{ + /// + /// 启用表初始化 + /// + public bool EnableInitTable { get; set; } + + /// + /// 启用表增量更新 + /// + public bool EnableIncreTable { get; set; } +} + +/// +/// 种子配置 +/// +public sealed class SeedSettings +{ + /// + /// 启用种子初始化 + /// + public bool EnableInitSeed { get; set; } + + /// + /// 启用种子增量更新 + /// + public bool EnableIncreSeed { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/DeepSeekOptions.cs b/Admin.NET.Core/Option/DeepSeekOptions.cs new file mode 100644 index 0000000..5f9b0b3 --- /dev/null +++ b/Admin.NET.Core/Option/DeepSeekOptions.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public sealed class DeepSeekOptions : IConfigurableOptions +{ + /// + /// 源语言 + /// + public string SourceLang { get; set; } + + /// + /// Api地址 + /// + public string ApiUrl { get; set; } + + /// + /// API KEY + /// + public string ApiKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/ElasticSearchOptions.cs b/Admin.NET.Core/Option/ElasticSearchOptions.cs new file mode 100644 index 0000000..1e6f73a --- /dev/null +++ b/Admin.NET.Core/Option/ElasticSearchOptions.cs @@ -0,0 +1,64 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// ES配置选项 +/// +public class ElasticSearchOptions +{ + /// + /// 是否启用 + /// + public bool Enabled { get; set; } = false; + + /// + /// ES认证类型,可选 Basic、ApiKey、Base64ApiKey + /// + public ElasticSearchAuthTypeEnum AuthType { get; set; } + + /// + /// Basic认证的用户名 + /// + public string User { get; set; } + + /// + /// Basic认证的密码 + /// + public string Password { get; set; } + + /// + /// ApiKey认证的ApiId + /// + public string ApiId { get; set; } + + /// + /// ApiKey认证的ApiKey + /// + public string ApiKey { get; set; } + + /// + /// Base64ApiKey认证时加密的加密字符串 + /// + public string Base64ApiKey { get; set; } + + /// + /// ES使用Https时的证书指纹,使用证书请自行实现 + /// https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/connecting.html + /// + public string Fingerprint { get; set; } + + /// + /// 地址 + /// + public List ServerUris { get; set; } = new List(); + + /// + /// 索引 + /// + public string DefaultIndex { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/EmailOptions.cs b/Admin.NET.Core/Option/EmailOptions.cs new file mode 100644 index 0000000..10933b4 --- /dev/null +++ b/Admin.NET.Core/Option/EmailOptions.cs @@ -0,0 +1,58 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 邮件配置选项 +/// +public sealed class EmailOptions : IConfigurableOptions +{ + /// + /// 主机 + /// + public string Host { get; set; } + + /// + /// 端口 + /// + public int Port { get; set; } + + /// + /// 默认发件者邮箱 + /// + public string DefaultFromEmail { get; set; } + + /// + /// 默认接收人邮箱 + /// + public string DefaultToEmail { get; set; } + + /// + /// 启用SSL + /// + public bool EnableSsl { get; set; } + + ///// + ///// 是否使用默认凭据 + ///// + //public bool UseDefaultCredentials { get; set; } + + /// + /// 邮箱账号 + /// + public string UserName { get; set; } + + /// + /// 邮箱密码 + /// + public string Password { get; set; } + + /// + /// 默认邮件标题 + /// + public string DefaultFromName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/EnumOptions.cs b/Admin.NET.Core/Option/EnumOptions.cs new file mode 100644 index 0000000..2da8992 --- /dev/null +++ b/Admin.NET.Core/Option/EnumOptions.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举配置选项 +/// +public sealed class EnumOptions : IConfigurableOptions +{ + /// + /// 枚举实体程序集名称集合 + /// + public List EntityAssemblyNames { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/EventBusOptions.cs b/Admin.NET.Core/Option/EventBusOptions.cs new file mode 100644 index 0000000..45e7261 --- /dev/null +++ b/Admin.NET.Core/Option/EventBusOptions.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件总线配置选项 +/// +public sealed class EventBusOptions : IConfigurableOptions +{ + /// + /// RabbitMQ + /// + public RabbitMQSettings RabbitMQ { get; set; } +} + +/// +/// RabbitMQ +/// +public sealed class RabbitMQSettings +{ + /// + /// 账号 + /// + public string UserName { get; set; } + + /// + /// 密码 + /// + public string Password { get; set; } + + /// + /// 主机 + /// + public string HostName { get; set; } + + /// + /// 端口 + /// + public int Port { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/LocalizationSettingsOptions.cs b/Admin.NET.Core/Option/LocalizationSettingsOptions.cs new file mode 100644 index 0000000..c4da9ef --- /dev/null +++ b/Admin.NET.Core/Option/LocalizationSettingsOptions.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public sealed class LocalizationSettingsOptions : IConfigurableOptions +{ + /// + /// 语言列表 + /// + public List SupportedCultures { get; set; } + + /// + /// 默认语言 + /// + public string DefaultCulture { get; set; } + + /// + /// 固定时间区域为特定时区(多语言) + /// + public string DateTimeFormatCulture { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/OAuthOptions.cs b/Admin.NET.Core/Option/OAuthOptions.cs new file mode 100644 index 0000000..4ea822b --- /dev/null +++ b/Admin.NET.Core/Option/OAuthOptions.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 第三方登录授权配置选项 +/// +public sealed class OAuthOptions : IConfigurableOptions +{ + /// + /// Weixin配置 + /// + public Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions Weixin { get; set; } + + /// + /// Gitee配置 + /// + public Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions Gitee { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/PayCallBackOptions.cs b/Admin.NET.Core/Option/PayCallBackOptions.cs new file mode 100644 index 0000000..b110fd0 --- /dev/null +++ b/Admin.NET.Core/Option/PayCallBackOptions.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 支付回调配置选项 +/// +public sealed class PayCallBackOptions : IConfigurableOptions +{ + /// + /// 微信支付回调 + /// + public string WechatPayUrl { get; set; } + + /// + /// 微信退款回调 + /// + public string WechatRefundUrl { get; set; } + + /// + /// 支付宝支付回调 + /// + public string AlipayUrl { get; set; } + + /// + /// 支付宝退款回调 + /// + public string AlipayRefundUrl { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/RateLimitOptions.cs b/Admin.NET.Core/Option/RateLimitOptions.cs new file mode 100644 index 0000000..057166a --- /dev/null +++ b/Admin.NET.Core/Option/RateLimitOptions.cs @@ -0,0 +1,37 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using AspNetCoreRateLimit; + +namespace Admin.NET.Core; + +/// +/// IP限流配置选项 +/// +public sealed class IpRateLimitingOptions : IpRateLimitOptions +{ +} + +/// +/// IP限流策略配置选项 +/// +public sealed class IpRateLimitPoliciesOptions : IpRateLimitPolicies, IConfigurableOptions +{ +} + +/// +/// 客户端限流配置选项 +/// +public sealed class ClientRateLimitingOptions : ClientRateLimitOptions, IConfigurableOptions +{ +} + +/// +/// 客户端限流策略配置选项 +/// +public sealed class ClientRateLimitPoliciesOptions : ClientRateLimitPolicies, IConfigurableOptions +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/SMSOptions.cs b/Admin.NET.Core/Option/SMSOptions.cs new file mode 100644 index 0000000..524e08b --- /dev/null +++ b/Admin.NET.Core/Option/SMSOptions.cs @@ -0,0 +1,138 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 短信配置选项 +/// +public sealed class SMSOptions : IConfigurableOptions +{ + /// + /// 验证码缓存过期时间(秒) + /// 默认: 60秒 + /// + public int VerifyCodeExpireSeconds { get; set; } = 60; + + /// + /// Aliyun + /// + public SMSSettings Aliyun { get; set; } + + /// + /// Tencentyun + /// + public SMSSettings Tencentyun { get; set; } + + /// + /// Custom 自定义短信接口 + /// + public CustomSMSSettings Custom { get; set; } +} + +public sealed class SMSSettings +{ + /// + /// SdkAppId + /// + public string SdkAppId { get; set; } + + /// + /// AccessKey ID + /// + public string AccessKeyId { get; set; } + + /// + /// AccessKey Secret + /// + public string AccessKeySecret { get; set; } + + /// + /// Templates + /// + public List Templates { get; set; } + + /// + /// GetTemplate + /// + public SmsTemplate GetTemplate(string id = "0") + { + foreach (var template in Templates) + { + if (template.Id == id) { return template; } + } + return null; + } +} + +public class SmsTemplate +{ + public string Id { get; set; } = string.Empty; + public string SignName { get; set; } + public string TemplateCode { get; set; } + public string Content { get; set; } +} + +/// +/// 自定义短信配置 +/// +public sealed class CustomSMSSettings +{ + /// + /// 是否启用自定义短信接口 + /// + public bool Enabled { get; set; } + + /// + /// API 接口地址模板 + /// 支持占位符: {mobile} - 手机号, {content} - 短信内容, {code} - 验证码 + /// + /// 示例: https://api.xxxx.com/sms?u=xxxx&key=59e03f49c3dbb5033&m={mobile}&c={content} + public string ApiUrl { get; set; } + + /// + /// 请求方法 (GET/POST) + /// + public string Method { get; set; } = "GET"; + + /// + /// POST 请求的 Content-Type (application/json 或 application/x-www-form-urlencoded) + /// 默认: application/x-www-form-urlencoded + /// + public string ContentType { get; set; } = "application/x-www-form-urlencoded"; + + /// + /// POST 请求的数据模板(支持占位符) + /// + /// + /// JSON 格式示例: {"mobile":"{mobile}","content":"{content}","apikey":"your_key"}
+ /// Form 格式示例: mobile={mobile}&content={content}&apikey=your_key + ///
+ public string PostData { get; set; } + + /// + /// 成功响应标识(用于判断发送是否成功) + /// 如果响应内容包含此字符串,则认为发送成功 + /// + public string SuccessFlag { get; set; } = "0"; + + /// + /// 短信模板列表 + /// + public List Templates { get; set; } + + /// + /// 获取模板 + /// + public SmsTemplate GetTemplate(string id = "0") + { + foreach (var template in Templates) + { + if (template.Id == id) { return template; } + } + return null; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/SnowIdOptions.cs b/Admin.NET.Core/Option/SnowIdOptions.cs new file mode 100644 index 0000000..07b2b53 --- /dev/null +++ b/Admin.NET.Core/Option/SnowIdOptions.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 雪花Id配置选项 +/// +public sealed class SnowIdOptions : IdGeneratorOptions, IConfigurableOptions +{ + /// + /// 缓存前缀 + /// + public string WorkerPrefix { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/UploadOptions.cs b/Admin.NET.Core/Option/UploadOptions.cs new file mode 100644 index 0000000..8eb7393 --- /dev/null +++ b/Admin.NET.Core/Option/UploadOptions.cs @@ -0,0 +1,60 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OnceMi.AspNetCore.OSS; + +namespace Admin.NET.Core; + +/// +/// 文件上传配置选项 +/// +public sealed class UploadOptions : IConfigurableOptions +{ + /// + /// 路径 + /// + public string Path { get; set; } + + /// + /// 大小 + /// + public long MaxSize { get; set; } + + /// + /// 上传格式 + /// + public List ContentType { get; set; } + + /// + /// 启用文件MD5验证 + /// + /// 防止重复上传 + public bool EnableMd5 { get; set; } +} + +/// +/// 对象存储配置选项 +/// +public sealed class OSSProviderOptions : OSSOptions, IConfigurableOptions +{ + /// + /// 是否启用OSS存储 + /// + public bool Enabled { get; set; } + + /// + /// 自定义桶名称 不能直接使用Provider来替代桶名称 + /// 例:阿里云 1.只能包括小写字母,数字,短横线(-)2.必须以小写字母或者数字开头 3.长度必须在3-63字节之间 + /// + public string Bucket { get; set; } + + /// + /// 自定义Host + /// 拼接外链的Host,若空则使用Endpoint拼接 + /// + /// + public string CustomHost { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/WechatOptions.cs b/Admin.NET.Core/Option/WechatOptions.cs new file mode 100644 index 0000000..1c47b6d --- /dev/null +++ b/Admin.NET.Core/Option/WechatOptions.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 微信相关配置选项 +/// +public sealed class WechatOptions : IConfigurableOptions +{ + // 公众号 + public string WechatAppId { get; set; } + + public string WechatAppSecret { get; set; } + + /// + /// 微信公众号服务器配置中的令牌(Token) + /// + public string WechatToken { get; set; } + + /// + /// 微信公众号服务器配置中的消息加解密密钥(EncodingAESKey) + /// + public string WechatEncodingAESKey { get; set; } + + // 小程序 + public string WxOpenAppId { get; set; } + + public string WxOpenAppSecret { get; set; } + + /// + /// 小程序消息推送中的令牌(Token) + /// + public string WxToken { get; set; } + + /// + /// 小程序消息推送中的消息加解密密钥(EncodingAESKey) + /// + public string WxEncodingAESKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Option/WechatPayOptions.cs b/Admin.NET.Core/Option/WechatPayOptions.cs new file mode 100644 index 0000000..cca5bb6 --- /dev/null +++ b/Admin.NET.Core/Option/WechatPayOptions.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 微信支付配置选项 +/// +public sealed class WechatPayOptions : WechatTenpayClientOptions, IConfigurableOptions +{ + /// + /// 微信公众平台AppId、开放平台AppId、小程序AppId、企业微信CorpId + /// + public string AppId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysConfigSeedData.cs b/Admin.NET.Core/SeedData/SysConfigSeedData.cs new file mode 100644 index 0000000..142b18c --- /dev/null +++ b/Admin.NET.Core/SeedData/SysConfigSeedData.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统配置表种子数据 +/// +public class SysConfigSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysConfig{ Id=1300000000101, Name="演示环境", Code=ConfigConst.SysDemoEnv, Value="False", SysFlag=YesNoEnum.Y, Remark="演示环境", OrderNo=10, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000111, Name="默认密码", Code=ConfigConst.SysPassword, Value="Admin.NET++010101", SysFlag=YesNoEnum.Y, Remark="默认密码", OrderNo=20, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000121, Name="密码最大错误次数", Code=ConfigConst.SysPasswordMaxErrorTimes, Value="5", SysFlag=YesNoEnum.Y, Remark="允许密码最大输入错误次数", OrderNo=30, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000131, Name="日志保留天数", Code=ConfigConst.SysLogRetentionDays, Value="180", SysFlag=YesNoEnum.Y, Remark="日志保留天数(天)", OrderNo=40, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000141, Name="记录操作日志", Code=ConfigConst.SysOpLog, Value="True", SysFlag=YesNoEnum.Y, Remark="是否记录操作日志", OrderNo=50, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000151, Name="单设备登录", Code=ConfigConst.SysSingleLogin, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启单设备登录", OrderNo=60, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000152, Name="登入登出提醒", Code=ConfigConst.SysLoginOutReminder, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启登入登出提醒", OrderNo=60, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000161, Name="登录二次验证", Code=ConfigConst.SysSecondVer, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启登录二次验证", OrderNo=70, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000171, Name="图形验证码", Code=ConfigConst.SysCaptcha, Value="True", SysFlag=YesNoEnum.Y, Remark="是否开启图形验证码", OrderNo=80, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000172, Name="登录时隐藏租户", Code=ConfigConst.SysHideTenantLogin, Value="True", SysFlag=YesNoEnum.Y, Remark="登录时隐藏租户", OrderNo=90, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000181, Name="Token过期时间", Code=ConfigConst.SysTokenExpire, Value="10080", SysFlag=YesNoEnum.Y, Remark="Token过期时间(分钟)", OrderNo=100, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000191, Name="RefreshToken过期时间", Code=ConfigConst.SysRefreshTokenExpire, Value="20160", SysFlag=YesNoEnum.Y, Remark="刷新Token过期时间(分钟)(一般 refresh_token 的有效时间 > 2 * access_token 的有效时间)", OrderNo=110, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000201, Name="发送异常日志邮件", Code=ConfigConst.SysErrorMail, Value="False", SysFlag=YesNoEnum.Y, Remark="是否发送异常日志邮件", OrderNo=120, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000211, Name="域登录验证", Code=ConfigConst.SysDomainLogin, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启域登录验证", OrderNo=130, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000221, Name="数据校验日志", Code=ConfigConst.SysValidationLog, Value="True", SysFlag=YesNoEnum.Y, Remark="是否数据校验日志", OrderNo=140, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000231, Name="行政区域同步层级", Code=ConfigConst.SysRegionSyncLevel, Value="3", SysFlag=YesNoEnum.Y, Remark="行政区域同步层级 1-省级,2-市级,3-区县级,4-街道级,5-村级", OrderNo=150, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysDictDataSeedData.cs b/Admin.NET.Core/SeedData/SysDictDataSeedData.cs new file mode 100644 index 0000000..9483362 --- /dev/null +++ b/Admin.NET.Core/SeedData/SysDictDataSeedData.cs @@ -0,0 +1,86 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统字典值表种子数据 +/// +public class SysDictDataSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var typeList = new SysDictTypeSeedData().HasData().ToList(); + return new[] + { + new SysDictData{ Id=1300000000101, DictTypeId=typeList[0].Id, Label="输入框", Value="Input", OrderNo=100, Remark="输入框", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000102, DictTypeId=typeList[0].Id, Label="字典选择器", Value="DictSelector", OrderNo=100, Remark="字典选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000103, DictTypeId=typeList[0].Id, Label="常量选择器", Value="ConstSelector", OrderNo=100, Remark="常量选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000104, DictTypeId=typeList[0].Id, Label="枚举选择器", Value="EnumSelector", OrderNo=100, Remark="枚举选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000105, DictTypeId=typeList[0].Id, Label="树选择器", Value="ApiTreeSelector", OrderNo=100, Remark="树选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000106, DictTypeId=typeList[0].Id, Label="外键", Value="ForeignKey", OrderNo=100, Remark="外键", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000107, DictTypeId=typeList[0].Id, Label="数字输入框", Value="InputNumber", OrderNo=100, Remark="数字输入框", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000108, DictTypeId=typeList[0].Id, Label="时间选择", Value="DatePicker", OrderNo=100, Remark="时间选择", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000109, DictTypeId=typeList[0].Id, Label="文本域", Value="InputTextArea", OrderNo=100, Remark="文本域", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000110, DictTypeId=typeList[0].Id, Label="上传", Value="Upload", OrderNo=100, Remark="上传", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000111, DictTypeId=typeList[0].Id, Label="开关", Value="Switch", OrderNo=100, Remark="开关", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000112, DictTypeId=typeList[0].Id, Label="上传单文件", Value="Upload_SingleFile", OrderNo=120, Remark="上传单文件", Status=StatusEnum.Enable, CreateTime= DateTime.Now }, + new SysDictData{ Id=1300000000113, DictTypeId=typeList[0].Id, Label="富文本编辑器", Value="Editor", OrderNo=130, Remark="富文本编辑器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2025-12-25 00:00:00") }, + + new SysDictData{ Id=1300000000201, DictTypeId=typeList[1].Id, Label="等于", Value="==", OrderNo=1, Remark="等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000202, DictTypeId=typeList[1].Id, Label="模糊", Value="like", OrderNo=1, Remark="模糊", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000203, DictTypeId=typeList[1].Id, Label="大于", Value=">", OrderNo=1, Remark="大于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000204, DictTypeId=typeList[1].Id, Label="小于", Value="<", OrderNo=1, Remark="小于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000205, DictTypeId=typeList[1].Id, Label="不等于", Value="!=", OrderNo=1, Remark="不等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000206, DictTypeId=typeList[1].Id, Label="大于等于", Value=">=", OrderNo=1, Remark="大于等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000207, DictTypeId=typeList[1].Id, Label="小于等于", Value="<=", OrderNo=1, Remark="小于等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000208, DictTypeId=typeList[1].Id, Label="不为空", Value="isNotNull", OrderNo=1, Remark="不为空", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000209, DictTypeId=typeList[1].Id, Label="时间范围", Value="~", OrderNo=1, Remark="时间范围", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000301, DictTypeId=typeList[2].Id, Label="long", Value="long", OrderNo=1, Remark="long", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000302, DictTypeId=typeList[2].Id, Label="string", Value="string", OrderNo=1, Remark="string", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000303, DictTypeId=typeList[2].Id, Label="DateTime", Value="DateTime", OrderNo=1, Remark="DateTime", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000304, DictTypeId=typeList[2].Id, Label="bool", Value="bool", OrderNo=1, Remark="bool", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000305, DictTypeId=typeList[2].Id, Label="int", Value="int", OrderNo=1, Remark="int", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000306, DictTypeId=typeList[2].Id, Label="double", Value="double", OrderNo=1, Remark="double", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000307, DictTypeId=typeList[2].Id, Label="float", Value="float", OrderNo=1, Remark="float", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000308, DictTypeId=typeList[2].Id, Label="decimal", Value="decimal", OrderNo=1, Remark="decimal", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000309, DictTypeId=typeList[2].Id, Label="Guid", Value="Guid", OrderNo=1, Remark="Guid", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000310, DictTypeId=typeList[2].Id, Label="DateTimeOffset", Value="DateTimeOffset", OrderNo=1, Remark="DateTimeOffset", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000401, DictTypeId=typeList[3].Id, Label="下载压缩包", Value="100", OrderNo=1, Remark="下载压缩包", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000402, DictTypeId=typeList[3].Id, Label="下载压缩包(前端)", Value="111", OrderNo=2, Remark="下载压缩包(前端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000403, DictTypeId=typeList[3].Id, Label="下载压缩包(后端)", Value="121", OrderNo=3, Remark="下载压缩包(后端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000404, DictTypeId=typeList[3].Id, Label="生成到本项目", Value="200", OrderNo=4, Remark="生成到本项目", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000405, DictTypeId=typeList[3].Id, Label="生成到本项目(前端)", Value="211", OrderNo=5, Remark="生成到本项目(前端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000406, DictTypeId=typeList[3].Id, Label="生成到本项目(后端)", Value="221", OrderNo=6, Remark="生成到本项目(后端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000501, DictTypeId=typeList[4].Id, Label="EntityBaseId【基础实体Id】", Value="EntityBaseId", OrderNo=1, Remark="【基础实体Id】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000502, DictTypeId=typeList[4].Id, Label="EntityBase【基础实体】", Value="EntityBase", OrderNo=1, Remark="【基础实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000503, DictTypeId=typeList[4].Id, Label="EntityBaseDel【基础软删除实体】", Value="EntityBaseDel", OrderNo=1, Remark="【基础软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000504, DictTypeId=typeList[4].Id, Label="EntityBaseOrg【机构实体】", Value="EntityBaseOrg", OrderNo=1, Remark="【机构实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000505, DictTypeId=typeList[4].Id, Label="EntityBaseOrgDel【机构软删除实体】", Value="EntityBaseOrgDel", OrderNo=1, Remark="【机构软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000506, DictTypeId=typeList[4].Id, Label="EntityBaseTenantId【租户实体Id】", Value="EntityBaseTenantId", OrderNo=1, Remark="【租户实体Id】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000507, DictTypeId=typeList[4].Id, Label="EntityBaseTenant【租户实体】", Value="EntityBaseTenant", OrderNo=1, Remark="【租户实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000508, DictTypeId=typeList[4].Id, Label="EntityBaseTenantDel【租户软删除实体】", Value="EntityBaseTenantDel", OrderNo=1, Remark="【租户软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000509, DictTypeId=typeList[4].Id, Label="EntityBaseTenantOrg【租户机构实体】", Value="EntityBaseTenantOrg", OrderNo=1, Remark="【租户机构实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000510, DictTypeId=typeList[4].Id, Label="EntityBaseTenantOrgDel【租户机构软删除实体】", Value="EntityBaseTenantOrgDel", OrderNo=1, Remark="【租户机构软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000601, DictTypeId=typeList[5].Id, Label="不需要", Value="off", OrderNo=100, Remark="不需要打印支持", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + new SysDictData{ Id=1300000000602, DictTypeId=typeList[5].Id, Label="绑定打印模版", Value="custom", OrderNo=101, Remark="绑定打印模版", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + + new SysDictData{ Id=1300000000701, DictTypeId=typeList[6].Id, Label="集团", Value="101", OrderNo=100, Remark="集团", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000702, DictTypeId=typeList[6].Id, Label="公司", Value="201", OrderNo=101, Remark="公司", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000703, DictTypeId=typeList[6].Id, Label="部门", Value="301", OrderNo=102, Remark="部门", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000704, DictTypeId=typeList[6].Id, Label="区域", Value="401", OrderNo=103, Remark="区域", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000705, DictTypeId=typeList[6].Id, Label="组", Value="501", OrderNo=104, Remark="组", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysDictTypeSeedData.cs b/Admin.NET.Core/SeedData/SysDictTypeSeedData.cs new file mode 100644 index 0000000..47b6def --- /dev/null +++ b/Admin.NET.Core/SeedData/SysDictTypeSeedData.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统字典类型表种子数据 +/// +public class SysDictTypeSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysDictType{ Id=1300000000111, Name="代码生成控件类型", Code="code_gen_effect_type", SysFlag=YesNoEnum.Y, OrderNo=100, Remark="代码生成控件类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000121, Name="代码生成查询类型", Code="code_gen_query_type", SysFlag=YesNoEnum.Y, OrderNo=101, Remark="代码生成查询类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000131, Name="代码生成.NET类型", Code="code_gen_net_type", SysFlag=YesNoEnum.Y, OrderNo=102, Remark="代码生成.NET类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000141, Name="代码生成方式", Code="code_gen_create_type", SysFlag=YesNoEnum.Y, OrderNo=103, Remark="代码生成方式", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000151, Name="代码生成基类", Code="code_gen_base_class", SysFlag=YesNoEnum.Y, OrderNo=104, Remark="代码生成基类", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000161, Name="代码生成打印类型", Code="code_gen_print_type", SysFlag=YesNoEnum.Y, OrderNo=105, Remark="代码生成打印类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + new SysDictType{ Id=1300000000171, Name="机构类型", Code="org_type", SysFlag=YesNoEnum.Y, OrderNo=201, Remark="机构类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysLangSeedData.cs b/Admin.NET.Core/SeedData/SysLangSeedData.cs new file mode 100644 index 0000000..b527bd3 --- /dev/null +++ b/Admin.NET.Core/SeedData/SysLangSeedData.cs @@ -0,0 +1,112 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 语言表种子数据 +/// +public class SysLangSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysLang{ Id=1300000000001,Name="Chinese (Simplified) / 简体中文", Code="zh-CN", IsoCode="zh_CN", UrlCode="zh-cn", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日", TimeFormat="%H时%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true}, + new SysLang{ Id=1300000000002,Name="Chinese (HK) / 繁體中文", Code="zh-HK", IsoCode="zh_HK", UrlCode="zh-hk", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日 %A", TimeFormat="%I時%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true}, + new SysLang{ Id=1300000000003,Name="Chinese (Traditional) / 繁體中文", Code="zh-TW", IsoCode="zh_TW", UrlCode="zh-tw", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日", TimeFormat="%H時%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true}, + new SysLang{ Id=1300000000004,Name="Italian / Italiano", Code="it-IT", IsoCode="it", UrlCode="it", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=true}, + new SysLang{ Id=1300000000005,Name="English (US)", Code="en-US", IsoCode="en", UrlCode="en", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true}, + new SysLang{ Id=1300000000006,Name="Amharic / አምሃርኛ", Code="am-ET", IsoCode="am_ET", UrlCode="am-et", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000007,Name="Arabic / الْعَرَبيّة", Code="ar-001", IsoCode="ar", UrlCode="ar", Direction=DirectionEnum.Rtl, DateFormat="%d %b, %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Saturday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000008,Name="Arabic (Syria) / الْعَرَبيّة", Code="ar-SY", IsoCode="ar_SY", UrlCode="ar-sy", Direction=DirectionEnum.Rtl, DateFormat="%d %b, %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Saturday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000009,Name="Azerbaijani / Azərbaycanca", Code="az-AZ", IsoCode="az", UrlCode="az", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000010,Name="Basque / Euskara", Code="eu-ES", IsoCode="eu_ES", UrlCode="eu-es", Direction=DirectionEnum.Ltr, DateFormat="%a, %Y.eko %bren %da", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000011,Name="Bengali / বাংলা", Code="bn-IN", IsoCode="bn_IN", UrlCode="bn-in", Direction=DirectionEnum.Ltr, DateFormat="%A %d %b %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000012,Name="Bosnian / bosanski jezik", Code="bs-BA", IsoCode="bs", UrlCode="bs", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000013,Name="Bulgarian / български език", Code="bg-BG", IsoCode="bg", UrlCode="bg", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H,%M,%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000014,Name="Catalan / Català", Code="ca-ES", IsoCode="ca_ES", UrlCode="ca-es", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000015,Name="Croatian / hrvatski jezik", Code="hr-HR", IsoCode="hr", UrlCode="hr", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000016,Name="Czech / Čeština", Code="cs-CZ", IsoCode="cs_CZ", UrlCode="cs-cz", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000017,Name="Danish / Dansk", Code="da-DK", IsoCode="da_DK", UrlCode="da-dk", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000018,Name="Dutch (BE) / Nederlands (BE)", Code="nl-BE", IsoCode="nl_BE", UrlCode="nl-be", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000019,Name="Dutch / Nederlands", Code="nl-NL", IsoCode="nl", UrlCode="nl", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000020,Name="English (AU)", Code="en-AU", IsoCode="en_AU", UrlCode="en-au", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000021,Name="English (CA)", Code="en-CA", IsoCode="en_CA", UrlCode="en-ca", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000022,Name="English (UK)", Code="en-GB", IsoCode="en_GB", UrlCode="en-gb", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000023,Name="English (IN)", Code="en-IN", IsoCode="en_IN", UrlCode="en-in", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,2,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000024,Name="Estonian / Eesti keel", Code="et-EE", IsoCode="et", UrlCode="et", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000025,Name="Finnish / Suomi", Code="fi-FI", IsoCode="fi", UrlCode="fi", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H.%M.%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000026,Name="French (BE) / Français (BE)", Code="fr-BE", IsoCode="fr_BE", UrlCode="fr-be", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000027,Name="French (CA) / Français (CA)", Code="fr-CA", IsoCode="fr_CA", UrlCode="fr-ca", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000028,Name="French (CH) / Français (CH)", Code="fr-CH", IsoCode="fr_CH", UrlCode="fr-ch", Direction=DirectionEnum.Ltr, DateFormat="%d. %m. %Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep="'",Active=false}, + new SysLang{ Id=1300000000029,Name="French / Français", Code="fr-FR", IsoCode="fr", UrlCode="fr", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000030,Name="Galician / Galego", Code="gl-ES", IsoCode="gl", UrlCode="gl", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000031,Name="Georgian / ქართული ენა", Code="ka-GE", IsoCode="ka", UrlCode="ka", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000032,Name="German / Deutsch", Code="de-DE", IsoCode="de", UrlCode="de", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000033,Name="German (CH) / Deutsch (CH)", Code="de-CH", IsoCode="de_CH", UrlCode="de-ch", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,3]",DecimalPoint=".",ThousandsSep="'",Active=false}, + new SysLang{ Id=1300000000034,Name="Greek / Ελληνικά", Code="el-GR", IsoCode="el_GR", UrlCode="el-gr", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000035,Name="Gujarati / ગુજરાતી", Code="gu-IN", IsoCode="gu", UrlCode="gu", Direction=DirectionEnum.Ltr, DateFormat="%A %d %b %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000036,Name="Hebrew / עִבְרִי", Code="he-IL", IsoCode="he", UrlCode="he", Direction=DirectionEnum.Rtl, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000037,Name="Hindi / हिंदी", Code="hi-IN", IsoCode="hi", UrlCode="hi", Direction=DirectionEnum.Ltr, DateFormat="%A %d %b %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000038,Name="Hungarian / Magyar", Code="hu-HU", IsoCode="hu", UrlCode="hu", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000039,Name="Indonesian / Bahasa Indonesia", Code="id-ID", IsoCode="id", UrlCode="id", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000040,Name="Japanese / 日本語", Code="ja-JP", IsoCode="ja", UrlCode="ja", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日", TimeFormat="%H時%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000041,Name="Kabyle / Taqbaylit", Code="kab-DZ", IsoCode="kab", UrlCode="kab", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Saturday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000042,Name="Khmer / ភាសាខ្មែរ", Code="km-KH", IsoCode="km", UrlCode="km", Direction=DirectionEnum.Ltr, DateFormat="%d %B %Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000043,Name="Korean (KP) / 한국어 (KP)", Code="ko-KP", IsoCode="ko_KP", UrlCode="ko-kp", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000044,Name="Korean (KR) / 한국어 (KR)", Code="ko-KR", IsoCode="ko_KR", UrlCode="ko-kr", Direction=DirectionEnum.Ltr, DateFormat="%Y년 %m월 %d일", TimeFormat="%H시 %M분 %S초",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000045,Name="Lao / ພາສາລາວ", Code="lo-LA", IsoCode="lo", UrlCode="lo", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000046,Name="Latvian / latviešu valoda", Code="lv-LV", IsoCode="lv", UrlCode="lv", Direction=DirectionEnum.Ltr, DateFormat="%Y.%m.%d.", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000047,Name="Lithuanian / Lietuvių kalba", Code="lt-LT", IsoCode="lt", UrlCode="lt", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000048,Name="Luxembourgish", Code="lb-LU", IsoCode="lb", UrlCode="lb", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000049,Name="Macedonian / македонски јазик", Code="mk-MK", IsoCode="mk", UrlCode="mk", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000050,Name="Malayalam / മലയാളം", Code="ml-IN", IsoCode="ml", UrlCode="ml", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000051,Name="Mongolian / монгол", Code="mn-MN", IsoCode="mn", UrlCode="mn", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep="'",Active=false}, + new SysLang{ Id=1300000000052,Name="Malay / Bahasa Melayu", Code="ms-MY", IsoCode="ms", UrlCode="ms", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000053,Name="Norwegian Bokmål / Norsk bokmål", Code="nb-NO", IsoCode="nb_NO", UrlCode="nb-no", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000054,Name="Persian / فارسی", Code="fa-IR", IsoCode="fa", UrlCode="fa", Direction=DirectionEnum.Rtl, DateFormat="%Y/%m/%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Saturday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000055,Name="Polish / Język polski", Code="pl-PL", IsoCode="pl", UrlCode="pl", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000056,Name="Portuguese (AO) / Português (AO)", Code="pt-AO", IsoCode="pt_AO", UrlCode="pt-ao", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000057,Name="Portuguese (BR) / Português (BR)", Code="pt-BR", IsoCode="pt_BR", UrlCode="pt-br", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000058,Name="Portuguese / Português", Code="pt-PT", IsoCode="pt", UrlCode="pt", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000059,Name="Romanian / română", Code="ro-RO", IsoCode="ro", UrlCode="ro", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000060,Name="Russian / русский язык", Code="ru-RU", IsoCode="ru", UrlCode="ru", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000061,Name="Serbian (Cyrillic) / српски", Code="sr-Cyrl-RS", IsoCode="sr_RS", UrlCode="sr-rs", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y.", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000062,Name="Serbian (Latin) / srpski", Code="sr-Latn-RS", IsoCode="sr@latin", UrlCode="sr@latin", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000063,Name="Slovak / Slovenský jazyk", Code="sk-SK", IsoCode="sk", UrlCode="sk", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000064,Name="Slovenian / slovenščina", Code="sl-SI", IsoCode="sl", UrlCode="sl", Direction=DirectionEnum.Ltr, DateFormat="%d. %m. %Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000065,Name="Spanish (AR) / Español (AR)", Code="es-AR", IsoCode="es_AR", UrlCode="es-ar", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000066,Name="Spanish (BO) / Español (BO)", Code="es-BO", IsoCode="es_BO", UrlCode="es-bo", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000067,Name="Spanish (CL) / Español (CL)", Code="es-CL", IsoCode="es_CL", UrlCode="es-cl", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000068,Name="Spanish (CO) / Español (CO)", Code="es-CO", IsoCode="es_CO", UrlCode="es-co", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000069,Name="Spanish (CR) / Español (CR)", Code="es-CR", IsoCode="es_CR", UrlCode="es-cr", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000070,Name="Spanish (DO) / Español (DO)", Code="es-DO", IsoCode="es_DO", UrlCode="es-do", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000071,Name="Spanish (EC) / Español (EC)", Code="es-EC", IsoCode="es_EC", UrlCode="es-ec", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000072,Name="Spanish (GT) / Español (GT)", Code="es-GT", IsoCode="es_GT", UrlCode="es-gt", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000073,Name="Spanish (MX) / Español (MX)", Code="es-MX", IsoCode="es_MX", UrlCode="es-mx", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000074,Name="Spanish (PA) / Español (PA)", Code="es-PA", IsoCode="es_PA", UrlCode="es-pa", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000075,Name="Spanish (PE) / Español (PE)", Code="es-PE", IsoCode="es_PE", UrlCode="es-pe", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000076,Name="Spanish (PY) / Español (PY)", Code="es-PY", IsoCode="es_PY", UrlCode="es-py", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000077,Name="Spanish (UY) / Español (UY)", Code="es-UY", IsoCode="es_UY", UrlCode="es-uy", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000078,Name="Spanish (VE) / Español (VE)", Code="es-VE", IsoCode="es_VE", UrlCode="es-ve", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000079,Name="Spanish / Español", Code="es-ES", IsoCode="es", UrlCode="es", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000080,Name="Swedish / Svenska", Code="sv-SE", IsoCode="sv", UrlCode="sv", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000081,Name="Thai / ภาษาไทย", Code="th-TH", IsoCode="th", UrlCode="th", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000082,Name="Tagalog / Filipino", Code="tl-PH", IsoCode="tl", UrlCode="tl", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000083,Name="Turkish / Türkçe", Code="tr-TR", IsoCode="tr", UrlCode="tr", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000084,Name="Ukrainian / українська", Code="uk-UA", IsoCode="uk", UrlCode="uk", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000085,Name="Vietnamese / Tiếng Việt", Code="vi-VN", IsoCode="vi", UrlCode="vi", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000086,Name="Albanian / Shqip", Code="sq-AL", IsoCode="sq", UrlCode="sq", Direction=DirectionEnum.Ltr, DateFormat="%Y-%b-%d", TimeFormat="%I.%M.%S.",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000087,Name="Telugu / తెలుగు", Code="te-IN", IsoCode="te", UrlCode="te", Direction=DirectionEnum.Ltr, DateFormat="%B %d %A %Y", TimeFormat="%p%I.%M.%S",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000088,Name="Burmese / ဗမာစာ", Code="my-MM", IsoCode="my", UrlCode="mya", Direction=DirectionEnum.Ltr, DateFormat="%Y %b %d %A", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Sunday,Grouping="[3,3]",DecimalPoint=".",ThousandsSep=",",Active=false}, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysMenuSeedData.cs b/Admin.NET.Core/SeedData/SysMenuSeedData.cs new file mode 100644 index 0000000..bc5aa88 --- /dev/null +++ b/Admin.NET.Core/SeedData/SysMenuSeedData.cs @@ -0,0 +1,313 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统菜单表种子数据 +/// +public class SysMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysMenu{ Id=1300100000101, Pid=0, Title="工作台", Path="/dashboard", Name="dashboard", Component="Layout", Icon="ele-HomeFilled", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=0 }, + new SysMenu{ Id=1300100010101, Pid=1300100000101, Title="工作台", Path="/dashboard/home", Name="home", Component="/home/index", IsAffix=true, Icon="ele-HomeFilled", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300100010201, Pid=1300100000101, Title="站内信", Path="/dashboard/notice", Name="notice", Component="/home/notice/index", Icon="ele-Bell", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=101 }, + + // 建议此处Id范围之间放置具体业务应用菜单 + + #region 系统管理 + + new SysMenu{ Id=1300200000101, Pid=0, Title="系统管理", Path="/system", Name="system", Component="Layout", Icon="ele-Setting", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=10000 }, + + // 账号管理 + new SysMenu{ Id=1300200010101, Pid=1300200000101, Title="账号管理", Path="/system/user", Name="sysUser", Component="/system/user/index", Icon="ele-User", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010201, Pid=1300200010101, Title="查询", Permission="sysUser:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010301, Pid=1300200010101, Title="编辑", Permission="sysUser:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010401, Pid=1300200010101, Title="增加", Permission="sysUser:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010501, Pid=1300200010101, Title="删除", Permission="sysUser:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010601, Pid=1300200010101, Title="详情", Permission="sysUser:detail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010701, Pid=1300200010101, Title="授权角色", Permission="sysUser:grantRole", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010801, Pid=1300200010101, Title="重置密码", Permission="sysUser:resetPwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010901, Pid=1300200010101, Title="设置状态", Permission="sysUser:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200011001, Pid=1300200010101, Title="强制下线", Permission="sysOnlineUser:forceOffline", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200011101, Pid=1300200010101, Title="解除锁定", Permission="sysUser:unlockLogin", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 角色管理 + new SysMenu{ Id=1300200020101, Pid=1300200000101, Title="角色管理", Path="/system/role", Name="sysRole", Component="/system/role/index", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300200020201, Pid=1300200020101, Title="查询", Permission="sysRole:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020301, Pid=1300200020101, Title="编辑", Permission="sysRole:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020401, Pid=1300200020101, Title="增加", Permission="sysRole:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020501, Pid=1300200020101, Title="删除", Permission="sysRole:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020601, Pid=1300200020101, Title="授权菜单", Permission="sysRole:grantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020701, Pid=1300200020101, Title="授权数据", Permission="sysRole:grantDataScope", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020801, Pid=1300200020101, Title="设置状态", Permission="sysRole:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 机构管理 + new SysMenu{ Id=1300200030101, Pid=1300200000101, Title="机构管理", Path="/system/org", Name="sysOrg", Component="/system/org/index", Icon="ele-OfficeBuilding", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300200030201, Pid=1300200030101, Title="查询", Permission="sysOrg:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030301, Pid=1300200030101, Title="编辑", Permission="sysOrg:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030401, Pid=1300200030101, Title="增加", Permission="sysOrg:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030501, Pid=1300200030101, Title="删除", Permission="sysOrg:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 职位管理 + new SysMenu{ Id=1300200040101, Pid=1300200000101, Title="职位管理", Path="/system/pos", Name="sysPos", Component="/system/pos/index",Icon="ele-Mug", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300200040201, Pid=1300200040101, Title="查询", Permission="sysPos:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040301, Pid=1300200040101, Title="编辑", Permission="sysPos:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040401, Pid=1300200040101, Title="增加", Permission="sysPos:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040501, Pid=1300200040101, Title="删除", Permission="sysPos:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 个人中心 + new SysMenu{ Id=1300200050101, Pid=1300200000101, Title="个人中心", Path="/system/userCenter", Name="sysUserCenter", Component="/system/user/component/userCenter",Icon="ele-Medal", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300200050201, Pid=1300200050101, Title="修改密码", Permission="sysUser:changePwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050301, Pid=1300200050101, Title="基本信息", Permission="sysUser:baseInfo", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050401, Pid=1300200050101, Title="电子签名", Permission="sysFile:uploadSignature", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050501, Pid=1300200050101, Title="上传头像", Permission="sysFile:uploadAvatar", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 通知公告 + new SysMenu{ Id=1300200060101, Pid=1300200000101, Title="通知公告", Path="/system/notice", Name="sysNotice", Component="/system/notice/index",Icon="ele-Bell", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + new SysMenu{ Id=1300200060201, Pid=1300200060101, Title="查询", Permission="sysNotice:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060301, Pid=1300200060101, Title="编辑", Permission="sysNotice:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060401, Pid=1300200060101, Title="增加", Permission="sysNotice:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060501, Pid=1300200060101, Title="删除", Permission="sysNotice:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060601, Pid=1300200060101, Title="发布", Permission="sysNotice:public", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060701, Pid=1300200060101, Title="撤回", Permission="sysNotice:cancel", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 三方账号 + new SysMenu{ Id=1300200070101, Pid=1300200000101, Title="三方账号", Path="/system/weChatUser", Name="sysWechatUser", Component="/system/weChatUser/index",Icon="ele-ChatDotRound", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=160 }, + new SysMenu{ Id=1300200070201, Pid=1300200070101, Title="查询", Permission="sysWechatUser:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200070301, Pid=1300200070101, Title="编辑", Permission="sysWechatUser:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200070401, Pid=1300200070101, Title="增加", Permission="sysWechatUser:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200070501, Pid=1300200070101, Title="删除", Permission="sysWechatUser:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // AD域配置 + new SysMenu{ Id=1300200080101, Pid=1300200000101, Title="AD域配置", Path="/system/ldap", Name="sysLdap", Component="/system/ldap/index",Icon="ele-Place", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=170 }, + new SysMenu{ Id=1300200080201, Pid=1300200080101, Title="查询", Permission="sysLdap:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200080301, Pid=1300200080101, Title="详情", Permission="sysLdap:detail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300200080401, Pid=1300200080101, Title="编辑", Permission="sysLdap:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300200080501, Pid=1300200080101, Title="增加", Permission="sysLdap:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300200080601, Pid=1300200080101, Title="删除", Permission="sysLdap:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300200080701, Pid=1300200080101, Title="同步域账户", Permission="sysLdap:syncUser", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + new SysMenu{ Id=1300200080801, Pid=1300200080101, Title="同步域组织", Permission="sysLdap:syncOrg", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=160 }, + + #endregion 系统管理 + + #region 平台管理 + + new SysMenu{ Id=1300300000101, Pid=0, Title="平台管理", Path="/platform", Name="platform", Component="Layout", Icon="ele-Operation", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=11000 }, + + // 租户管理 + new SysMenu{ Id=1300300010101, Pid=1300300000101, Title="租户管理", Path="/platform/tenant", Name="sysTenant", Component="/system/tenant/index", Icon="ele-School", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010201, Pid=1300300010101, Title="查询", Permission="sysTenant:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010301, Pid=1300300010101, Title="编辑", Permission="sysTenant:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010401, Pid=1300300010101, Title="增加", Permission="sysTenant:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010501, Pid=1300300010101, Title="删除", Permission="sysTenant:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010601, Pid=1300300010101, Title="授权菜单", Permission="sysTenant:grantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010701, Pid=1300300010101, Title="重置密码", Permission="sysTenant:resetPwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010801, Pid=1300300010101, Title="生成库", Permission="sysTenant:createDb", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010901, Pid=1300300010101, Title="设置状态", Permission="sysTenant:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011001, Pid=1300300010101, Title="同步授权", Permission="sysTenant:syncGrantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011101, Pid=1300300010101, Title="切换租户", Permission="sysTenant:changeTenant", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011201, Pid=1300300010101, Title="进入租管端", Permission="sysTenant:goTenant", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 注册方案 + new SysMenu{ Id=1300300020101, Pid=1300300000101, Title="注册方案", Path="/platform/regWay", Name="sysUserRegWay", Component="/system/userRegWay/index", Icon="ele-Pointer", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=105 }, + new SysMenu{ Id=1300300020201, Pid=1300300020101, Title="查询", Permission="sysUserRegWay:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300020301, Pid=1300300020101, Title="编辑", Permission="sysUserRegWay:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300020401, Pid=1300300020101, Title="增加", Permission="sysUserRegWay:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300020501, Pid=1300300020101, Title="删除", Permission="sysUserRegWay:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 菜单管理 + new SysMenu{ Id=1300300030101, Pid=1300300000101, Title="菜单管理", Path="/platform/menu", Name="sysMenu", Component="/system/menu/index", Icon="ele-Menu", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300300030201, Pid=1300300030101, Title="查询", Permission="sysMenu:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030301, Pid=1300300030101, Title="编辑", Permission="sysMenu:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030401, Pid=1300300030101, Title="增加", Permission="sysMenu:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030501, Pid=1300300030101, Title="删除", Permission="sysMenu:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 平台参数 + new SysMenu{ Id=1300300040101, Pid=1300300000101, Title="平台参数", Path="/platform/config", Name="sysConfig", Component="/system/config/index", Icon="ele-DocumentCopy", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300300040201, Pid=1300300040101, Title="查询", Permission="sysConfig:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040301, Pid=1300300040101, Title="编辑", Permission="sysConfig:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040401, Pid=1300300040101, Title="增加", Permission="sysConfig:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040501, Pid=1300300040101, Title="删除", Permission="sysConfig:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 字典管理 + new SysMenu{ Id=1300300050101, Pid=1300300000101, Title="字典管理", Path="/platform/dict", Name="sysDict", Component="/system/dict/index", Icon="ele-Collection", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300300050201, Pid=1300300050101, Title="查询", Permission="sysDictType:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050301, Pid=1300300050101, Title="编辑", Permission="sysDictType:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050401, Pid=1300300050101, Title="增加", Permission="sysDictType:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050501, Pid=1300300050101, Title="删除", Permission="sysDictType:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050601, Pid=1300300050101, Title="增加字典值", Permission="sysDictData:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050701, Pid=1300300050101, Title="删除字典值", Permission="sysDictData:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050801, Pid=1300300050101, Title="编辑字典值", Permission="sysDictData:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050901, Pid=1300300050101, Title="字典迁移", Permission="sysDictType:move", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 模板管理 + new SysMenu{ Id=1300300051101, Pid=1300300000101, Title="模板管理", Path="/platform/template", Name="sysTemplate", Component="/system/template/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=135 }, + new SysMenu{ Id=1300300051201, Pid=1300300051101, Title="查询", Permission="sysTemplate:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051301, Pid=1300300051101, Title="编辑", Permission="sysTemplate:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051401, Pid=1300300051101, Title="增加", Permission="sysTemplate:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051501, Pid=1300300051101, Title="删除", Permission="sysTemplate:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051601, Pid=1300300051101, Title="预览", Permission="sysTemplate:preview", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 任务调度 + new SysMenu{ Id=1300300060101, Pid=1300300000101, Title="任务调度", Path="/platform/job", Name="sysJob", Component="/system/job/index", Icon="ele-AlarmClock", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300300060201, Pid=1300300060101, Title="查询", Permission="sysJob:pageJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060301, Pid=1300300060101, Title="编辑", Permission="sysJob:updateJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060401, Pid=1300300060101, Title="增加", Permission="sysJob:addJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060501, Pid=1300300060101, Title="删除", Permission="sysJob:deleteJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统参数 + new SysMenu{ Id=1300200090101, Pid=1300200000101, Title="系统参数", Path="/system/tenantConfig", Name="sysTenantConfig", Component="/system/tenantConfig/index", Icon="ele-DocumentCopy", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=180 }, + new SysMenu{ Id=1300200090201, Pid=1300200090101, Title="查询", Permission="sysTenantConfig:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090301, Pid=1300200090101, Title="编辑", Permission="sysTenantConfig:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090401, Pid=1300200090101, Title="增加", Permission="sysTenantConfig:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090501, Pid=1300200090101, Title="删除", Permission="sysTenantConfig:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 语言管理 + new SysMenu{ Id=1300200100101, Pid=1300200000101, Title="语言管理", Path="/system/lang", Name="sysLang", Component="/system/lang/index", Icon="iconfont icon-diqiu", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2025-06-28 00:00:00"), OrderNo=190 }, + new SysMenu{ Id=1300200100201, Pid=1300200100101, Title="查询", Permission="sysLang:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200100301, Pid=1300200100101, Title="编辑", Permission="sysLang:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200100401, Pid=1300200100101, Title="增加", Permission="sysLang:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200100501, Pid=1300200100101, Title="删除", Permission="sysLang:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 翻译管理 + new SysMenu{ Id=1300200200101, Pid=1300200000101, Title="翻译管理", Path="/system/langText", Name="sysLangText", Component="/system/langText/index", Icon="iconfont icon-zhongyingwen", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2025-06-28 00:00:00"), OrderNo=200 }, + new SysMenu{ Id=1300200200201, Pid=1300200200101, Title="查询", Permission="sysLangText:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200200301, Pid=1300200200101, Title="编辑", Permission="sysLangText:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200200401, Pid=1300200200101, Title="增加", Permission="sysLangText:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200200501, Pid=1300200200101, Title="删除", Permission="sysLangText:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统监控 + new SysMenu{ Id=1300300070101, Pid=1300300000101, Title="系统监控", Path="/platform/server", Name="sysServer", Component="/system/server/index", Icon="ele-Monitor", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + + // 缓存管理 + new SysMenu{ Id=1300300080101, Pid=1300300000101, Title="缓存管理", Path="/platform/cache", Name="sysCache", Component="/system/cache/index", Icon="ele-Loading", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=160 }, + new SysMenu{ Id=1300300080201, Pid=1300300080101, Title="查询", Permission="sysCache:keyList", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300080301, Pid=1300300080101, Title="删除", Permission="sysCache:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300080401, Pid=1300300080101, Title="清空", Permission="sysCache:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 行政区域 + new SysMenu{ Id=1300300090101, Pid=1300300000101, Title="行政区域", Path="/platform/region", Name="sysRegion", Component="/system/region/index", Icon="ele-LocationInformation", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=170 }, + new SysMenu{ Id=1300300090201, Pid=1300300090101, Title="查询", Permission="sysRegion:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090301, Pid=1300300090101, Title="编辑", Permission="sysRegion:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090401, Pid=1300300090101, Title="增加", Permission="sysRegion:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090501, Pid=1300300090101, Title="删除", Permission="sysRegion:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090601, Pid=1300300090101, Title="同步", Permission="sysRegion:sync", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 文件管理 + new SysMenu{ Id=1300300100101, Pid=1300300000101, Title="文件管理", Path="/platform/file", Name="sysFile", Component="/system/file/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=180 }, + new SysMenu{ Id=1300300100201, Pid=1300300100101, Title="查询", Permission="sysFile:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100301, Pid=1300300100101, Title="上传", Permission="sysFile:uploadFile", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100401, Pid=1300300100101, Title="下载", Permission="sysFile:downloadFile", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100501, Pid=1300300100101, Title="删除", Permission="sysFile:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100601, Pid=1300300100101, Title="编辑", Permission="sysFile:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2023-10-27 00:00:00"), OrderNo=100 }, + + // 打印模板 + new SysMenu{ Id=1300300110101, Pid=1300300000101, Title="打印模板", Path="/platform/print", Name="sysPrint", Component="/system/print/index", Icon="ele-Printer", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=190 }, + new SysMenu{ Id=1300300110201, Pid=1300300110101, Title="查询", Permission="sysPrint:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110301, Pid=1300300110101, Title="编辑", Permission="sysPrint:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110401, Pid=1300300110101, Title="增加", Permission="sysPrint:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110501, Pid=1300300110101, Title="删除", Permission="sysPrint:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 动态插件 + new SysMenu{ Id=1300300120101, Pid=1300300000101, Title="动态插件", Path="/platform/plugin", Name="sysPlugin", Component="/system/plugin/index", Icon="ele-Connection", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=200 }, + new SysMenu{ Id=1300300120201, Pid=1300300120101, Title="查询", Permission="sysPlugin:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300120301, Pid=1300300120101, Title="编辑", Permission="sysPlugin:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300120401, Pid=1300300120101, Title="增加", Permission="sysPlugin:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300120501, Pid=1300300120101, Title="删除", Permission="sysPlugin:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 开放接口 + new SysMenu{ Id=1300300130101, Pid=1300300000101, Title="开放接口", Path="/platform/openAccess", Name="sysOpenAccess", Component="/system/openAccess/index", Icon="ele-Link", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=210 }, + new SysMenu{ Id=1300300130201, Pid=1300300130101, Title="查询", Permission="sysOpenAccess:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300130301, Pid=1300300130101, Title="编辑", Permission="sysOpenAccess:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300130401, Pid=1300300130101, Title="增加", Permission="sysOpenAccess:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300130501, Pid=1300300130101, Title="删除", Permission="sysOpenAccess:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统配置 + new SysMenu{ Id=1300300140101, Pid=1300300000101, Title="系统配置", Path="/platform/infoSetting", Name="sysInfoSetting", Component="/system/infoSetting/index", Icon="ele-Setting", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=220 }, + + // 微信支付 + new SysMenu{ Id=1300300150101, Pid=1300300000101, Title="微信支付", Path="/platform/wechatpay", Name="sysWechatPay", Component="/system/weChatPay/index", Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=230 }, + new SysMenu{ Id=1300300150201, Pid=1300300150101, Title="微信支付下单Native", Permission="sysWechatPay:payTransactionNative", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300150301, Pid=1300300150101, Title="查询退款信息", Permission="sysWechatPay:listRefund", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300150401, Pid=1300300150101, Title="获取支付订单详情(本地库)", Permission="sysWechatPay:payInfo", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300150501, Pid=1300300150101, Title="获取支付订单详情(微信接口)", Permission="sysWechatPay:payInfoFromWechat", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300150601, Pid=1300300150101, Title="退款申请", Permission="sysWechatPay:refundDomestic", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统更新 + new SysMenu{ Id=1300300160101, Pid=1300300000101, Title="系统更新", Path="/platform/update", Name="sysUpdate", Component="/system/update/index", Icon="ele-Upload", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=240 }, + new SysMenu{ Id=1300300160201, Pid=1300300160101, Title="更新", Permission="sysUpdate:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160301, Pid=1300300160101, Title="还原", Permission="sysUpdate:restore", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160401, Pid=1300300160101, Title="备份列表", Permission="sysUpdate:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160501, Pid=1300300160101, Title="日志列表", Permission="sysUpdate:logs", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160601, Pid=1300300160101, Title="清除日志", Permission="sysUpdate:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160701, Pid=1300300160101, Title="获取密钥", Permission="sysUpdate:webHookKey", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + #endregion 平台管理 + + #region 日志管理 + + new SysMenu{ Id=1300400000101, Pid=0, Title="日志管理", Path="/log", Name="log", Component="Layout", Icon="ele-DocumentCopy", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=12000 }, + + // 访问日志 + new SysMenu{ Id=1300400010101, Pid=1300400000101, Title="访问日志", Path="/log/vislog", Name="sysVisLog", Component="/system/log/vislog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400010201, Pid=1300400010101, Title="查询", Permission="sysVislog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400010301, Pid=1300400010101, Title="清空", Permission="sysVislog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 操作日志 + new SysMenu{ Id=1300400020101, Pid=1300400000101, Title="操作日志", Path="/log/oplog", Name="sysOpLog", Component="/system/log/oplog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300400020201, Pid=1300400020101, Title="查询", Permission="sysOplog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400020301, Pid=1300400020101, Title="清空", Permission="sysOplog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400020401, Pid=1300400020101, Title="导出", Permission="sysOplog:export", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 异常日志 + new SysMenu{ Id=1300400030101, Pid=1300400000101, Title="异常日志", Path="/log/exlog", Name="sysExLog", Component="/system/log/exlog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300400030201, Pid=1300400030101, Title="查询", Permission="sysExlog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400030301, Pid=1300400030101, Title="清空", Permission="sysExlog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400030401, Pid=1300400030101, Title="导出", Permission="sysExlog:export", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 差异日志 + new SysMenu{ Id=1300400040101, Pid=1300400000101, Title="差异日志", Path="/log/difflog", Name="sysDiffLog", Component="/system/log/difflog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300400040201, Pid=1300400040101, Title="查询", Permission="sysDifflog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400040301, Pid=1300400040101, Title="清空", Permission="sysDifflog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + #endregion 日志管理 + + #region 开发工具 + + // 开发工具 + new SysMenu{ Id=1300500000101, Pid=0, Title="开发工具", Path="/develop", Name="develop", Component="Layout", Icon="ele-Cpu", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=13000 }, + new SysMenu{ Id=1300500010101, Pid=1300500000101, Title="库表管理", Path="/develop/database", Name="sysDatabase", Component="/system/database/index",Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300500020101, Pid=1300500000101, Title="代码生成", Path="/develop/codeGen", Name="sysCodeGen", Component="/system/codeGen/index", Icon="ele-Crop", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300500030101, Pid=1300500000101, Title="表单设计", Path="/develop/formDes", Name="sysFormDes", Component="/system/formDes/index", Icon="ele-Edit", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300500040101, Pid=1300500000101, Title="接口压测", Path="/develop/stressTest", Name="SysStressTest", Component="/system/stressTest/index", Icon="ele-DataLine", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300500050101, Pid=1300500000101, Title="系统接口", Path="/develop/api", Name="sysApi", Component="layout/routerView/iframe", IsIframe=true, OutLink="http://localhost:5005", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + + #endregion 开发工具 + + #region 帮助文档 + + // 帮助文档 + new SysMenu{ Id=1300600000101, Pid=0, Title="帮助文档", Path="/doc", Name="doc", Component="Layout", Icon="ele-Notebook", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=14000 }, + new SysMenu{ Id=1300600010101, Pid=1300600000101, Title="框架教程", Path="/doc/admin", Name="sysAdmin", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="http://101.43.53.74:5050/", Icon="ele-Sunny", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300600020101, Pid=1300600000101, Title="后台教程", Path="/doc/furion", Name="sysFurion", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="https://furion.baiqian.ltd/", Icon="ele-Promotion", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300600030101, Pid=1300600000101, Title="前端教程", Path="/doc/element", Name="sysElement", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="https://element-plus.gitee.io/zh-CN/", Icon="ele-Position", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300600040101, Pid=1300600000101, Title="SqlSugar", Path="/doc/SqlSugar", Name="sysSqlSugar", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="https://www.donet5.com/Home/Doc", Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + + #endregion 帮助文档 + + // 关于项目 + new SysMenu{ Id=1300700000101, Pid=0, Title="关于项目", Path="/about", Name="about", Component="/about/index", Icon="ele-InfoFilled", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2023-03-12 00:00:00"), OrderNo=15000 }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysOrgSeedData.cs b/Admin.NET.Core/SeedData/SysOrgSeedData.cs new file mode 100644 index 0000000..35ba85d --- /dev/null +++ b/Admin.NET.Core/SeedData/SysOrgSeedData.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统机构表种子数据 +/// +[IgnoreUpdateSeed] +public class SysOrgSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var admin = new SysUserSeedData().HasData().First(u => u.Account == "Admin.NET"); + return new[] + { + new SysOrg{ Id=SqlSugarConst.DefaultTenantId, Pid=0, Name="系统默认", Code="1001", Type="101", Level=1, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="系统默认", TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 1, Pid=SqlSugarConst.DefaultTenantId, Name="市场部", Code="100101", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="市场部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 2, Pid=SqlSugarConst.DefaultTenantId, Name="开发部", Code="100102", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="开发部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 3, Pid=SqlSugarConst.DefaultTenantId, Name="售后部", Code="100103", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="售后部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 4, Pid=SqlSugarConst.DefaultTenantId, Name="其他", Code="10010301", Level=3, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="其他", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysPosSeedData.cs b/Admin.NET.Core/SeedData/SysPosSeedData.cs new file mode 100644 index 0000000..64b62e6 --- /dev/null +++ b/Admin.NET.Core/SeedData/SysPosSeedData.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统职位表种子数据 +/// +public class SysPosSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysPos{ Id=1300000000101, Name="党委书记", Code="dwsj", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="党委书记", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000102, Name="董事长", Code="dsz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="董事长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000103, Name="副董事长", Code="fdsz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副董事长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000104, Name="总经理", Code="zjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="总经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000105, Name="副总经理", Code="fzjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副总经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000106, Name="部门经理", Code="bmjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="部门经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000107, Name="部门副经理", Code="bmfjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="部门副经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000108, Name="主任", Code="zr", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="主任", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000109, Name="副主任", Code="fzr", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副主任", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000110, Name="局长", Code="jz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="局长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000111, Name="副局长", Code="fjz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副局长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000112, Name="科长", Code="kz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="科长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000113, Name="副科长", Code="fkz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副科长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000114, Name="财务", Code="cw", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="财务", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000115, Name="职员", Code="zy", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="职员", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000116, Name="其他", Code="qt", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="其他", TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysRoleMenuSeedData.cs b/Admin.NET.Core/SeedData/SysRoleMenuSeedData.cs new file mode 100644 index 0000000..2c4bdb1 --- /dev/null +++ b/Admin.NET.Core/SeedData/SysRoleMenuSeedData.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色菜单表种子数据 +/// +public class SysRoleMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var roleMenuList = new List(); + + var roleList = new SysRoleSeedData().HasData().ToList(); + var menuList = new SysMenuSeedData().HasData().ToList(); + var defaultMenuList = new SysTenantMenuSeedData().HasData().ToList(); + + // 第一个角色拥有全部默认租户菜单 + roleMenuList.AddRange(defaultMenuList.Select(u => new SysRoleMenu { Id = u.MenuId + (roleList[0].Id % 1300000000000), RoleId = roleList[0].Id, MenuId = u.MenuId })); + + // 其他角色权限:工作台、系统管理、个人中心、帮助文档、关于项目 + var otherRoleMenuList = menuList.ToChildList(u => u.Id, u => u.Pid, u => new[] { "工作台", "帮助文档", "关于项目", "个人中心" }.Contains(u.Title)).ToList(); + otherRoleMenuList.Add(menuList.First(u => u.Type == MenuTypeEnum.Dir && u.Title == "系统管理")); + foreach (var role in roleList.Skip(1)) roleMenuList.AddRange(otherRoleMenuList.Select(u => new SysRoleMenu { Id = u.Id + (role.Id % 1300000000000), RoleId = role.Id, MenuId = u.Id })); + + return roleMenuList; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysRoleSeedData.cs b/Admin.NET.Core/SeedData/SysRoleSeedData.cs new file mode 100644 index 0000000..5dc8c8f --- /dev/null +++ b/Admin.NET.Core/SeedData/SysRoleSeedData.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色表种子数据 +/// +public class SysRoleSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysRole{ Id=1300000000101, Name="系统管理员", DataScope=DataScopeEnum.All, Code="sys_admin", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="系统管理员", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000102, Name="本部门及以下数据", DataScope=DataScopeEnum.DeptChild, Code="sys_deptChild", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="本部门及以下数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000103, Name="本部门数据", DataScope=DataScopeEnum.Dept, Code="sys_dept", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="本部门数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000104, Name="仅本人数据", DataScope=DataScopeEnum.Self, Code="sys_self", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="仅本人数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000105, Name="自定义数据", DataScope=DataScopeEnum.Define, Code="sys_define", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="自定义数据", TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysTenantMenuSeedData.cs b/Admin.NET.Core/SeedData/SysTenantMenuSeedData.cs new file mode 100644 index 0000000..50fad12 --- /dev/null +++ b/Admin.NET.Core/SeedData/SysTenantMenuSeedData.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户菜单表种子数据 +/// +[IgnoreUpdateSeed] +public class SysTenantMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return App.GetService().GetTenantDefaultMenuList() + .Select(u => new SysTenantMenu + { + Id = CommonUtil.GetFixedHashCode("" + SqlSugarConst.DefaultTenantId + u.MenuId, 1300000000000), + TenantId = SqlSugarConst.DefaultTenantId, + MenuId = u.MenuId + }); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysTenantSeedData.cs b/Admin.NET.Core/SeedData/SysTenantSeedData.cs new file mode 100644 index 0000000..ccef80d --- /dev/null +++ b/Admin.NET.Core/SeedData/SysTenantSeedData.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户表种子数据 +/// +public class SysTenantSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var defaultDbConfig = App.GetOptions().ConnectionConfigs[0]; + var userList = new SysUserSeedData().HasData().ToList(); + var admin = userList.First(u => u.Account == "Admin.NET"); + return new[] + { + new SysTenant{ Id=SqlSugarConst.DefaultTenantId, OrgId=SqlSugarConst.DefaultTenantId, UserId=admin.Id, Host="gitee.com", TenantType=TenantTypeEnum.Id, DbType=defaultDbConfig.DbType, Connection=defaultDbConfig.ConnectionString, ConfigId=SqlSugarConst.MainConfigId, Logo="/upload/logo.png", Title="Admin.NET", ViceTitle="Admin.NET", ViceDesc="站在巨人肩膀上的 .NET 通用权限开发框架", Watermark="Admin.NET", Copyright="Copyright \u00a9 2021-present Admin.NET All rights reserved.", Icp="省ICP备12345678号", IcpUrl="https://beian.miit.gov.cn", Remark="系统默认", CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysUserExtOrgSeedData.cs b/Admin.NET.Core/SeedData/SysUserExtOrgSeedData.cs new file mode 100644 index 0000000..a54ee7d --- /dev/null +++ b/Admin.NET.Core/SeedData/SysUserExtOrgSeedData.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户扩展机构表种子数据 +/// +public class SysUserExtOrgSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var userList = new SysUserSeedData().HasData().ToList(); + var orgList = new SysOrgSeedData().HasData().ToList(); + var posList = new SysPosSeedData().HasData().ToList(); + var admin = userList.First(u => u.Account == "Admin.NET"); + var user3 = userList.First(u => u.Account == "TestUser3"); + var org1 = orgList.First(u => u.Name == "系统默认"); + var org2 = orgList.First(u => u.Name == "开发部"); + var pos1 = posList.First(u => u.Name == "部门经理"); + var pos2 = posList.First(u => u.Name == "主任"); + return new[] + { + new SysUserExtOrg{ Id=1300000000101, UserId=admin.Id, OrgId=org1.Id, PosId=pos1.Id }, + new SysUserExtOrg{ Id=1300000000102, UserId=user3.Id, OrgId=org2.Id, PosId=pos2.Id } + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysUserRoleSeedData.cs b/Admin.NET.Core/SeedData/SysUserRoleSeedData.cs new file mode 100644 index 0000000..29f89ec --- /dev/null +++ b/Admin.NET.Core/SeedData/SysUserRoleSeedData.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户角色表种子数据 +/// +public class SysUserRoleSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var userList = new SysUserSeedData().HasData().ToList(); + var roleList = new SysRoleSeedData().HasData().ToList(); + return new[] + { + new SysUserRole{ Id=1300000000101, UserId=userList.First(u => u.Account == "TestUser1").Id, RoleId=roleList.First(u => u.Code == "sys_deptChild").Id }, + new SysUserRole{ Id=1300000000102, UserId=userList.First(u => u.Account == "TestUser2").Id, RoleId=roleList.First(u => u.Code == "sys_dept").Id }, + new SysUserRole{ Id=1300000000103, UserId=userList.First(u => u.Account == "TestUser3").Id, RoleId=roleList.First(u => u.Code == "sys_self").Id }, + new SysUserRole{ Id=1300000000104, UserId=userList.First(u => u.Account == "TestUser4").Id, RoleId=roleList.First(u => u.Code == "sys_define").Id }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SeedData/SysUserSeedData.cs b/Admin.NET.Core/SeedData/SysUserSeedData.cs new file mode 100644 index 0000000..7c1f8a7 --- /dev/null +++ b/Admin.NET.Core/SeedData/SysUserSeedData.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户表种子数据 +/// +[IgnoreUpdateSeed] +public class SysUserSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var encryptPassword = CryptogramUtil.Encrypt(new SysConfigSeedData().HasData().First(u => u.Code == ConfigConst.SysPassword).Value); + var posList = new SysPosSeedData().HasData().ToList(); + return new[] + { + new SysUser{ Id=1300000000101, Account="superAdmin.NET", Password=encryptPassword, NickName="超级管理员", RealName="超级管理员", Phone="18012345678", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Male, AccountType=AccountTypeEnum.SuperAdmin, Remark="超级管理员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000111, Account="Admin.NET", Password=encryptPassword, NickName="系统管理员", RealName="系统管理员", Phone="18012345677", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Male, AccountType=AccountTypeEnum.SysAdmin, Remark="系统管理员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId, PosId=posList[0].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000112, Account="TestUser1", Password=encryptPassword, NickName="部门主管", RealName="部门主管", Phone="18012345676", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="部门主管", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 1, PosId=posList[1].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000113, Account="TestUser2", Password=encryptPassword, NickName="部门职员", RealName="部门职员", Phone="18012345675", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="部门职员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 2, PosId=posList[2].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000114, Account="TestUser3", Password=encryptPassword, NickName="普通用户", RealName="普通用户", Phone="18012345674", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="普通用户", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 3, PosId=posList[3].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000115, Account="TestUser4", Password=encryptPassword, NickName="其他", RealName="其他", Phone="18012345673", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.Member, Remark="会员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 4, PosId=posList[4].Id, TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/APIJSON/APIJSONService.cs b/Admin.NET.Core/Service/APIJSON/APIJSONService.cs new file mode 100644 index 0000000..17bd21d --- /dev/null +++ b/Admin.NET.Core/Service/APIJSON/APIJSONService.cs @@ -0,0 +1,214 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// APIJSON服务 🧩 +/// +[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); + } + + /// + /// 统一查询入口 🔖 + /// + /// + /// 参数:{"[]":{"SYSLOGOP":{}}} + /// + [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); + } + + /// + /// 查询 🔖 + /// + /// + /// + /// + [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 { "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); + } + + /// + /// 新增 🔖 + /// + /// 表对象或数组,若没有传Id则后端生成Id + /// + [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(); + foreach (var record in table.Value) + { + var cols = record.ToObject(); + 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(); + var id = _selectTable.InsertSingle(talbeName, cols, role); + result = JToken.FromObject(new { id }); + } + ht.Add(talbeName, result); + } + return ht; + } + + /// + /// 更新(只支持Id作为条件) 🔖 + /// + /// 支持多表、多Id批量更新 + /// + [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; + } + + /// + /// 删除(支持非Id条件、支持批量) 🔖 + /// + /// + /// + [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(); + 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().AS(talbeName).Where(whereSql, parameters).ExecuteCommand(); // 无实体删除 + value.Add("count", count); // 命中数量 + ht.Add(talbeName, value); + } + return ht; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/APIJSON/FuncList.cs b/Admin.NET.Core/Service/APIJSON/FuncList.cs new file mode 100644 index 0000000..139e02f --- /dev/null +++ b/Admin.NET.Core/Service/APIJSON/FuncList.cs @@ -0,0 +1,116 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 自定义方法 +/// +public class FuncList +{ + /// + /// 字符串相加 + /// + /// + /// + /// + public string Merge(object a, object b) + { + return a.ToString() + b.ToString(); + } + + /// + /// 对象合并 + /// + /// + /// + /// + public object MergeObj(object a, object b) + { + return new { a, b }; + } + + /// + /// 是否包含 + /// + /// + /// + /// + public bool IsContain(object a, object b) + { + return a.ToString().Split(',').Contains(b); + } + + /// + /// 根据jtoken的实际类型来转换SugarParameter,避免全转成字符串 + /// + /// + /// + 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 + }; + } + + /// + /// 根据jArray的实际类型来转换SugarParameter,避免全转成字符串 + /// + /// + /// + 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(), + JTokenType.Float => jToken.ToObject(), + JTokenType.Boolean => jToken.ToObject(), + JTokenType.Date => jToken.ToObject(), + JTokenType.Bytes => jToken.ToObject(), + JTokenType.Guid => jToken.ToObject(), + JTokenType.TimeSpan => jToken.ToObject(), + _ => jToken.ToArray() + }; + } + + return (JArray)jToken; + } + + /// + /// 获取字符串里的值的真正类型 + /// + /// + /// + 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"; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/APIJSON/IdentityService.cs b/Admin.NET.Core/Service/APIJSON/IdentityService.cs new file mode 100644 index 0000000..f62bde5 --- /dev/null +++ b/Admin.NET.Core/Service/APIJSON/IdentityService.cs @@ -0,0 +1,96 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Security.Claims; + +namespace Admin.NET.Core.Service; + +/// +/// 权限验证 +/// +public class IdentityService : ITransient +{ + private readonly IHttpContextAccessor _context; + private readonly List _roles; + + public IdentityService(IHttpContextAccessor context, IOptions roles) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _roles = roles.Value.Roles; + } + + /// + /// 获取当前用户Id + /// + /// + public string GetUserIdentity() + { + return _context.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); + } + + /// + /// 获取当前用户权限名称 + /// + /// + public string GetUserRoleName() + { + return _context.HttpContext.User.FindFirstValue(ClaimTypes.Role); + } + + /// + /// 获取当前用户权限 + /// + /// + public APIJSON_Role GetRole() + { + var role = string.IsNullOrEmpty(GetUserRoleName()) + ? _roles.FirstOrDefault() + : _roles.FirstOrDefault(it => it.RoleName.Equals(GetUserRoleName(), StringComparison.CurrentCultureIgnoreCase)); + return role; + } + + /// + /// 获取当前表的可查询字段 + /// + /// + /// + 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); + } + + /// + /// 当前列是否在角色里面 + /// + /// + /// + /// + 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); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/APIJSON/SelectTable.cs b/Admin.NET.Core/Service/APIJSON/SelectTable.cs new file mode 100644 index 0000000..8b83cb9 --- /dev/null +++ b/Admin.NET.Core/Service/APIJSON/SelectTable.cs @@ -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; + +/// +/// +/// +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; + } + + /// + /// 判断表名是否正确,若不正确则抛异常 + /// + /// + /// + public virtual bool IsTable(string table) + { + return _db.DbMaintenance.GetTableInfoList().Any(it => it.Name.Equals(table, StringComparison.CurrentCultureIgnoreCase)) + ? true + : throw new Exception($"表名【{table}】不正确!"); + } + + /// + /// 判断表的列名是否正确,如果不正确则抛异常,更早地暴露给调用方 + /// + /// + /// + /// + 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}】!请检查输入参数"); + } + + /// + /// 查询列表数据 + /// + /// + /// + /// + /// + /// + /// + /// + public virtual Tuple 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(null, tb.MergeTable().Count()); + } + else + { + if (page > 0) // 分页 + { + int total = 0; + if (query == 0) + return new Tuple(tb.ToPageList(page, count), total); // 0-对象 + else + return new Tuple(tb.ToPageList(page, count, ref total), total); // 2-以上全部 + } + else // 列表 + { + IList l = tb.ToList(); + return query == 0 ? new Tuple(l, 0) : new Tuple(l, l.Count); + } + } + } + + /// + /// 解析并查询 + /// + /// + /// + public virtual JObject Query(string queryJson) + { + var queryJobj = JObject.Parse(queryJson); + return Query(queryJobj); + } + + /// + /// 单表查询 + /// + /// + /// 返回数据的节点名称 默认为 infos + /// + 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; + } + + /// + /// 获取查询语句 + /// + /// + /// + public virtual string ToSql(JObject queryObj) + { + foreach (var item in queryObj) + { + if (item.Key.Trim().EndsWith("[]")) + return ToSql(item); + } + return string.Empty; + } + + /// + /// 解析并查询 + /// + /// + /// + 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; + } + + /// + /// 查询第一条数据 + /// + /// + /// + /// + /// + /// + 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)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(); + var paramss = new List(); + 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 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 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 item) + { + var key = item.Key.TrimEnd("[]"); + return QuerySingleList(resultObj, item, key); + } + + /// + /// 多列表查询 + /// + /// + /// + /// + private int QueryMoreList(JObject resultObj, KeyValuePair 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 tables = new List(), where = new List(); + 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 item) + { + var jb = JObject.Parse(item.Value.ToString()); + + var dataJObj = new JObject(); + foreach (var f in jb) + { + var types = new List(); + var param = new List(); + 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); + } + + /// + /// 构造查询过程 + /// + /// + /// + /// + /// + /// + private ISugarQueryable 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 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(',')); + } + + /// + /// 构造查询条件 where + /// + /// + /// + /// + /// + private void ProcessWhere(string subtable, JObject values, ISugarQueryable tb, JObject dd) + { + var conModels = new List(); + 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 tb) + { + if (!values["@having"].IsNullOrEmpty()) + { + var hw = new List(); + var havingItems = new List(); + 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 tb) + { + if (!values["@group"].IsNullOrEmpty()) + { + var groupList = new List(); // 多库兼容写法 + 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 tb) + { + if (!values["@order"].IsNullOrEmpty()) + { + var orderList = new List(); // 多库兼容写法 + 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); + } + } + + /// + /// 表内参数"@count"(int):查询前几行,不能同时使用count和@count函数 + /// + /// + /// + private static void ProcessLimit(JObject values, ISugarQueryable tb) + { + if (!values["@count"].IsNullOrEmpty()) + { + int c = values["@count"].ToObject(); + tb.Take(c); + } + } + + // 条件查询 "key{}":"条件0,条件1...",条件为任意SQL比较表达式字符串,非Number类型必须用''包含条件的值,如'a' + // &, |, ! 逻辑运算符,对应数据库 SQL 中的 AND, OR, NOT。 + // 横或纵与:同一字段的值内条件默认 | 或连接,不同字段的条件默认 & 与连接。 + // ① & 可用于"key&{}":"条件"等 + // ② | 可用于"key|{}":"条件", "key|{}":[] 等,一般可省略 + // ③ ! 可单独使用,如"key!":Object,也可像&,|一样配合其他功能符使用 + private void ConditionQuery(string subtable, List conModels, KeyValuePair va) + { + var vakey = va.Key.Trim(); + var field = vakey.TrimEnd("{}".ToCharArray()); + var columnName = field.TrimEnd(new char[] { '&', '|' }); + IsCol(subtable, columnName); + var ddt = new List>(); + 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(field.EndsWith("!") ? WhereType.Or : WhereType.And, model)); + } + conModels.Add(new ConditionalCollections() { ConditionalList = ddt }); + } + + /// + /// "key%":"start,end" => "key%":["start,end"],其中 start 和 end 都只能为 Boolean, Number, String 中的一种,如 "2017-01-01,2019-01-01" ,["1,90000", "82001,100000"] ,可用于连续范围内的筛选 + /// 目前不支持数组形式 + /// + /// + /// + /// + /// + private static void ConditionBetween(string subtable, List conModels, KeyValuePair va, ISugarQueryable tb) + { + var vakey = va.Key.Trim(); + var field = vakey.TrimEnd("%".ToCharArray()); + var inValues = new List(); + 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); + } + } + } + + /// + /// 等于、不等于、in 、not in + /// + /// + /// + /// + private void ConditionEqual(string subtable, List conModels, KeyValuePair 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().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 conModels, KeyValuePair 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; + } + + /// + /// 单条插入 + /// + /// + /// + /// + /// (各种类型的)id + public object InsertSingle(string tableName, JObject cols, APIJSON_Role role = null) + { + role ??= _identitySvc.GetRole(); + var dt = new Dictionary(); + + 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; + } + + /// + /// 为每天记录创建udpate sql + /// + /// + /// + /// + /// + 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(); + 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; + } + + /// + /// 更新单表,支持同表多条记录 + /// + /// + /// + /// + /// + 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()) + { + count += UpdateSingleRecord(tableName, record, role); + } + } + else + { + count = UpdateSingleRecord(tableName, records.ToObject(), role); + } + return count; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/APIJSON/TableMapper.cs b/Admin.NET.Core/Service/APIJSON/TableMapper.cs new file mode 100644 index 0000000..1cbb5ff --- /dev/null +++ b/Admin.NET.Core/Service/APIJSON/TableMapper.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 表名映射 +/// +public class TableMapper : ITransient +{ + private readonly Dictionary _options = new(StringComparer.OrdinalIgnoreCase); + + public TableMapper(IOptions> options) + { + foreach (var item in options.Value) + { + _options.Add(item.Key, item.Value); + } + } + + /// + /// 获取表别名 + /// + /// + /// + public string GetTableName(string oldname) + { + return _options.ContainsKey(oldname) ? _options[oldname] : oldname; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/APIJSON/用例APIFOX.json b/Admin.NET.Core/Service/APIJSON/用例APIFOX.json new file mode 100644 index 0000000..5b48a4e --- /dev/null +++ b/Admin.NET.Core/Service/APIJSON/用例APIFOX.json @@ -0,0 +1,23120 @@ +{ + "apifoxProject": "1.0.0", + "$schema": { + "app": "apifox", + "type": "project", + "version": "1.2.0" + }, + "info": { + "name": "Admin.Net", + "description": "", + "mockRule": { + "rules": [ + ], + "enableSystemRule": true + } + }, + "apiCollection": [ + { + "name": "根目录", + "id": 29711526, + "auth": { + }, + "parentId": 0, + "serverId": "", + "description": "", + "identityPattern": { + "httpApi": { + "type": "methodAndPath", + "bodyType": "" + } + }, + "preProcessors": [ + { + "id": "inheritProcessors", + "type": "inheritProcessors", + "data": { + } + } + ], + "postProcessors": [ + { + "id": "inheritProcessors", + "type": "inheritProcessors", + "data": { + } + } + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + }, + "items": [ + { + "name": "aPIJSON", + "id": 29711668, + "auth": { + }, + "parentId": 0, + "serverId": "", + "description": "说明文档:https://github.com/Tencent/APIJSON/blob/master/Document.md#3.1", + "identityPattern": { + "httpApi": { + "type": "inherit", + "bodyType": "" + } + }, + "preProcessors": [ + { + "id": "inheritProcessors", + "type": "inheritProcessors", + "data": { + } + } + ], + "postProcessors": [ + { + "id": "inheritProcessors", + "type": "inheritProcessors", + "data": { + } + } + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + }, + "items": [ + { + "name": "统一查询入口", + "api": { + "id": "151219333", + "method": "post", + "path": "/api/aPIJSON/get", + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "auth": { + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "responses": [ + { + "id": "405647728", + "name": "成功", + "code": 200, + "contentType": "json", + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + }, + "result": { + "type": "object", + "properties": { + "[]": { + "type": "array", + "items": { + "type": "object", + "properties": { + }, + "x-apifox-orders": [ + ] + } + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "count": { + "type": "integer", + "description": "每页条数" + }, + "max": { + "type": "integer", + "description": "最大页数" + }, + "total": { + "type": "integer", + "description": "总条数" + } + }, + "x-apifox-orders": [ + "[]", + "page", + "count", + "max", + "total" + ] + }, + "extras": { + "type": "null" + }, + "time": { + "type": "string" + } + }, + "required": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ], + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + ], + "responseExamples": [ + ], + "requestBody": { + "type": "application/json", + "parameters": [ + ], + "jsonSchema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "x-apifox-orders": [ + ], + "properties": { + } + }, + "example": "{\r\n \"table1\": {\r\n \"@column\": \"id\",\r\n \"httpmethod\": \"Get\",\r\n }\r\n}" + }, + "description": "参数:{\"[]\":{\"SYS_LOG_OP\":{}}}", + "tags": [ + "aPIJSON" + ], + "status": "released", + "serverId": "", + "operationId": "api-aPIJSON-Post", + "sourceUrl": "", + "ordering": 0, + "cases": [ + { + "id": 143284761, + "type": "http", + "path": null, + "name": "单条查询", + "responseId": 405647728, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table1\": {\r\n \"@column\": \"id,createtime,Actionname,loglevel,httpmethod,RequestParam\",//显示列\r\n //\"Actionname\":\"SwaggerCheckUrl\",\r\n //\"loglevel\":2,\r\n //\"httpmethod!\":[\"POST\",\"GET\"],//! not in\r\n \"createtime{}\": \">=2024-3-1\", //逻辑运算\r\n //\"isdelete\":0, //bool 支持 1、0、true、false \r\n \"RequestParam!\": null //not null\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.decba6c4-b3e2-40af-a30e-8c8d09865bf1\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"decba6c4-b3e2-40af-a30e-8c8d09865bf1\",\"requestIndex\":0,\"httpRequestId\":\"7e107ae6-74d4-44c8-8582-e9bfefb5e611\"},\"type\":\"http\",\"response\":{\"id\":\"7b5fc148-4f73-4e56-b054-26bebe9db57a\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Wed, 13 Mar 2024 08:19:05 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86375\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-14T07:18:51.6721222Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,57,54,51,53,53,48,48,50,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,48,58,51,51,58,50,51,46,51,56,55,53,57,48,51,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,76,111,103,76,101,118,101,108,34,58,50,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,51,44,92,34,99,111,117,110,116,92,34,58,50,44,92,34,113,117,101,114,121,92,34,58,51,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,51,32,49,54,58,49,57,58,48,54,34,125]},\"cookie\":[],\"responseTime\":99,\"responseSize\":328,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.698800027370453,\"wait\":0.3564000129699707,\"dns\":0,\"tcp\":0,\"firstByte\":96.46359997987747,\"download\":1.736199975013733,\"process\":0.03470003604888916,\"total\":101.28970003128052}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":157,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"table1\\\": {\\n \\\"@column\\\": \\\"id,createtime,Actionname,loglevel,httpmethod,RequestParam\\\",\\n \\\"createtime{}\\\": \\\">=2024-3-1\\\",\\n \\\"RequestParam!\\\": null\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"157\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Wed, 13 Mar 2024 08:19:05 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86375\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-14T07:18:51.6721222Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710317946491,\"requestStart\":1710317946493,\"offset\":{\"request\":2.698800027370453,\"socket\":3.0552000403404236,\"response\":99.5188000202179,\"end\":101.25499999523163,\"lookup\":3.0552000403404236,\"connect\":3.0552000403404236,\"done\":101.28970003128052}}}]}},\"responseValidation\":{\"schema\":{\"valid\":true,\"message\":\"\",\"errors\":null},\"responseCode\":{\"valid\":true}},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143284761,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"单条查询\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143272865, + "type": "http", + "path": null, + "name": "列表查询", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"[]\": {\r\n \"table1\": {\r\n \"@column\": \"id,createtime,httpmethod,RequestUrl,actionname,RequestParam,Elapsed\",//需要显示的列名\r\n //\"httpmethod\": \"POST\",//条件查询\r\n //\"RequestUrl$\": \"%swagger%\", //$模糊查询\r\n //\"@order\": \"RequestParam desc,createtime,actionname desc\", //按最新时间排序:-/desc 均为倒序 \r\n //\"@count\": \"10\", //前n条 很少用到 \r\n //\"RequestParam\":null,//匹配null or '',\r\n \"createtime%\":\"2024-3-5,2024-3-14\",// between 日期过滤\r\n \"Elapsed%\":\"1,20\"\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.1c48ba22-4372-4ab1-a874-a79449144e5d\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"1c48ba22-4372-4ab1-a874-a79449144e5d\",\"requestIndex\":0,\"httpRequestId\":\"70f19533-b333-460b-beeb-2e5ecbc5298c\"},\"type\":\"http\",\"response\":{\"id\":\"0f169b35-6729-4512-956d-2081d9fdb91d\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 14 Mar 2024 02:00:47 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-15T02:00:05.6689995Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,91,93,34,58,91,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,51,48,49,56,48,54,55,57,50,51,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,53,32,49,54,58,50,48,58,52,53,46,50,57,56,49,57,49,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,50,54,50,54,48,51,48,55,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,48,57,58,52,55,58,52,53,46,51,49,51,48,53,50,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,51,50,57,56,51,50,55,55,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,48,57,58,53,50,58,48,55,46,57,50,57,50,54,52,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,51,54,51,52,53,55,56,54,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,48,57,58,53,52,58,49,57,46,50,55,55,52,54,53,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,51,56,54,55,57,55,56,57,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,48,57,58,53,53,58,53,48,46,52,52,57,55,52,57,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,57,50,57,50,49,52,50,55,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,48,58,51,49,58,48,57,46,50,54,51,55,53,49,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,48,50,52,48,55,57,57,52,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,48,58,51,55,58,49,57,46,56,51,50,49,53,50,56,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,44,92,34,113,117,101,114,121,92,34,58,51,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,49,56,56,53,57,57,49,48,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,48,58,52,56,58,48,50,46,52,56,53,55,57,50,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,83,89,83,76,79,71,79,80,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,66,121,84,97,98,108,101,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,92,34,83,89,83,76,79,71,79,80,92,34,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,53,52,57,57,55,57,52,54,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,49,49,58,51,52,46,49,50,55,53,49,56,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,44,92,34,113,117,101,114,121,92,34,58,49,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,53,53,52,49,49,53,57,48,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,49,49,58,53,48,46,50,56,53,52,56,54,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,44,92,34,113,117,101,114,121,92,34,58,48,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,51,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,53,53,55,51,56,52,48,48,53,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,49,50,58,48,51,46,48,53,49,50,55,54,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,53,54,54,57,51,57,57,55,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,49,50,58,52,48,46,51,55,57,52,48,56,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,52,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,56,49,56,50,52,56,53,51,49,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,53,50,58,52,52,46,56,53,51,53,57,52,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,48,50,57,49,51,50,52,55,52,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,49,48,58,48,50,46,53,48,55,49,56,48,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,48,52,50,50,50,57,55,49,53,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,49,56,58,51,52,46,49,49,56,52,57,56,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,48,52,51,48,57,53,54,56,54,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,49,57,58,48,55,46,57,52,53,57,49,56,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,48,52,52,56,49,56,55,55,49,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,50,48,58,49,53,46,50,53,51,55,52,49,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,113,117,101,114,121,92,34,58,92,34,50,92,34,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,48,52,49,52,53,53,52,50,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,53,56,58,53,50,46,55,48,53,55,50,56,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,52,52,54,54,50,53,57,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,48,54,46,52,56,51,53,49,52,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,105,100,45,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,50,48,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,52,54,51,53,51,50,50,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,49,51,46,48,56,54,57,57,50,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,105,100,43,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,52,55,55,57,53,50,54,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,49,56,46,55,50,48,51,52,54,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,105,100,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,53,50,55,51,52,50,55,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,51,56,46,48,49,51,48,56,48,56,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,53,53,49,56,53,50,50,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,52,55,46,53,56,55,53,49,52,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,45,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,51,57,57,55,52,57,55,48,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,50,50,58,49,50,46,50,57,50,57,51,57,49,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,45,92,34,125,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,52,55,48,54,51,51,48,50,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,50,54,58,52,57,46,49,56,48,51,57,54,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,45,92,34,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,53,48,44,92,34,113,117,101,114,121,92,34,58,50,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,53,49,52,50,48,56,48,54,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,50,57,58,51,57,46,51,57,53,52,54,52,56,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,53,49,57,52,57,53,50,51,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,51,48,58,48,48,46,48,52,56,49,51,52,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,45,92,34,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,53,48,44,92,34,113,117,101,114,121,92,34,58,50,125,44,92,34,116,111,116,97,108,92,34,58,92,34,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,54,48,57,52,49,56,48,53,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,51,53,58,53,49,46,51,48,57,56,56,51,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,50,48,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,54,52,48,51,49,54,52,56,53,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,51,55,58,53,50,46,48,48,54,56,55,49,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,105,100,92,34,58,92,34,51,50,54,53,49,54,48,57,52,49,56,48,53,51,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,48,49,50,52,57,54,48,53,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,52,58,50,51,46,49,53,49,54,53,53,50,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,50,52,48,55,48,57,56,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,53,58,53,50,46,50,57,55,55,48,50,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,105,100,41,58,115,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,50,54,54,55,48,54,54,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,54,58,48,50,46,52,53,50,52,56,51,56,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,49,41,58,115,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,51,48,53,51,57,51,51,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,54,58,49,55,46,53,54,52,54,54,55,51,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,49,41,58,99,111,117,110,116,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,51,53,48,48,48,51,56,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,54,58,51,52,46,57,57,48,54,50,51,50,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,49,41,58,230,149,176,233,135,143,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,51,57,48,54,52,51,56,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,54,58,53,48,46,56,54,53,56,49,49,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,49,41,58,230,157,161,230,149,176,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,57,57,51,49,57,51,55,57,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,55,58,48,53,58,53,54,46,54,56,51,50,51,48,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,51,50,54,54,50,50,48,51,53,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,55,58,50,51,58,52,51,46,49,57,51,54,55,53,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,85,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,105,100,92,34,58,92,34,51,50,54,53,49,54,48,57,52,49,56,48,53,51,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,93,44,34,116,111,116,97,108,34,58,51,55,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,52,32,49,48,58,48,48,58,52,55,34,125]},\"cookie\":[],\"responseTime\":137,\"responseSize\":9324,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.5074999928474426,\"wait\":0.32520002126693726,\"dns\":0,\"tcp\":0,\"firstByte\":135.28540003299713,\"download\":1.2343999743461609,\"process\":0.05519998073577881,\"total\":138.40770000219345}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":195,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"[]\\\": {\\n \\\"table1\\\": {\\n \\\"@column\\\": \\\"id,createtime,httpmethod,RequestUrl,actionname,RequestParam,Elapsed\\\",\\n \\\"createtime%\\\": \\\"2024-3-5,2024-3-14\\\",\\n \\\"Elapsed%\\\": \\\"1,20\\\"\\n }\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"195\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 14 Mar 2024 02:00:47 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-15T02:00:05.6689995Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710381647691,\"requestStart\":1710381647692,\"offset\":{\"request\":1.5074999928474426,\"socket\":1.8327000141143799,\"response\":137.1181000471115,\"end\":138.35250002145767,\"lookup\":1.8327000141143799,\"connect\":1.8327000141143799,\"done\":138.40770000219345}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143272865,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"列表查询\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143191882, + "type": "http", + "path": null, + "name": "分页查询", + "responseId": 405647728, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"[]\": {//列表\r\n \"SYSLOGOP\": {//表名\r\n \"@column\": \"id,createtime\",//只查询两列\r\n \"@order\":\"createtime-\" //-倒序排序\r\n },\r\n \"page\": 1, //分页查询:第1页\r\n \"count\": 3, //每页3条\r\n //\"query\": 1 //查询内容: 0-对象,1-总数,2-数据、总数,默认为2\r\n },\r\n //\"total\": \"\", //总数 默认返回总数,不用传\r\n \r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.bcfe838e-0335-42f2-ae40-05fb07285447\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"bcfe838e-0335-42f2-ae40-05fb07285447\",\"requestIndex\":0,\"httpRequestId\":\"74d59ab7-76bd-4c77-9bcf-714ff5eb470e\"},\"type\":\"http\",\"response\":{\"id\":\"673bc01a-a4df-4514-b2e9-b13172c5eca8\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Tue, 12 Mar 2024 08:29:32 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-13T08:27:50.0233019Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,91,93,34,58,91,123,34,83,89,83,76,79,71,79,80,34,58,123,34,73,100,34,58,51,50,55,56,53,49,52,51,50,52,49,53,52,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,54,58,50,57,58,50,55,46,56,48,55,57,53,49,52,34,125,125,44,123,34,83,89,83,76,79,71,79,80,34,58,123,34,73,100,34,58,51,50,55,56,53,49,49,56,51,48,53,56,54,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,54,58,50,55,58,53,48,46,52,48,50,49,56,34,125,125,44,123,34,83,89,83,76,79,71,79,80,34,58,123,34,73,100,34,58,51,50,55,56,53,49,49,53,55,51,54,51,56,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,54,58,50,55,58,52,48,46,51,54,53,57,51,48,54,34,125,125,93,44,34,112,97,103,101,34,58,49,44,34,99,111,117,110,116,34,58,51,44,34,109,97,120,34,58,49,50,49,44,34,116,111,116,97,108,34,58,51,54,49,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,54,58,50,57,58,51,51,34,125]},\"cookie\":[],\"responseTime\":72,\"responseSize\":376,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.7229999899864197,\"wait\":0.46160000562667847,\"dns\":0,\"tcp\":0,\"firstByte\":69.22839999198914,\"download\":1.886900007724762,\"process\":0.04899996519088745,\"total\":73.34889996051788}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":136,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"[]\\\": {\\n \\\"SYSLOGOP\\\": {\\n \\\"@column\\\": \\\"id,createtime\\\",\\n \\\"@order\\\": \\\"createtime-\\\"\\n },\\n \\\"page\\\": 1,\\n \\\"count\\\": 3\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"136\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Tue, 12 Mar 2024 08:29:32 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-13T08:27:50.0233019Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710232173277,\"requestStart\":1710232173279,\"offset\":{\"request\":1.7229999899864197,\"socket\":2.184599995613098,\"response\":71.41299998760223,\"end\":73.299899995327,\"lookup\":2.184599995613098,\"connect\":2.184599995613098,\"done\":73.34889996051788}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143191882,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"分页查询\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143312444, + "type": "http", + "path": null, + "name": "查询数量(聚合函数)", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"SYSLOGOP\": {\r\n \"@column\": \"count(1):数量,sum(id):合计\",//支持聚合函数count、sum等, :后跟别名\r\n \"httpmethod\":\"GET\"\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.8cdec2eb-2a6e-4b14-b841-d45980eb6969\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"8cdec2eb-2a6e-4b14-b841-d45980eb6969\",\"requestIndex\":0,\"httpRequestId\":\"b833e53d-ca56-464c-a0f3-bf293186f3c0\"},\"type\":\"http\",\"response\":{\"id\":\"c4a883ba-8b68-4bcf-b155-5300deb9b77e\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Tue, 12 Mar 2024 03:49:08 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86393\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-13T03:46:44.5267221Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,83,89,83,76,79,71,79,80,34,58,123,34,230,149,176,233,135,143,34,58,54,44,34,229,144,136,232,174,161,34,58,49,57,53,56,55,56,50,53,54,53,53,56,55,53,48,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,49,58,52,57,58,48,56,34,125]},\"cookie\":[],\"responseTime\":57,\"responseSize\":145,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.178399980068207,\"wait\":0.46469998359680176,\"dns\":0,\"tcp\":0,\"firstByte\":54.569000005722046,\"download\":1.8371000289916992,\"process\":0.08829998970031738,\"total\":59.13749998807907}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":96,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"SYSLOGOP\\\": {\\n \\\"@column\\\": \\\"count(1):数量,sum(id):合计\\\",\\n \\\"httpmethod\\\": \\\"GET\\\"\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"96\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Tue, 12 Mar 2024 03:49:08 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86393\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-13T03:46:44.5267221Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710215348896,\"requestStart\":1710215348898,\"offset\":{\"request\":2.178399980068207,\"socket\":2.6430999636650085,\"response\":57.212099969387054,\"end\":59.049199998378754,\"lookup\":2.6430999636650085,\"connect\":2.6430999636650085,\"done\":59.13749998807907}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143312444,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"查询数量(聚合函数)\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143871046, + "type": "http", + "path": null, + "name": "关联查询", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "//用于子表补充数据,没有实现join\r\n{\r\n \"[]\": {\r\n //必须先查子表,再循环补充主表数据\r\n \"table3\": {\r\n //\"@column\": \"id\",//必须在此返回后面的关联列\r\n \"text\": \"update3\",\r\n \"@order\": \"id-\", //只能在第一个表排序\r\n },\r\n \"table2\": {\r\n \"id@\": \"/table3/table2id\",\r\n //\"@order\": \"id-\", //这里排序无效\r\n },\r\n \"table1\": {\r\n \"@column\": \"id,httpmethod\",\r\n \"id@\": \"/table2/table1id\",\r\n },\r\n \"table4\": {\r\n \"id@\": \"/table3/table4id\",\r\n },\r\n \"page\": 1,\r\n \"count\": 3\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.59b633e1-8099-48e6-9943-d0e27e50cae7\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"59b633e1-8099-48e6-9943-d0e27e50cae7\",\"requestIndex\":0,\"httpRequestId\":\"a04d8d6c-aad7-4eaa-8fe4-11d3edaf05e7\"},\"type\":\"http\",\"response\":{\"id\":\"1a94a35f-3d89-44ef-8923-0f9c5d54e59d\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 14 Mar 2024 03:53:26 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-15T03:53:27.0136472Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,91,93,34,58,91,123,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,51,51,51,44,34,116,97,98,108,101,50,105,100,34,58,50,51,44,34,116,101,120,116,34,58,34,117,112,100,97,116,101,51,34,44,34,116,97,98,108,101,52,105,100,34,58,52,52,125,44,34,116,97,98,108,101,50,34,58,123,34,105,100,34,58,50,51,44,34,116,101,120,116,34,58,34,110,101,119,103,101,116,34,44,34,116,97,98,108,101,49,105,100,34,58,51,50,54,53,51,50,51,54,56,53,48,55,53,55,44,34,105,115,68,101,108,101,116,101,34,58,48,125,44,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,51,50,51,54,56,53,48,55,53,55,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,68,69,76,69,84,69,34,125,44,34,116,97,98,108,101,52,34,58,123,34,105,100,34,58,52,52,44,34,116,101,120,116,34,58,34,52,52,34,125,125,44,123,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,51,51,44,34,116,97,98,108,101,50,105,100,34,58,50,51,44,34,116,101,120,116,34,58,34,117,112,100,97,116,101,51,34,44,34,116,97,98,108,101,52,105,100,34,58,52,51,125,44,34,116,97,98,108,101,50,34,58,123,34,105,100,34,58,50,51,44,34,116,101,120,116,34,58,34,110,101,119,103,101,116,34,44,34,116,97,98,108,101,49,105,100,34,58,51,50,54,53,51,50,51,54,56,53,48,55,53,55,44,34,105,115,68,101,108,101,116,101,34,58,48,125,44,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,51,50,51,54,56,53,48,55,53,55,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,68,69,76,69,84,69,34,125,125,44,123,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,51,50,44,34,116,97,98,108,101,50,105,100,34,58,50,50,44,34,116,101,120,116,34,58,34,117,112,100,97,116,101,51,34,44,34,116,97,98,108,101,52,105,100,34,58,52,50,125,44,34,116,97,98,108,101,50,34,58,123,34,105,100,34,58,50,50,44,34,116,101,120,116,34,58,34,110,101,119,103,101,116,34,44,34,116,97,98,108,101,49,105,100,34,58,51,50,54,53,51,50,51,56,56,48,48,57,54,53,44,34,105,115,68,101,108,101,116,101,34,58,48,125,44,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,51,50,51,56,56,48,48,57,54,53,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,71,69,84,34,125,44,34,116,97,98,108,101,52,34,58,123,34,105,100,34,58,52,50,44,34,116,101,120,116,34,58,34,52,50,34,125,125,93,44,34,112,97,103,101,34,58,49,44,34,99,111,117,110,116,34,58,51,44,34,109,97,120,34,58,50,44,34,116,111,116,97,108,34,58,52,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,52,32,49,49,58,53,51,58,50,55,34,125]},\"cookie\":[],\"responseTime\":590,\"responseSize\":779,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.1902999877929688,\"wait\":1.2958000302314758,\"dns\":0.8892999887466431,\"tcp\":0.6733999848365784,\"firstByte\":584.8849999904633,\"download\":1.787600040435791,\"process\":0.033399999141693115,\"total\":591.7548000216484}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":316,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"[]\\\": {\\n \\\"table3\\\": {\\n \\\"text\\\": \\\"update3\\\",\\n \\\"@order\\\": \\\"id-\\\"\\n },\\n \\\"table2\\\": {\\n \\\"id@\\\": \\\"/table3/table2id\\\"\\n },\\n \\\"table1\\\": {\\n \\\"@column\\\": \\\"id,httpmethod\\\",\\n \\\"id@\\\": \\\"/table2/table1id\\\"\\n },\\n \\\"table4\\\": {\\n \\\"id@\\\": \\\"/table3/table4id\\\"\\n },\\n \\\"page\\\": 1,\\n \\\"count\\\": 3\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"316\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 14 Mar 2024 03:53:26 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-15T03:53:27.0136472Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710388406828,\"requestStart\":1710388406830,\"offset\":{\"request\":2.1902999877929688,\"socket\":3.4861000180244446,\"lookup\":4.375400006771088,\"connect\":5.048799991607666,\"response\":589.9337999820709,\"end\":591.7214000225067,\"done\":591.7548000216484}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143871046,\"httpApiName\":\"统一查询入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"关联查询\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143975340, + "type": "http", + "path": null, + "name": "group by", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"[]\": {\r\n \"table1\": {\r\n \"@column\": \"actionname,httpmethod,max(id)\",\r\n \"@group\": \"actionname,httpmethod\",\r\n },\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.00314312-a9fe-4ad2-859c-b7555f9a0d3e\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"00314312-a9fe-4ad2-859c-b7555f9a0d3e\",\"requestIndex\":0,\"httpRequestId\":\"e6ba19a4-e4f9-4412-9b45-f48e81bb7277\"},\"type\":\"http\",\"response\":{\"id\":\"6134deb2-55f4-419b-9387-38ae669f137f\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Fri, 08 Mar 2024 08:23:12 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86397\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-09T08:14:42.5201051Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,91,93,34,58,91,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,80,111,115,116,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,53,50,49,51,57,56,52,53,52,51,52,49,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,53,51,50,51,56,56,48,48,57,54,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,66,121,84,97,98,108,101,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,52,55,52,52,57,50,50,54,51,48,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,82,101,109,111,118,101,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,53,51,49,53,48,50,48,55,53,53,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,53,51,50,54,54,50,50,48,51,53,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,83,117,98,109,105,116,85,114,108,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,53,48,52,50,57,50,50,50,50,49,51,125,125,93,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,56,32,49,54,58,50,51,58,49,51,34,125]},\"cookie\":[],\"responseTime\":8399,\"responseSize\":605,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.243699997663498,\"wait\":0.2954000011086464,\"dns\":0,\"tcp\":0,\"firstByte\":8396.081299997866,\"download\":1.9274000003933907,\"process\":0.052000001072883606,\"total\":8400.599799998105}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":162,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"[]\\\": {\\r\\n \\\"table1\\\": {\\r\\n \\\"@column\\\": \\\"actionname,httpmethod,max(id)\\\",\\r\\n \\\"@group\\\": \\\"actionname,httpmethod\\\",\\r\\n },\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"162\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Fri, 08 Mar 2024 08:23:12 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86397\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-09T08:14:42.5201051Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1709886184664,\"requestStart\":1709886184667,\"offset\":{\"request\":2.243699997663498,\"socket\":2.5390999987721443,\"response\":8398.620399996638,\"end\":8400.547799997032,\"lookup\":2.5390999987721443,\"connect\":2.5390999987721443,\"done\":8400.599799998105}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143975340,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"group by\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + ], + "mocks": [ + ], + "customApiFields": "{}", + "advancedSettings": { + "disabledSystemHeaders": { + } + }, + "mockScript": { + }, + "codeSamples": [ + ], + "commonResponseStatus": { + }, + "responseChildren": [ + "BLANK.405647728" + ], + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + }, + { + "name": "新增", + "api": { + "id": "152848473", + "method": "post", + "path": "/api/aPIJSON/post", + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "auth": { + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "responses": [ + { + "id": "408753512", + "name": "Success", + "code": 200, + "contentType": "json", + "jsonSchema": { + "$ref": "#/definitions/84275190" + } + } + ], + "responseExamples": [ + ], + "requestBody": { + "type": "application/json", + "parameters": [ + ], + "jsonSchema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "x-apifox-orders": [ + ] + }, + "example": "{\r\n \"table2\": \r\n {\r\n //\"id\": 236, //如果传id,就用前端id,如果没有,就后端生成\r\n \"text\": \"newget\"\r\n }\r\n\r\n}" + }, + "description": "", + "tags": [ + "aPIJSON" + ], + "status": "released", + "serverId": "", + "operationId": "api-aPIJSON-post-Post", + "sourceUrl": "", + "ordering": 12, + "cases": [ + { + "id": 144371019, + "type": "http", + "path": null, + "name": "单条", + "responseId": 0, + "parameters": { + "query": [ + ], + "header": [ + ], + "cookie": [ + ], + "path": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table2\": \r\n {\r\n //\"id\": 236, //如果传id,就用前端id,如果没有,就后端生成\r\n \"text\": \"newget\"\r\n }\r\n\r\n}", + "type": "application/json" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.04906a0f-2a35-491b-8335-13c224a6e496\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"04906a0f-2a35-491b-8335-13c224a6e496\",\"requestIndex\":0,\"httpRequestId\":\"8b54192a-17b5-4a20-b7c0-115533d436f3\"},\"type\":\"http\",\"response\":{\"id\":\"000965cc-02df-4aae-8b23-18368ab61ecb\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 06:58:40 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86395\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T06:48:49.8348531Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,50,34,58,91,51,50,55,54,49,54,51,48,53,50,52,57,57,55,44,50,51,56,93,44,34,116,97,98,108,101,51,34,58,51,50,55,54,49,54,51,48,53,50,56,48,54,57,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,52,58,53,56,58,52,49,34,125]},\"cookie\":[],\"responseTime\":118,\"responseSize\":150,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.154699981212616,\"wait\":0.394599974155426,\"dns\":0,\"tcp\":0,\"firstByte\":114.57359999418259,\"download\":2.3081000447273254,\"process\":0.08730000257492065,\"total\":119.51829999685287}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"post\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":171,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"table2\\\": [\\n {\\n \\\"text\\\": \\\"newget\\\"\\n },\\n {\\n \\\"id\\\": 238,\\n \\\"text\\\": \\\"newget2\\\"\\n }\\n ],\\n \\\"table3\\\": {\\n \\\"table2id\\\": 238,\\n \\\"text\\\": \\\"newget\\\"\\n }\\n}\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/post\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"171\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 06:58:40 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86395\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T06:48:49.8348531Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710140321198,\"requestStart\":1710140321200,\"offset\":{\"request\":2.154699981212616,\"socket\":2.549299955368042,\"response\":117.12289994955063,\"end\":119.43099999427795,\"lookup\":2.549299955368042,\"connect\":2.549299955368042,\"done\":119.51829999685287}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848473,\"httpApiName\":\"新增\",\"httpApiPath\":\"/api/aPIJSON/post\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"成功\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 144370602, + "type": "http", + "path": null, + "name": "多表批量", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table2\": [\r\n {\r\n //\"id\": 236, //如果传id,就用前端id,如果没有,就后端生成\r\n \"text\": \"newget\"\r\n },\r\n {\r\n \"id\": 240,\r\n \"text\": \"newget2\"\r\n }\r\n ],\r\n \"table3\":{\r\n \"table2id\":240,\r\n \"text\":\"newget\"\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.89a3b724-e244-47bd-8332-b18d13b87f39\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"89a3b724-e244-47bd-8332-b18d13b87f39\",\"requestIndex\":0,\"httpRequestId\":\"c4ff5552-4d6b-4816-bac7-951f9acbd466\"},\"type\":\"http\",\"response\":{\"id\":\"a2e99ccb-e877-4151-9d9e-157c49c50e65\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 07:32:24 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T07:30:34.5282454Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,50,34,58,123,34,105,100,34,58,91,51,50,55,54,50,49,52,56,54,49,48,54,50,57,44,50,52,48,93,44,34,99,111,117,110,116,34,58,50,125,44,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,51,50,55,54,50,49,52,56,54,49,55,53,52,49,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,53,58,51,50,58,50,53,34,125]},\"cookie\":[],\"responseTime\":168,\"responseSize\":174,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.51419997215271,\"wait\":0.2290000319480896,\"dns\":0,\"tcp\":0,\"firstByte\":165.6187999844551,\"download\":1.2360000014305115,\"process\":0.05760002136230469,\"total\":168.65560001134872}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"post\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":171,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"table2\\\": [\\n {\\n \\\"text\\\": \\\"newget\\\"\\n },\\n {\\n \\\"id\\\": 240,\\n \\\"text\\\": \\\"newget2\\\"\\n }\\n ],\\n \\\"table3\\\": {\\n \\\"table2id\\\": 240,\\n \\\"text\\\": \\\"newget\\\"\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/post\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"171\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 07:32:24 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T07:30:34.5282454Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710142344933,\"requestStart\":1710142344934,\"offset\":{\"request\":1.51419997215271,\"socket\":1.7432000041007996,\"response\":167.3619999885559,\"end\":168.59799998998642,\"lookup\":1.7432000041007996,\"connect\":1.7432000041007996,\"done\":168.65560001134872}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848473,\"httpApiCaseId\":144370602,\"httpApiName\":\"新增\",\"httpApiPath\":\"/api/aPIJSON/post\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"多表批量\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + ], + "mocks": [ + ], + "customApiFields": "{}", + "advancedSettings": { + "disabledSystemHeaders": { + } + }, + "mockScript": { + }, + "codeSamples": [ + ], + "commonResponseStatus": { + }, + "responseChildren": [ + "BLANK.408753512" + ], + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + }, + { + "name": "修改", + "api": { + "id": "152848474", + "method": "post", + "path": "/api/aPIJSON/put", + "parameters": { + "path": [ + ], + "query": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "auth": { + }, + "commonParameters": { + }, + "responses": [ + { + "id": "408753513", + "name": "Success", + "code": 200, + "contentType": "json", + "jsonSchema": { + "$ref": "#/definitions/84275190" + } + } + ], + "responseExamples": [ + ], + "requestBody": { + "type": "application/json", + "parameters": [ + ], + "jsonSchema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "x-apifox-orders": [ + ] + }, + "example": "" + }, + "description": "", + "tags": [ + "aPIJSON" + ], + "status": "released", + "serverId": "", + "operationId": "api-aPIJSON-put-Post", + "sourceUrl": "", + "ordering": 18, + "cases": [ + { + "id": 144398604, + "type": "http", + "path": null, + "name": "通过id单条更新", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table3\": {\r\n \"id\": 32762120351813,\r\n \"text\": \"edit\",\r\n \"table2id\":255\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.3c2d26df-ff36-4408-84d1-ba86a03a15d9\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"3c2d26df-ff36-4408-84d1-ba86a03a15d9\",\"requestIndex\":0,\"httpRequestId\":\"01efc573-4df9-4ee4-8f19-329b479d8f83\"},\"type\":\"http\",\"response\":{\"id\":\"c0132dd1-a39d-491a-a497-7a91230bfbc7\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 08:24:50 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T08:24:51.3365034Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,51,34,58,123,34,99,111,100,101,34,58,50,48,48,44,34,109,115,103,34,58,34,115,117,99,99,101,115,115,34,44,34,105,100,34,58,34,51,50,55,54,50,49,50,48,51,53,49,56,49,51,34,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,54,58,50,52,58,53,49,34,125]},\"cookie\":[],\"responseTime\":626,\"responseSize\":156,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.3127999901771545,\"wait\":1.4280999898910522,\"dns\":0.35510003566741943,\"tcp\":0.7379999756813049,\"firstByte\":621.6582000255585,\"download\":1.3226999640464783,\"process\":0.029600024223327637,\"total\":627.8445000052452}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"put\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":108,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table3\\\": {\\r\\n \\\"id\\\": 32762120351813,\\r\\n \\\"text\\\": \\\"edit\\\",\\r\\n \\\"table2id\\\":255\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/put\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"108\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 08:24:50 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T08:24:51.3365034Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710145491156,\"requestStart\":1710145491158,\"offset\":{\"request\":2.3127999901771545,\"socket\":3.740899980068207,\"lookup\":4.096000015735626,\"connect\":4.833999991416931,\"response\":626.4922000169754,\"end\":627.8148999810219,\"done\":627.8445000052452}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848474,\"httpApiCaseId\":144398604,\"httpApiName\":\"修改\",\"httpApiPath\":\"/api/aPIJSON/put\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"单条更新\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 144399593, + "type": "http", + "path": null, + "name": "多表多id批量更新", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table3\": [\r\n {\r\n \"id\": [31,32,33],\r\n \"text\": \"update3\"\r\n },\r\n {\r\n \"id\": 32762148617541,\r\n \"text\": \"update2\"\r\n }\r\n ],\r\n \"table2\": {\r\n \"id\": 32761658134341,\r\n \"text\": \"update\"\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.a347f44c-82a5-437f-afef-bef75f5f55f4\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"a347f44c-82a5-437f-afef-bef75f5f55f4\",\"requestIndex\":0,\"httpRequestId\":\"1ee88481-8f7b-480f-adf0-3a74acff8426\"},\"type\":\"http\",\"response\":{\"id\":\"6d9b1b3d-167c-4fe8-832e-f336630e6e64\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 09:38:05 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86397\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T09:36:21.4486506Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,51,34,58,123,34,99,111,117,110,116,34,58,52,125,44,34,116,97,98,108,101,50,34,58,123,34,99,111,117,110,116,34,58,49,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,55,58,51,56,58,48,53,34,125]},\"cookie\":[],\"responseTime\":135,\"responseSize\":138,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.440500020980835,\"wait\":0.2959999442100525,\"dns\":0,\"tcp\":0,\"firstByte\":132.63910001516342,\"download\":1.2211000323295593,\"process\":0.04819995164871216,\"total\":135.64489996433258}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"put\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":283,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table3\\\": [\\r\\n {\\r\\n \\\"id\\\": [31,32,33],\\r\\n \\\"text\\\": \\\"update3\\\"\\r\\n },\\r\\n {\\r\\n \\\"id\\\": 32762148617541,\\r\\n \\\"text\\\": \\\"update2\\\"\\r\\n }\\r\\n ],\\r\\n \\\"table2\\\": {\\r\\n \\\"id\\\": 32761658134341,\\r\\n \\\"text\\\": \\\"update\\\"\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/put\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"283\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 09:38:05 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86397\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T09:36:21.4486506Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710149885351,\"requestStart\":1710149885352,\"offset\":{\"request\":1.440500020980835,\"socket\":1.7364999651908875,\"response\":134.3755999803543,\"end\":135.59670001268387,\"lookup\":1.7364999651908875,\"connect\":1.7364999651908875,\"done\":135.64489996433258}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848474,\"httpApiCaseId\":144399593,\"httpApiName\":\"修改\",\"httpApiPath\":\"/api/aPIJSON/put\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"多表更新\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + ], + "mocks": [ + ], + "customApiFields": "{}", + "advancedSettings": { + "disabledSystemHeaders": { + } + }, + "mockScript": { + }, + "codeSamples": [ + ], + "commonResponseStatus": { + }, + "responseChildren": [ + ], + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + }, + { + "name": "删除", + "api": { + "id": "152848475", + "method": "post", + "path": "/api/aPIJSON/delete", + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "auth": { + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "responses": [ + { + "id": "408753514", + "name": "Success", + "code": 200, + "contentType": "json", + "jsonSchema": { + "$ref": "#/definitions/84275190" + } + } + ], + "responseExamples": [ + ], + "requestBody": { + "type": "application/json", + "parameters": [ + ], + "jsonSchema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "x-apifox-orders": [ + ] + }, + "example": "{\r\n \"table1\": {\r\n \"id\": [32520112744261,32520179315781,32520200693573],\r\n }\r\n}" + }, + "description": "", + "tags": [ + "aPIJSON" + ], + "status": "released", + "serverId": "", + "operationId": "api-aPIJSON-delete-Post", + "sourceUrl": "", + "ordering": 24, + "cases": [ + { + "id": 143343716, + "type": "http", + "path": null, + "name": "删除单条数据", + "responseId": 0, + "parameters": { + "query": [ + ], + "header": [ + ], + "cookie": [ + ], + "path": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table1\": {\r\n \"id\": \"32520494044741\",\r\n }\r\n}", + "type": "application/json" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.26698a36-85d0-4cbb-b880-abbb4872dddf\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"26698a36-85d0-4cbb-b880-abbb4872dddf\",\"requestIndex\":0,\"httpRequestId\":\"e91c2ef7-03a4-41fa-b114-138973559bec\"},\"type\":\"http\",\"response\":{\"id\":\"a6999ac7-bec5-4b65-a905-42dbe74794e7\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Wed, 06 Mar 2024 09:16:09 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-07T09:15:13.4609976Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,49,34,58,123,34,99,111,100,101,34,58,50,48,48,44,34,109,115,103,34,58,34,115,117,99,99,101,115,115,34,44,34,105,100,34,58,34,51,50,53,50,48,52,57,52,48,52,52,55,52,49,34,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,55,58,49,54,58,48,57,34,125]},\"cookie\":[],\"responseTime\":111,\"responseSize\":156,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.5057999789714813,\"wait\":0.2891000211238861,\"dns\":0,\"tcp\":0,\"firstByte\":109.1957999765873,\"download\":1.360700011253357,\"process\":0.034900009632110596,\"total\":112.38629999756813}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"delete\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTE5NzE1MiwibmJmIjoxNzA5MTk3MTUyLCJleHAiOjE3MDk4MDE5NTIsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.96Y2rBegXqiYjuXcXeVyuXV75Cmzu8FpILylDcougt8\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":61,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table1\\\": {\\r\\n \\\"id\\\": \\\"32520494044741\\\",\\r\\n }\\r\\n}\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTE5NzE1MiwibmJmIjoxNzA5MTk3MTUyLCJleHAiOjE3MDk4MDE5NTIsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.96Y2rBegXqiYjuXcXeVyuXV75Cmzu8FpILylDcougt8\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/delete\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTE5NzE1MiwibmJmIjoxNzA5MTk3MTUyLCJleHAiOjE3MDk4MDE5NTIsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.96Y2rBegXqiYjuXcXeVyuXV75Cmzu8FpILylDcougt8\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"61\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Wed, 06 Mar 2024 09:16:09 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-07T09:15:13.4609976Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1709716569879,\"requestStart\":1709716569880,\"offset\":{\"request\":1.5057999789714813,\"socket\":1.7949000000953674,\"response\":110.99069997668266,\"end\":112.35139998793602,\"lookup\":1.7949000000953674,\"connect\":1.7949000000953674,\"done\":112.38629999756813}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848475,\"httpApiName\":\"删除\",\"httpApiPath\":\"/api/aPIJSON/delete\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"成功\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143368262, + "type": "http", + "path": null, + "name": "批量删除id", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table3\": {\r\n \"id\": [\r\n 32761630528069,\r\n 239\r\n ]\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.c39182fc-fc2f-4357-b3b7-32e41ef1fb57\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"c39182fc-fc2f-4357-b3b7-32e41ef1fb57\",\"requestIndex\":0,\"httpRequestId\":\"7443b4a1-f762-4a24-8dd2-ad33ce1c8da7\"},\"type\":\"http\",\"response\":{\"id\":\"43956c21-adae-41c7-a722-c856c7c7c0aa\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 07:34:26 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86396\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T07:26:16.5966009Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,91,51,50,55,54,49,54,51,48,53,50,56,48,54,57,44,50,51,57,93,44,34,99,111,117,110,116,34,58,49,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,53,58,51,52,58,50,55,34,125]},\"cookie\":[],\"responseTime\":158,\"responseSize\":143,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.1498000025749207,\"wait\":0.36399996280670166,\"dns\":0,\"tcp\":0,\"firstByte\":156.0023000240326,\"download\":1.451200008392334,\"process\":0.04890000820159912,\"total\":160.01620000600815}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"delete\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":55,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table3\\\": {\\r\\n\\\"id\\\":[32761630528069,239]\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/delete\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"55\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 07:34:26 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86396\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T07:26:16.5966009Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710142466906,\"requestStart\":1710142466908,\"offset\":{\"request\":2.1498000025749207,\"socket\":2.5137999653816223,\"response\":158.51609998941422,\"end\":159.96729999780655,\"lookup\":2.5137999653816223,\"connect\":2.5137999653816223,\"done\":160.01620000600815}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848475,\"httpApiCaseId\":143368262,\"httpApiName\":\"删除\",\"httpApiPath\":\"/api/aPIJSON/delete\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"批量删除id\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143543753, + "type": "http", + "path": null, + "name": "批量删除(匹配条件)", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table1\": {\r\n \"httpmethod\": [\"GET\",\"POST\"],\r\n \"ThreadId\": [37,39],\"Actionname\":\"SwaggerCheckUrl\",\"loglevel\":2.5,\"isdelete\":false\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.44a9ebb3-1e3d-4049-936a-a412fb468f00\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"44a9ebb3-1e3d-4049-936a-a412fb468f00\",\"requestIndex\":0,\"httpRequestId\":\"2365b41d-b930-4f24-85d5-12b1198e2c4d\"},\"type\":\"http\",\"response\":{\"id\":\"097a32a0-1cbe-410f-979f-e1170b268276\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 07 Mar 2024 10:10:46 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-08T10:10:47.0961001Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,49,34,58,123,34,104,116,116,112,109,101,116,104,111,100,34,58,91,34,71,69,84,34,44,34,80,79,83,84,34,93,44,34,84,104,114,101,97,100,73,100,34,58,91,51,55,44,51,57,93,44,34,65,99,116,105,111,110,110,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,108,111,103,108,101,118,101,108,34,58,50,46,53,44,34,105,115,100,101,108,101,116,101,34,58,102,97,108,115,101,44,34,99,111,117,110,116,34,58,48,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,55,32,49,56,58,49,48,58,52,55,34,125]},\"cookie\":[],\"responseTime\":602,\"responseSize\":227,\"type\":\"http\",\"tempFilePath\":\"\"},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"delete\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":159,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table1\\\": {\\r\\n \\\"httpmethod\\\": [\\\"GET\\\",\\\"POST\\\"],\\r\\n \\\"ThreadId\\\": [37,39],\\\"Actionname\\\":\\\"SwaggerCheckUrl\\\",\\\"loglevel\\\":2.5,\\\"isdelete\\\":false\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/delete\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"159\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 07 Mar 2024 10:10:46 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-08T10:10:47.0961001Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1709806246909,\"requestStart\":1709806246912,\"offset\":{\"request\":2.155799984931946,\"socket\":3.6095999479293823,\"lookup\":3.9983999729156494,\"connect\":4.8549999594688416,\"response\":602.0516999959946,\"end\":603.2913999557495,\"done\":603.3172999620438}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848475,\"httpApiCaseId\":143543753,\"httpApiName\":\"删除\",\"httpApiPath\":\"/api/aPIJSON/delete\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"批量删除2\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + ], + "mocks": [ + ], + "customApiFields": "{}", + "advancedSettings": { + "disabledSystemHeaders": { + } + }, + "mockScript": { + }, + "codeSamples": [ + ], + "commonResponseStatus": { + }, + "responseChildren": [ + "BLANK.408753514" + ], + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + } + ] + } + ] + } + ], + "socketCollection": [ + ], + "docCollection": [ + ], + "schemaCollection": [ + { + "name": "根目录", + "items": [ + { + "name": "Schemas", + "items": [ + { + "name": "AccountTypeEnum", + "displayName": "", + "id": "#/definitions/84275167", + "description": "账号类型枚举
 会员 Member = 666
 普通账号 NormalUser = 777
 系统管理员 SysAdmin = 888
 超级管理员 SuperAdmin = 999
", + "schema": { + "jsonSchema": { + "enum": [ + 666, + 777, + 888, + 999 + ], + "type": "integer", + "description": "账号类型枚举
 会员 Member = 666
 普通账号 NormalUser = 777
 系统管理员 SysAdmin = 888
 超级管理员 SuperAdmin = 999
", + "format": "int32" + } + } + }, + { + "name": "AddCodeGenInput", + "displayName": "", + "id": "#/definitions/84275168", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "authorName", + "busName", + "generateType", + "menuPid", + "nameSpace", + "tableName" + ], + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "className": { + "type": [ + "string", + "null" + ], + "description": "类名" + }, + "tablePrefix": { + "type": [ + "string", + "null" + ], + "description": "是否移除表前缀" + }, + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbName": { + "type": [ + "string", + "null" + ], + "description": "数据库名(保留字段)" + }, + "dbType": { + "type": [ + "string", + "null" + ], + "description": "数据库类型" + }, + "connectionString": { + "type": [ + "string", + "null" + ], + "description": "数据库链接" + }, + "tableComment": { + "type": [ + "string", + "null" + ], + "description": "功能名(数据库表名称)" + }, + "menuApplication": { + "type": [ + "string", + "null" + ], + "description": "菜单应用分类(应用编码)" + }, + "printType": { + "type": [ + "string", + "null" + ], + "description": "支持打印类型" + }, + "printName": { + "type": [ + "string", + "null" + ], + "description": "打印模版名称" + }, + "tableName": { + "minLength": 1, + "type": "string", + "description": "数据库表名" + }, + "busName": { + "minLength": 1, + "type": "string", + "description": "业务名(业务代码包名称)" + }, + "nameSpace": { + "minLength": 1, + "type": "string", + "description": "命名空间" + }, + "authorName": { + "minLength": 1, + "type": "string", + "description": "作者姓名" + }, + "generateType": { + "minLength": 1, + "type": "string", + "description": "生成方式" + }, + "menuPid": { + "type": "integer", + "description": "菜单父级", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "className", + "tablePrefix", + "configId", + "dbName", + "dbType", + "connectionString", + "tableComment", + "menuApplication", + "printType", + "printName", + "tableName", + "busName", + "nameSpace", + "authorName", + "generateType", + "menuPid" + ] + } + } + }, + { + "name": "AddConfigInput", + "displayName": "", + "id": "#/definitions/84275169", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "value": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "属性值" + }, + "sysFlag": { + "$ref": "#/definitions/84275444" + }, + "groupCode": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "分组编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "value", + "sysFlag", + "groupCode", + "orderNo", + "remark" + ] + } + } + }, + { + "name": "AddDictDataInput", + "displayName": "", + "id": "#/definitions/84275170", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "value" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "dictTypeId": { + "type": "integer", + "description": "字典类型Id", + "format": "int64" + }, + "value": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "值" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "tagType": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "显示样式-标签颜色" + }, + "styleSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Style(控制显示样式)" + }, + "classSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Class(控制显示样式)" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 2048, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "extData": { + "type": [ + "string", + "null" + ], + "description": "拓展数据(保存业务功能的配置项)" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "dictTypeId", + "value", + "code", + "tagType", + "styleSetting", + "classSetting", + "orderNo", + "remark", + "extData", + "status" + ] + } + } + }, + { + "name": "AddDictTypeInput", + "displayName": "", + "id": "#/definitions/84275171", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "字典值集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "orderNo", + "remark", + "status", + "children" + ] + } + } + }, + { + "name": "AddJobDetailInput", + "displayName": "", + "id": "#/definitions/84275172", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "jobId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "groupName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组名称" + }, + "jobType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "作业类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "concurrent": { + "type": "boolean", + "description": "是否并行执行" + }, + "includeAnnotations": { + "type": "boolean", + "description": "是否扫描特性触发器" + }, + "properties": { + "type": [ + "string", + "null" + ], + "description": "额外数据" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createType": { + "$ref": "#/definitions/84275308" + }, + "scriptCode": { + "type": [ + "string", + "null" + ], + "description": "脚本代码" + }, + "jobId": { + "minLength": 2, + "type": "string", + "description": "作业Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "groupName", + "jobType", + "assemblyName", + "description", + "concurrent", + "includeAnnotations", + "properties", + "updatedTime", + "createType", + "scriptCode", + "jobId" + ] + } + } + }, + { + "name": "AddJobTriggerInput", + "displayName": "", + "id": "#/definitions/84275173", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "jobId", + "triggerId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "triggerType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "触发器类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "args": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "参数" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "status": { + "$ref": "#/definitions/84275411" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "起始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + }, + "lastRunTime": { + "type": [ + "string", + "null" + ], + "description": "最近运行时间", + "format": "date-time" + }, + "nextRunTime": { + "type": [ + "string", + "null" + ], + "description": "下一次运行时间", + "format": "date-time" + }, + "numberOfRuns": { + "type": "integer", + "description": "触发次数", + "format": "int64" + }, + "maxNumberOfRuns": { + "type": "integer", + "description": "最大触发次数(0:不限制,n:N次)", + "format": "int64" + }, + "numberOfErrors": { + "type": "integer", + "description": "出错次数", + "format": "int64" + }, + "maxNumberOfErrors": { + "type": "integer", + "description": "最大出错次数(0:不限制,n:N次)", + "format": "int64" + }, + "numRetries": { + "type": "integer", + "description": "重试次数", + "format": "int32" + }, + "retryTimeout": { + "type": "integer", + "description": "重试间隔时间(ms)", + "format": "int32" + }, + "startNow": { + "type": "boolean", + "description": "是否立即启动" + }, + "runOnStart": { + "type": "boolean", + "description": "是否启动时执行一次" + }, + "resetOnlyOnce": { + "type": "boolean", + "description": "是否在启动时重置最大触发次数等于一次的作业" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "jobId": { + "minLength": 2, + "type": "string", + "description": "作业Id" + }, + "triggerId": { + "minLength": 2, + "type": "string", + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "triggerType", + "assemblyName", + "args", + "description", + "status", + "startTime", + "endTime", + "lastRunTime", + "nextRunTime", + "numberOfRuns", + "maxNumberOfRuns", + "numberOfErrors", + "maxNumberOfErrors", + "numRetries", + "retryTimeout", + "startNow", + "runOnStart", + "resetOnlyOnce", + "updatedTime", + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "AddMenuInput", + "displayName": "", + "id": "#/definitions/84275174", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "type": { + "$ref": "#/definitions/84275319" + }, + "name": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "路由名称" + }, + "path": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "路由地址" + }, + "component": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组件路径" + }, + "redirect": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "重定向" + }, + "permission": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "权限标识" + }, + "icon": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "图标" + }, + "isIframe": { + "type": "boolean", + "description": "是否内嵌" + }, + "outLink": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "外链链接" + }, + "isHide": { + "type": "boolean", + "description": "是否隐藏" + }, + "isKeepAlive": { + "type": "boolean", + "description": "是否缓存" + }, + "isAffix": { + "type": "boolean", + "description": "是否固定" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275390" + }, + "description": "菜单子项" + }, + "title": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "pid", + "type", + "name", + "path", + "component", + "redirect", + "permission", + "icon", + "isIframe", + "outLink", + "isHide", + "isKeepAlive", + "isAffix", + "orderNo", + "status", + "remark", + "children", + "title" + ] + } + } + }, + { + "name": "AddNoticeInput", + "displayName": "", + "id": "#/definitions/84275175", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "content", + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "title": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "标题" + }, + "content": { + "minLength": 1, + "type": "string", + "description": "内容" + }, + "type": { + "$ref": "#/definitions/84275325" + }, + "publicUserId": { + "type": "integer", + "description": "发布人Id", + "format": "int64" + }, + "publicUserName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "发布人姓名" + }, + "publicOrgId": { + "type": "integer", + "description": "发布机构Id", + "format": "int64" + }, + "publicOrgName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "发布机构名称" + }, + "publicTime": { + "type": [ + "string", + "null" + ], + "description": "发布时间", + "format": "date-time" + }, + "cancelTime": { + "type": [ + "string", + "null" + ], + "description": "撤回时间", + "format": "date-time" + }, + "status": { + "$ref": "#/definitions/84275324" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "title", + "content", + "type", + "publicUserId", + "publicUserName", + "publicOrgId", + "publicOrgName", + "publicTime", + "cancelTime", + "status" + ] + } + } + }, + { + "name": "AddOpenAccessInput", + "displayName": "", + "id": "#/definitions/84275176", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "accessKey", + "accessSecret", + "bindUserId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "bindTenantId": { + "type": "integer", + "description": "绑定租户Id", + "format": "int64" + }, + "accessKey": { + "minLength": 1, + "type": "string", + "description": "身份标识" + }, + "accessSecret": { + "minLength": 1, + "type": "string", + "description": "密钥" + }, + "bindUserId": { + "type": "integer", + "description": "绑定用户Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "bindTenantId", + "accessKey", + "accessSecret", + "bindUserId" + ] + } + } + }, + { + "name": "AddOrgInput", + "displayName": "", + "id": "#/definitions/84275177", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "level": { + "type": [ + "integer", + "null" + ], + "description": "级别", + "format": "int32" + }, + "type": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "机构类型-数据字典" + }, + "directorId": { + "type": [ + "integer", + "null" + ], + "description": "负责人Id", + "format": "int64" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275395" + }, + "description": "机构子项" + }, + "disabled": { + "type": "boolean", + "description": "是否禁止选中" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "pid", + "code", + "level", + "type", + "directorId", + "orderNo", + "status", + "remark", + "children", + "disabled", + "name" + ] + } + } + }, + { + "name": "AddPluginInput", + "displayName": "", + "id": "#/definitions/84275178", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "csharpCode", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "csharpCode": { + "minLength": 1, + "type": "string", + "description": "C#代码" + }, + "assemblyName": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "程序集名称" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "csharpCode", + "assemblyName", + "orderNo", + "status", + "remark", + "name" + ] + } + } + }, + { + "name": "AddPosInput", + "displayName": "", + "id": "#/definitions/84275179", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "code", + "orderNo", + "remark", + "status", + "name" + ] + } + } + }, + { + "name": "AddPrintInput", + "displayName": "", + "id": "#/definitions/84275180", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name", + "template" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "template": { + "minLength": 1, + "type": "string", + "description": "打印模板" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "template", + "orderNo", + "status", + "remark", + "name" + ] + } + } + }, + { + "name": "AddRegionInput", + "displayName": "", + "id": "#/definitions/84275181", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "shortName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "简称" + }, + "mergerName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "组合名" + }, + "code": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "行政代码" + }, + "zipCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "邮政编码" + }, + "cityCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "区号" + }, + "level": { + "type": "integer", + "description": "层级", + "format": "int32" + }, + "pinYin": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "拼音" + }, + "lng": { + "type": "number", + "description": "经度", + "format": "float" + }, + "lat": { + "type": "number", + "description": "维度", + "format": "float" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "机构子项" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "pid", + "shortName", + "mergerName", + "code", + "zipCode", + "cityCode", + "level", + "pinYin", + "lng", + "lat", + "orderNo", + "remark", + "children", + "name" + ] + } + } + }, + { + "name": "AddRoleInput", + "displayName": "", + "id": "#/definitions/84275182", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "dataScope": { + "$ref": "#/definitions/84275269" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + }, + "menuIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "菜单Id集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "code", + "orderNo", + "dataScope", + "remark", + "status", + "name", + "menuIdList" + ] + } + } + }, + { + "name": "AddSubscribeMessageTemplateInput", + "displayName": "", + "id": "#/definitions/84275183", + "description": "增加订阅消息模板", + "schema": { + "jsonSchema": { + "required": [ + "keyworkIdList", + "sceneDescription", + "templateTitleId" + ], + "type": "object", + "properties": { + "templateTitleId": { + "minLength": 1, + "type": "string", + "description": "模板标题Id" + }, + "keyworkIdList": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "description": "模板关键词列表,例如 [3,5,4]" + }, + "sceneDescription": { + "minLength": 1, + "type": "string", + "description": "服务场景描述,15个字以内" + } + }, + "additionalProperties": false, + "description": "增加订阅消息模板", + "x-apifox-orders": [ + "templateTitleId", + "keyworkIdList", + "sceneDescription" + ] + } + } + }, + { + "name": "AddTenantInput", + "displayName": "", + "id": "#/definitions/84275184", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "adminAccount", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "host": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "主机" + }, + "tenantType": { + "$ref": "#/definitions/84275409" + }, + "dbType": { + "$ref": "#/definitions/84275276" + }, + "connection": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "数据库连接" + }, + "configId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库标识" + }, + "slaveConnections": { + "type": [ + "string", + "null" + ], + "description": "从库连接/读写分离" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "电子邮箱" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + }, + "name": { + "minLength": 2, + "type": "string", + "description": "租户名称" + }, + "adminAccount": { + "minLength": 3, + "type": "string", + "description": "租管账号" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "userId", + "orgId", + "host", + "tenantType", + "dbType", + "connection", + "configId", + "slaveConnections", + "orderNo", + "remark", + "status", + "email", + "phone", + "name", + "adminAccount" + ] + } + } + }, + { + "name": "AddUserInput", + "displayName": "", + "id": "#/definitions/84275185", + "description": "增加用户输入参数", + "schema": { + "jsonSchema": { + "required": [ + "account", + "realName" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "nickName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "sex": { + "$ref": "#/definitions/84275305" + }, + "age": { + "type": "integer", + "description": "年龄", + "format": "int32" + }, + "birthday": { + "type": [ + "string", + "null" + ], + "description": "出生日期", + "format": "date-time" + }, + "nation": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "民族" + }, + "phone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "cardType": { + "$ref": "#/definitions/84275258" + }, + "idCardNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "身份证号" + }, + "email": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "address": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "cultureLevel": { + "$ref": "#/definitions/84275267" + }, + "politicalOutlook": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "政治面貌" + }, + "college": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "毕业院校" + }, + "officePhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "办公电话" + }, + "emergencyContact": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "紧急联系人" + }, + "emergencyPhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "紧急联系人电话" + }, + "emergencyAddress": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "紧急联系人地址" + }, + "introduction": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "orgId": { + "type": "integer", + "description": "直属机构Id", + "format": "int64" + }, + "sysOrg": { + "$ref": "#/definitions/84275395" + }, + "managerUserId": { + "type": [ + "integer", + "null" + ], + "description": "直属主管Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "posTitle": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职称" + }, + "expertise": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "擅长领域" + }, + "officeZone": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公区域" + }, + "office": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公室" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + }, + "lastLoginIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "最新登录Ip" + }, + "lastLoginAddress": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录地点" + }, + "lastLoginTime": { + "type": [ + "string", + "null" + ], + "description": "最新登录时间", + "format": "date-time" + }, + "lastLoginDevice": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录设备" + }, + "signature": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "电子签名" + }, + "account": { + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "minLength": 1, + "type": "string", + "description": "真实姓名" + }, + "roleIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "角色集合" + }, + "extOrgIdList": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275402" + }, + "description": "扩展机构集合" + } + }, + "additionalProperties": false, + "description": "增加用户输入参数", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "nickName", + "avatar", + "sex", + "age", + "birthday", + "nation", + "phone", + "cardType", + "idCardNum", + "email", + "address", + "cultureLevel", + "politicalOutlook", + "college", + "officePhone", + "emergencyContact", + "emergencyPhone", + "emergencyAddress", + "introduction", + "orderNo", + "status", + "remark", + "accountType", + "orgId", + "sysOrg", + "managerUserId", + "posId", + "jobNum", + "posLevel", + "posTitle", + "expertise", + "officeZone", + "office", + "joinDate", + "lastLoginIp", + "lastLoginAddress", + "lastLoginTime", + "lastLoginDevice", + "signature", + "account", + "realName", + "roleIdList", + "extOrgIdList" + ] + } + } + }, + { + "name": "AdminResult_Boolean", + "displayName": "", + "id": "#/definitions/84275186", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": "boolean", + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_IActionResult", + "displayName": "", + "id": "#/definitions/84275187", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275306" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_Int32", + "displayName": "", + "id": "#/definitions/84275188", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": "integer", + "description": "数据", + "format": "int32" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_Int64", + "displayName": "", + "id": "#/definitions/84275189", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": "integer", + "description": "数据", + "format": "int64" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_JObject", + "displayName": "", + "id": "#/definitions/84275190", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "description": "数据", + "x-apifox-orders": [ + ] + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_ApiOutput", + "displayName": "", + "id": "#/definitions/84275191", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275257" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_CodeGenConfig", + "displayName": "", + "id": "#/definitions/84275192", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275261" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_ColumnOuput", + "displayName": "", + "id": "#/definitions/84275193", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275263" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_ConstOutput", + "displayName": "", + "id": "#/definitions/84275194", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275264" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_DatabaseOutput", + "displayName": "", + "id": "#/definitions/84275195", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275270" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_DbColumnOutput", + "displayName": "", + "id": "#/definitions/84275196", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275272" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_DbTableInfo", + "displayName": "", + "id": "#/definitions/84275197", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275274" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_EnumEntity", + "displayName": "", + "id": "#/definitions/84275198", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275301" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_EnumTypeOutput", + "displayName": "", + "id": "#/definitions/84275199", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275302" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_Int64", + "displayName": "", + "id": "#/definitions/84275200", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_MenuOutput", + "displayName": "", + "id": "#/definitions/84275201", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275318" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_RoleOutput", + "displayName": "", + "id": "#/definitions/84275202", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275349" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_String", + "displayName": "", + "id": "#/definitions/84275203", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysConfig", + "displayName": "", + "id": "#/definitions/84275204", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275378" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysDictData", + "displayName": "", + "id": "#/definitions/84275205", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysDictType", + "displayName": "", + "id": "#/definitions/84275206", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275380" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysFile", + "displayName": "", + "id": "#/definitions/84275207", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275381" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysJobCluster", + "displayName": "", + "id": "#/definitions/84275208", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275382" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysJobTrigger", + "displayName": "", + "id": "#/definitions/84275209", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275384" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysMenu", + "displayName": "", + "id": "#/definitions/84275210", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275390" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysNotice", + "displayName": "", + "id": "#/definitions/84275211", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275392" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysOrg", + "displayName": "", + "id": "#/definitions/84275212", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275395" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysPos", + "displayName": "", + "id": "#/definitions/84275213", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275397" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysRegion", + "displayName": "", + "id": "#/definitions/84275214", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysUser", + "displayName": "", + "id": "#/definitions/84275215", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275401" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysUserExtOrg", + "displayName": "", + "id": "#/definitions/84275216", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275402" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_TableOutput", + "displayName": "", + "id": "#/definitions/84275217", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275405" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_LoginOutput", + "displayName": "", + "id": "#/definitions/84275218", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275315" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_LoginUserOutput", + "displayName": "", + "id": "#/definitions/84275219", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275317" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_Object", + "displayName": "", + "id": "#/definitions/84275220", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "additionalProperties": false, + "description": "数据", + "type": "null" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SmKeyPairOutput", + "displayName": "", + "id": "#/definitions/84275221", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275352" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_JobDetailOutput", + "displayName": "", + "id": "#/definitions/84275222", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275353" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_OpenAccessOutput", + "displayName": "", + "id": "#/definitions/84275223", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275354" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysCodeGen", + "displayName": "", + "id": "#/definitions/84275224", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275355" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysConfig", + "displayName": "", + "id": "#/definitions/84275225", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275356" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysDictData", + "displayName": "", + "id": "#/definitions/84275226", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275357" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysDictType", + "displayName": "", + "id": "#/definitions/84275227", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275358" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysFile", + "displayName": "", + "id": "#/definitions/84275228", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275359" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysJobTriggerRecord", + "displayName": "", + "id": "#/definitions/84275229", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275360" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysLogDiff", + "displayName": "", + "id": "#/definitions/84275230", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275361" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysLogEx", + "displayName": "", + "id": "#/definitions/84275231", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275362" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysLogOp", + "displayName": "", + "id": "#/definitions/84275232", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275363" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysLogVis", + "displayName": "", + "id": "#/definitions/84275233", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275364" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysNotice", + "displayName": "", + "id": "#/definitions/84275234", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275365" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysNoticeUser", + "displayName": "", + "id": "#/definitions/84275235", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275366" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysOnlineUser", + "displayName": "", + "id": "#/definitions/84275236", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275367" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysPlugin", + "displayName": "", + "id": "#/definitions/84275237", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275368" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysPrint", + "displayName": "", + "id": "#/definitions/84275238", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275369" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysRegion", + "displayName": "", + "id": "#/definitions/84275239", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275370" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysRole", + "displayName": "", + "id": "#/definitions/84275240", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275371" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysWechatUser", + "displayName": "", + "id": "#/definitions/84275241", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275372" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_TenantOutput", + "displayName": "", + "id": "#/definitions/84275242", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275373" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_UserOutput", + "displayName": "", + "id": "#/definitions/84275243", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275374" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_String", + "displayName": "", + "id": "#/definitions/84275244", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "string", + "null" + ], + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysCodeGen", + "displayName": "", + "id": "#/definitions/84275245", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275376" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysCodeGenConfig", + "displayName": "", + "id": "#/definitions/84275246", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275377" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysConfig", + "displayName": "", + "id": "#/definitions/84275247", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275378" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysDictData", + "displayName": "", + "id": "#/definitions/84275248", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275379" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysDictType", + "displayName": "", + "id": "#/definitions/84275249", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275380" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysFile", + "displayName": "", + "id": "#/definitions/84275250", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275381" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysPrint", + "displayName": "", + "id": "#/definitions/84275251", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275398" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysUser", + "displayName": "", + "id": "#/definitions/84275252", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275401" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysWechatPay", + "displayName": "", + "id": "#/definitions/84275253", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275403" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_WechatPayOutput", + "displayName": "", + "id": "#/definitions/84275254", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275436" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_WxOpenIdOutput", + "displayName": "", + "id": "#/definitions/84275255", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275442" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_WxPhoneOutput", + "displayName": "", + "id": "#/definitions/84275256", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275443" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "ApiOutput", + "displayName": "", + "id": "#/definitions/84275257", + "description": "接口/动态API输出", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "groupName": { + "type": [ + "string", + "null" + ], + "description": "组名称" + }, + "displayName": { + "type": [ + "string", + "null" + ], + "description": "接口名称" + }, + "routeName": { + "type": [ + "string", + "null" + ], + "description": "路由名称" + } + }, + "additionalProperties": false, + "description": "接口/动态API输出", + "x-apifox-orders": [ + "groupName", + "displayName", + "routeName" + ] + } + } + }, + { + "name": "CardTypeEnum", + "displayName": "", + "id": "#/definitions/84275258", + "description": "证件类型枚举
 身份证 IdCard = 0
 护照 PassportCard = 1
 出生证 BirthCard = 2
 港澳台通行证 GatCard = 3
 外国人居留证 ForeignCard = 4
 营业执照 License = 5
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "description": "证件类型枚举
 身份证 IdCard = 0
 护照 PassportCard = 1
 出生证 BirthCard = 2
 港澳台通行证 GatCard = 3
 外国人居留证 ForeignCard = 4
 营业执照 License = 5
", + "format": "int32" + } + } + }, + { + "name": "ChangePwdInput", + "displayName": "", + "id": "#/definitions/84275259", + "description": "修改用户密码输入参数", + "schema": { + "jsonSchema": { + "required": [ + "passwordNew", + "passwordOld" + ], + "type": "object", + "properties": { + "passwordOld": { + "minLength": 1, + "type": "string", + "description": "当前密码" + }, + "passwordNew": { + "maxLength": 20, + "minLength": 5, + "type": "string", + "description": "新密码" + } + }, + "additionalProperties": false, + "description": "修改用户密码输入参数", + "x-apifox-orders": [ + "passwordOld", + "passwordNew" + ] + } + } + }, + { + "name": "ClusterStatus", + "displayName": "", + "id": "#/definitions/84275260", + "description": "
  Crashed = 0
  Working = 1
  Waiting = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "description": "
  Crashed = 0
  Working = 1
  Waiting = 2
", + "format": "int32" + } + } + }, + { + "name": "CodeGenConfig", + "displayName": "", + "id": "#/definitions/84275261", + "description": "代码生成详细配置参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "codeGenId": { + "type": "integer", + "description": "代码生成主表ID", + "format": "int64" + }, + "columnName": { + "type": [ + "string", + "null" + ], + "description": "数据库字段名" + }, + "propertyName": { + "type": [ + "string", + "null" + ], + "description": "实体属性名" + }, + "columnLength": { + "type": "integer", + "description": "字段数据长度", + "format": "int32" + }, + "lowerPropertyName": { + "type": [ + "string", + "null" + ], + "description": "数据库字段名(首字母小写)", + "readOnly": true + }, + "columnComment": { + "type": [ + "string", + "null" + ], + "description": "字段描述" + }, + "netType": { + "type": [ + "string", + "null" + ], + "description": ".NET类型" + }, + "effectType": { + "type": [ + "string", + "null" + ], + "description": "作用类型(字典)" + }, + "fkEntityName": { + "type": [ + "string", + "null" + ], + "description": "外键实体名称" + }, + "fkTableName": { + "type": [ + "string", + "null" + ], + "description": "外键表名称" + }, + "lowerFkEntityName": { + "type": [ + "string", + "null" + ], + "description": "外键实体名称(首字母小写)", + "readOnly": true + }, + "fkColumnName": { + "type": [ + "string", + "null" + ], + "description": "外键显示字段" + }, + "lowerFkColumnName": { + "type": [ + "string", + "null" + ], + "description": "外键显示字段(首字母小写)", + "readOnly": true + }, + "fkColumnNetType": { + "type": [ + "string", + "null" + ], + "description": "外键显示字段.NET类型" + }, + "dictTypeCode": { + "type": [ + "string", + "null" + ], + "description": "字典code" + }, + "whetherRetract": { + "type": [ + "string", + "null" + ], + "description": "列表是否缩进(字典)" + }, + "whetherRequired": { + "type": [ + "string", + "null" + ], + "description": "是否必填(字典)" + }, + "whetherSortable": { + "type": [ + "string", + "null" + ], + "description": "是否可排序(字典)" + }, + "queryWhether": { + "type": [ + "string", + "null" + ], + "description": "是否是查询条件" + }, + "queryType": { + "type": [ + "string", + "null" + ], + "description": "查询方式" + }, + "whetherTable": { + "type": [ + "string", + "null" + ], + "description": "列表显示" + }, + "whetherAddUpdate": { + "type": [ + "string", + "null" + ], + "description": "增改" + }, + "columnKey": { + "type": [ + "string", + "null" + ], + "description": "主外键" + }, + "dataType": { + "type": [ + "string", + "null" + ], + "description": "数据库中类型(物理类型)" + }, + "whetherCommon": { + "type": [ + "string", + "null" + ], + "description": "是否是通用字段" + }, + "tableNickName": { + "type": [ + "string", + "null" + ], + "description": "表的别名 Table as XXX", + "readOnly": true + }, + "displayColumn": { + "type": [ + "string", + "null" + ], + "description": "显示文本字段" + }, + "valueColumn": { + "type": [ + "string", + "null" + ], + "description": "选中值字段" + }, + "pidColumn": { + "type": [ + "string", + "null" + ], + "description": "父级字段" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "代码生成详细配置参数", + "x-apifox-orders": [ + "id", + "codeGenId", + "columnName", + "propertyName", + "columnLength", + "lowerPropertyName", + "columnComment", + "netType", + "effectType", + "fkEntityName", + "fkTableName", + "lowerFkEntityName", + "fkColumnName", + "lowerFkColumnName", + "fkColumnNetType", + "dictTypeCode", + "whetherRetract", + "whetherRequired", + "whetherSortable", + "queryWhether", + "queryType", + "whetherTable", + "whetherAddUpdate", + "columnKey", + "dataType", + "whetherCommon", + "tableNickName", + "displayColumn", + "valueColumn", + "pidColumn", + "orderNo" + ] + } + } + }, + { + "name": "CodeGenInput", + "displayName": "", + "id": "#/definitions/84275262", + "description": "代码生成参数类", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "authorName": { + "type": [ + "string", + "null" + ], + "description": "作者姓名" + }, + "className": { + "type": [ + "string", + "null" + ], + "description": "类名" + }, + "tablePrefix": { + "type": [ + "string", + "null" + ], + "description": "是否移除表前缀" + }, + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbName": { + "type": [ + "string", + "null" + ], + "description": "数据库名(保留字段)" + }, + "dbType": { + "type": [ + "string", + "null" + ], + "description": "数据库类型" + }, + "connectionString": { + "type": [ + "string", + "null" + ], + "description": "数据库链接" + }, + "generateType": { + "type": [ + "string", + "null" + ], + "description": "生成方式" + }, + "tableName": { + "type": [ + "string", + "null" + ], + "description": "数据库表名" + }, + "nameSpace": { + "type": [ + "string", + "null" + ], + "description": "命名空间" + }, + "busName": { + "type": [ + "string", + "null" + ], + "description": "业务名(业务代码包名称)" + }, + "tableComment": { + "type": [ + "string", + "null" + ], + "description": "功能名(数据库表名称)" + }, + "menuApplication": { + "type": [ + "string", + "null" + ], + "description": "菜单应用分类(应用编码)" + }, + "menuPid": { + "type": "integer", + "description": "菜单父级", + "format": "int64" + }, + "printType": { + "type": [ + "string", + "null" + ], + "description": "支持打印类型" + }, + "printName": { + "type": [ + "string", + "null" + ], + "description": "打印模版名称" + } + }, + "additionalProperties": false, + "description": "代码生成参数类", + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "authorName", + "className", + "tablePrefix", + "configId", + "dbName", + "dbType", + "connectionString", + "generateType", + "tableName", + "nameSpace", + "busName", + "tableComment", + "menuApplication", + "menuPid", + "printType", + "printName" + ] + } + } + }, + { + "name": "ColumnOuput", + "displayName": "", + "id": "#/definitions/84275263", + "description": "数据库表列", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "columnName": { + "type": [ + "string", + "null" + ], + "description": "字段名" + }, + "propertyName": { + "type": [ + "string", + "null" + ], + "description": "实体的Property名" + }, + "columnLength": { + "type": "integer", + "description": "字段数据长度", + "format": "int32" + }, + "dataType": { + "type": [ + "string", + "null" + ], + "description": "数据库中类型" + }, + "isPrimarykey": { + "type": "boolean", + "description": "是否为主键" + }, + "isNullable": { + "type": "boolean", + "description": "是否允许为空" + }, + "netType": { + "type": [ + "string", + "null" + ], + "description": ".NET字段类型" + }, + "columnComment": { + "type": [ + "string", + "null" + ], + "description": "字段描述" + }, + "columnKey": { + "type": [ + "string", + "null" + ], + "description": "主外键" + } + }, + "additionalProperties": false, + "description": "数据库表列", + "x-apifox-orders": [ + "columnName", + "propertyName", + "columnLength", + "dataType", + "isPrimarykey", + "isNullable", + "netType", + "columnComment", + "columnKey" + ] + } + } + }, + { + "name": "ConstOutput", + "displayName": "", + "id": "#/definitions/84275264", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "additionalProperties": false, + "description": "编码", + "type": "null" + }, + "data": { + "additionalProperties": false, + "description": "扩展字段", + "type": "null" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "name", + "code", + "data" + ] + } + } + }, + { + "name": "CreateEntityInput", + "displayName": "", + "id": "#/definitions/84275265", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "tableName": { + "type": [ + "string", + "null" + ], + "description": "表名", + "examples": [ + "student" + ] + }, + "entityName": { + "type": [ + "string", + "null" + ], + "description": "实体名", + "examples": [ + "Student" + ] + }, + "baseClassName": { + "type": [ + "string", + "null" + ], + "description": "基类名", + "examples": [ + "AutoIncrementEntity" + ] + }, + "position": { + "type": [ + "string", + "null" + ], + "description": "导出位置", + "examples": [ + "Web.Application" + ] + }, + "configId": { + "type": [ + "string", + "null" + ], + "description": "库标识" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "tableName", + "entityName", + "baseClassName", + "position", + "configId" + ] + } + } + }, + { + "name": "CreateSeedDataInput", + "displayName": "", + "id": "#/definitions/84275266", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ], + "description": "库标识" + }, + "tableName": { + "type": [ + "string", + "null" + ], + "description": "表名", + "examples": [ + "student" + ] + }, + "entityName": { + "type": [ + "string", + "null" + ], + "description": "实体名称", + "examples": [ + "Student" + ] + }, + "seedDataName": { + "type": [ + "string", + "null" + ], + "description": "种子名称", + "examples": [ + "Student" + ] + }, + "position": { + "type": [ + "string", + "null" + ], + "description": "导出位置", + "examples": [ + "Web.Application" + ] + }, + "suffix": { + "type": [ + "string", + "null" + ], + "description": "后缀", + "examples": [ + "Web.Application" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "entityName", + "seedDataName", + "position", + "suffix" + ] + } + } + }, + { + "name": "CultureLevelEnum", + "displayName": "", + "id": "#/definitions/84275267", + "description": "文化程度枚举
 其他 Level0 = 0
 小学 Level1 = 1
 初中 Level2 = 2
 普通高中 Level3 = 3
 技工学校 Level4 = 4
 职业教育 Level5 = 5
 职业高中 Level6 = 6
 中等专科 Level7 = 7
 大学专科 Level8 = 8
 大学本科 Level9 = 9
 硕士研究生 Level10 = 10
 博士研究生 Level11 = 11
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ], + "type": "integer", + "description": "文化程度枚举
 其他 Level0 = 0
 小学 Level1 = 1
 初中 Level2 = 2
 普通高中 Level3 = 3
 技工学校 Level4 = 4
 职业教育 Level5 = 5
 职业高中 Level6 = 6
 中等专科 Level7 = 7
 大学专科 Level8 = 8
 大学本科 Level9 = 9
 硕士研究生 Level10 = 10
 博士研究生 Level11 = 11
", + "format": "int32" + } + } + }, + { + "name": "DataItem", + "displayName": "", + "id": "#/definitions/84275268", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "value": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "value" + ] + } + } + }, + { + "name": "DataScopeEnum", + "displayName": "", + "id": "#/definitions/84275269", + "description": "角色数据范围枚举
 全部数据 All = 1
 本部门及以下数据 DeptChild = 2
 本部门数据 Dept = 3
 仅本人数据 Self = 4
 自定义数据 Define = 5
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "description": "角色数据范围枚举
 全部数据 All = 1
 本部门及以下数据 DeptChild = 2
 本部门数据 Dept = 3
 仅本人数据 Self = 4
 自定义数据 Define = 5
", + "format": "int32" + } + } + }, + { + "name": "DatabaseOutput", + "displayName": "", + "id": "#/definitions/84275270", + "description": "数据库", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbType": { + "$ref": "#/definitions/84275276" + }, + "connectionString": { + "type": [ + "string", + "null" + ], + "description": "数据库连接字符串" + } + }, + "additionalProperties": false, + "description": "数据库", + "x-apifox-orders": [ + "configId", + "dbType", + "connectionString" + ] + } + } + }, + { + "name": "DbColumnInput", + "displayName": "", + "id": "#/definitions/84275271", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "dbColumnName": { + "type": [ + "string", + "null" + ] + }, + "dataType": { + "type": [ + "string", + "null" + ] + }, + "length": { + "type": "integer", + "format": "int32" + }, + "columnDescription": { + "type": [ + "string", + "null" + ] + }, + "isNullable": { + "type": "integer", + "format": "int32" + }, + "isIdentity": { + "type": "integer", + "format": "int32" + }, + "isPrimarykey": { + "type": "integer", + "format": "int32" + }, + "decimalDigits": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "dbColumnName", + "dataType", + "length", + "columnDescription", + "isNullable", + "isIdentity", + "isPrimarykey", + "decimalDigits" + ] + } + } + }, + { + "name": "DbColumnOutput", + "displayName": "", + "id": "#/definitions/84275272", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "tableName": { + "type": [ + "string", + "null" + ] + }, + "tableId": { + "type": "integer", + "format": "int32" + }, + "dbColumnName": { + "type": [ + "string", + "null" + ] + }, + "propertyName": { + "type": [ + "string", + "null" + ] + }, + "dataType": { + "type": [ + "string", + "null" + ] + }, + "propertyType": { + "additionalProperties": false, + "type": "null" + }, + "length": { + "type": "integer", + "format": "int32" + }, + "columnDescription": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "isNullable": { + "type": "boolean" + }, + "isIdentity": { + "type": "boolean" + }, + "isPrimarykey": { + "type": "boolean" + }, + "value": { + "additionalProperties": false, + "type": "null" + }, + "decimalDigits": { + "type": "integer", + "format": "int32" + }, + "scale": { + "type": "integer", + "format": "int32" + }, + "isArray": { + "type": "boolean" + }, + "isJson": { + "type": "boolean" + }, + "isUnsigned": { + "type": [ + "boolean", + "null" + ] + }, + "createTableFieldSort": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "tableName", + "tableId", + "dbColumnName", + "propertyName", + "dataType", + "propertyType", + "length", + "columnDescription", + "defaultValue", + "isNullable", + "isIdentity", + "isPrimarykey", + "value", + "decimalDigits", + "scale", + "isArray", + "isJson", + "isUnsigned", + "createTableFieldSort" + ] + } + } + }, + { + "name": "DbObjectType", + "displayName": "", + "id": "#/definitions/84275273", + "description": "", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "DbTableInfo", + "displayName": "", + "id": "#/definitions/84275274", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "dbObjectType": { + "$ref": "#/definitions/84275273" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "name", + "description", + "dbObjectType" + ] + } + } + }, + { + "name": "DbTableInput", + "displayName": "", + "id": "#/definitions/84275275", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "dbColumnInfoList": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275271" + } + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "description", + "dbColumnInfoList" + ] + } + } + }, + { + "name": "DbType", + "displayName": "", + "id": "#/definitions/84275276", + "description": "", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 900 + ], + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "DeleteCodeGenInput", + "displayName": "", + "id": "#/definitions/84275277", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "代码生成器Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteConfigInput", + "displayName": "", + "id": "#/definitions/84275278", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteDbColumnInput", + "displayName": "", + "id": "#/definitions/84275279", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "dbColumnName": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "dbColumnName" + ] + } + } + }, + { + "name": "DeleteDbTableInput", + "displayName": "", + "id": "#/definitions/84275280", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName" + ] + } + } + }, + { + "name": "DeleteDictDataInput", + "displayName": "", + "id": "#/definitions/84275281", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteDictTypeInput", + "displayName": "", + "id": "#/definitions/84275282", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteFileInput", + "displayName": "", + "id": "#/definitions/84275283", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteJobDetailInput", + "displayName": "", + "id": "#/definitions/84275284", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobId" + ] + } + } + }, + { + "name": "DeleteJobTriggerInput", + "displayName": "", + "id": "#/definitions/84275285", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + }, + "triggerId": { + "type": [ + "string", + "null" + ], + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "DeleteMenuInput", + "displayName": "", + "id": "#/definitions/84275286", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteMessageTemplateInput", + "displayName": "", + "id": "#/definitions/84275287", + "description": "删除消息模板", + "schema": { + "jsonSchema": { + "required": [ + "templateId" + ], + "type": "object", + "properties": { + "templateId": { + "minLength": 1, + "type": "string", + "description": "订阅模板Id" + } + }, + "additionalProperties": false, + "description": "删除消息模板", + "x-apifox-orders": [ + "templateId" + ] + } + } + }, + { + "name": "DeleteNoticeInput", + "displayName": "", + "id": "#/definitions/84275288", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteOpenAccessInput", + "displayName": "", + "id": "#/definitions/84275289", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteOrgInput", + "displayName": "", + "id": "#/definitions/84275290", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeletePluginInput", + "displayName": "", + "id": "#/definitions/84275291", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeletePosInput", + "displayName": "", + "id": "#/definitions/84275292", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeletePrintInput", + "displayName": "", + "id": "#/definitions/84275293", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteRegionInput", + "displayName": "", + "id": "#/definitions/84275294", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteRoleInput", + "displayName": "", + "id": "#/definitions/84275295", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteTenantInput", + "displayName": "", + "id": "#/definitions/84275296", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteUserInput", + "displayName": "", + "id": "#/definitions/84275297", + "description": "删除用户输入参数", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "删除用户输入参数", + "x-apifox-orders": [ + "id", + "orgId" + ] + } + } + }, + { + "name": "DeleteWechatUserInput", + "displayName": "", + "id": "#/definitions/84275298", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DictDataInput", + "displayName": "", + "id": "#/definitions/84275299", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "DictTypeInput", + "displayName": "", + "id": "#/definitions/84275300", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "EnumEntity", + "displayName": "", + "id": "#/definitions/84275301", + "description": "枚举实体", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "describe": { + "type": [ + "string", + "null" + ], + "description": "枚举的描述" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "枚举名称" + }, + "value": { + "type": "integer", + "description": "枚举对象的值", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "枚举实体", + "x-apifox-orders": [ + "describe", + "name", + "value" + ] + } + } + }, + { + "name": "EnumTypeOutput", + "displayName": "", + "id": "#/definitions/84275302", + "description": "枚举类型输出参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "typeDescribe": { + "type": [ + "string", + "null" + ], + "description": "枚举类型描述" + }, + "typeName": { + "type": [ + "string", + "null" + ], + "description": "枚举类型名称" + }, + "typeRemark": { + "type": [ + "string", + "null" + ], + "description": "枚举类型备注" + } + }, + "additionalProperties": false, + "description": "枚举类型输出参数", + "x-apifox-orders": [ + "typeDescribe", + "typeName", + "typeRemark" + ] + } + } + }, + { + "name": "FileInput", + "displayName": "", + "id": "#/definitions/84275303", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "fileName": { + "type": [ + "string", + "null" + ], + "description": "文件名称" + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "文件Url" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "fileName", + "url" + ] + } + } + }, + { + "name": "GenAuthUrlInput", + "displayName": "", + "id": "#/definitions/84275304", + "description": "生成网页授权Url", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "redirectUrl": { + "type": [ + "string", + "null" + ], + "description": "RedirectUrl" + }, + "scope": { + "type": [ + "string", + "null" + ], + "description": "Scope" + } + }, + "additionalProperties": false, + "description": "生成网页授权Url", + "x-apifox-orders": [ + "redirectUrl", + "scope" + ] + } + } + }, + { + "name": "GenderEnum", + "displayName": "", + "id": "#/definitions/84275305", + "description": "性别枚举
 男 Male = 1
 女 Female = 2
 其他 Other = 3
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "性别枚举
 男 Male = 1
 女 Female = 2
 其他 Other = 3
", + "format": "int32" + } + } + }, + { + "name": "IActionResult", + "displayName": "", + "id": "#/definitions/84275306", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "additionalProperties": false, + "x-apifox-orders": [ + ] + } + } + }, + { + "name": "JToken", + "displayName": "", + "id": "#/definitions/84275307", + "description": "", + "schema": { + "jsonSchema": { + "type": "array", + "items": { + "$ref": "#/definitions/84275307" + } + } + } + }, + { + "name": "JobCreateTypeEnum", + "displayName": "", + "id": "#/definitions/84275308", + "description": "作业创建类型枚举
 内置 BuiltIn = 0
 脚本 Script = 1
 HTTP请求 Http = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "description": "作业创建类型枚举
 内置 BuiltIn = 0
 脚本 Script = 1
 HTTP请求 Http = 2
", + "format": "int32" + } + } + }, + { + "name": "JobDetailInput", + "displayName": "", + "id": "#/definitions/84275309", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobId" + ] + } + } + }, + { + "name": "JobDetailOutput", + "displayName": "", + "id": "#/definitions/84275310", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobDetail": { + "$ref": "#/definitions/84275383" + }, + "jobTriggers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275384" + }, + "description": "触发器集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobDetail", + "jobTriggers" + ] + } + } + }, + { + "name": "JobTriggerInput", + "displayName": "", + "id": "#/definitions/84275311", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + }, + "triggerId": { + "type": [ + "string", + "null" + ], + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "LogInput", + "displayName": "", + "id": "#/definitions/84275312", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "startTime": { + "type": [ + "string", + "null" + ], + "description": "开始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "startTime", + "endTime" + ] + } + } + }, + { + "name": "LogLevel", + "displayName": "", + "id": "#/definitions/84275313", + "description": "", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "LoginInput", + "displayName": "", + "id": "#/definitions/84275314", + "description": "用户登录参数", + "schema": { + "jsonSchema": { + "required": [ + "account", + "password" + ], + "type": "object", + "properties": { + "account": { + "minLength": 2, + "type": "string", + "description": "账号", + "examples": [ + "admin" + ] + }, + "password": { + "minLength": 3, + "type": "string", + "description": "密码", + "examples": [ + "123456" + ] + }, + "codeId": { + "type": "integer", + "description": "验证码Id", + "format": "int64" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "验证码" + } + }, + "additionalProperties": false, + "description": "用户登录参数", + "x-apifox-orders": [ + "account", + "password", + "codeId", + "code" + ] + } + } + }, + { + "name": "LoginOutput", + "displayName": "", + "id": "#/definitions/84275315", + "description": "用户登录结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "accessToken": { + "type": [ + "string", + "null" + ], + "description": "令牌Token" + }, + "refreshToken": { + "type": [ + "string", + "null" + ], + "description": "刷新Token" + } + }, + "additionalProperties": false, + "description": "用户登录结果", + "x-apifox-orders": [ + "accessToken", + "refreshToken" + ] + } + } + }, + { + "name": "LoginPhoneInput", + "displayName": "", + "id": "#/definitions/84275316", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "phone" + ], + "type": "object", + "properties": { + "phone": { + "minLength": 1, + "type": "string", + "description": "手机号码", + "examples": [ + "admin" + ] + }, + "code": { + "minLength": 4, + "type": "string", + "description": "验证码", + "examples": [ + "123456" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "phone", + "code" + ] + } + } + }, + { + "name": "LoginUserOutput", + "displayName": "", + "id": "#/definitions/84275317", + "description": "用户登录信息", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "用户id", + "format": "int64" + }, + "account": { + "type": [ + "string", + "null" + ], + "description": "账号名称" + }, + "realName": { + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + }, + "idCardNum": { + "type": [ + "string", + "null" + ], + "description": "身份证" + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "avatar": { + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "introduction": { + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "address": { + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "signature": { + "type": [ + "string", + "null" + ], + "description": "电子签名" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "orgName": { + "type": [ + "string", + "null" + ], + "description": "机构名称" + }, + "orgType": { + "type": [ + "string", + "null" + ], + "description": "机构类型" + }, + "posName": { + "type": [ + "string", + "null" + ], + "description": "职位名称" + }, + "buttons": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "description": "按钮权限集合" + }, + "roleIds": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "角色集合" + } + }, + "additionalProperties": false, + "description": "用户登录信息", + "x-apifox-orders": [ + "id", + "account", + "realName", + "phone", + "idCardNum", + "email", + "accountType", + "avatar", + "introduction", + "address", + "signature", + "orgId", + "orgName", + "orgType", + "posName", + "buttons", + "roleIds" + ] + } + } + }, + { + "name": "MenuOutput", + "displayName": "", + "id": "#/definitions/84275318", + "description": "系统菜单返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "type": { + "$ref": "#/definitions/84275319" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "路由地址" + }, + "component": { + "type": [ + "string", + "null" + ], + "description": "组件路径" + }, + "permission": { + "type": [ + "string", + "null" + ], + "description": "权限标识" + }, + "redirect": { + "type": [ + "string", + "null" + ], + "description": "重定向" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "createTime": { + "type": "string", + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": "string", + "description": "更新时间", + "format": "date-time" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "meta": { + "$ref": "#/definitions/84275391" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275318" + }, + "description": "菜单子项" + } + }, + "additionalProperties": false, + "description": "系统菜单返回结果", + "x-apifox-orders": [ + "id", + "pid", + "type", + "name", + "path", + "component", + "permission", + "redirect", + "orderNo", + "status", + "remark", + "createTime", + "updateTime", + "createUserName", + "updateUserName", + "meta", + "children" + ] + } + } + }, + { + "name": "MenuTypeEnum", + "displayName": "", + "id": "#/definitions/84275319", + "description": "系统菜单类型枚举
 目录 Dir = 1
 菜单 Menu = 2
 按钮 Btn = 3
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "系统菜单类型枚举
 目录 Dir = 1
 菜单 Menu = 2
 按钮 Btn = 3
", + "format": "int32" + } + } + }, + { + "name": "MessageInput", + "displayName": "", + "id": "#/definitions/84275320", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "description": "用户ID", + "format": "int64" + }, + "userIds": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "用户ID列表" + }, + "title": { + "type": [ + "string", + "null" + ], + "description": "消息标题" + }, + "messageType": { + "$ref": "#/definitions/84275322" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "消息内容" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "userId", + "userIds", + "title", + "messageType", + "message" + ] + } + } + }, + { + "name": "MessageTemplateSendInput", + "displayName": "", + "id": "#/definitions/84275321", + "description": "获取消息模板列表", + "schema": { + "jsonSchema": { + "required": [ + "data", + "templateId", + "toUserOpenId" + ], + "type": "object", + "properties": { + "templateId": { + "minLength": 1, + "type": "string", + "description": "订阅模板Id" + }, + "toUserOpenId": { + "minLength": 1, + "type": "string", + "description": "接收者的OpenId" + }, + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275268" + }, + "description": "模板数据,格式形如 { \"key1\": { \"value\": any }, \"key2\": { \"value\": any } }", + "x-apifox-orders": [ + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "模板跳转链接" + }, + "miniProgramPagePath": { + "type": [ + "string", + "null" + ], + "description": "所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar)" + } + }, + "additionalProperties": false, + "description": "获取消息模板列表", + "x-apifox-orders": [ + "templateId", + "toUserOpenId", + "data", + "url", + "miniProgramPagePath" + ] + } + } + }, + { + "name": "MessageTypeEnum", + "displayName": "", + "id": "#/definitions/84275322", + "description": "消息类型枚举
 消息 Info = 0
 成功 Success = 1
 警告 Warning = 2
 错误 Error = 3
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "description": "消息类型枚举
 消息 Info = 0
 成功 Success = 1
 警告 Warning = 2
 错误 Error = 3
", + "format": "int32" + } + } + }, + { + "name": "NoticeInput", + "displayName": "", + "id": "#/definitions/84275323", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "NoticeStatusEnum", + "displayName": "", + "id": "#/definitions/84275324", + "description": "通知公告状态枚举
 草稿 DRAFT = 0
 发布 PUBLIC = 1
 撤回 CANCEL = 2
 删除 DELETED = 3
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "description": "通知公告状态枚举
 草稿 DRAFT = 0
 发布 PUBLIC = 1
 撤回 CANCEL = 2
 删除 DELETED = 3
", + "format": "int32" + } + } + }, + { + "name": "NoticeTypeEnum", + "displayName": "", + "id": "#/definitions/84275325", + "description": "通知公告状类型枚举
 通知 NOTICE = 1
 公告 ANNOUNCEMENT = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "description": "通知公告状类型枚举
 通知 NOTICE = 1
 公告 ANNOUNCEMENT = 2
", + "format": "int32" + } + } + }, + { + "name": "NoticeUserStatusEnum", + "displayName": "", + "id": "#/definitions/84275326", + "description": "通知公告用户状态枚举
 未读 UNREAD = 0
 已读 READ = 1
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "通知公告用户状态枚举
 未读 UNREAD = 0
 已读 READ = 1
", + "format": "int32" + } + } + }, + { + "name": "OpenAccessInput", + "displayName": "", + "id": "#/definitions/84275327", + "description": "开放接口身份输入参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "accessKey": { + "type": [ + "string", + "null" + ], + "description": "身份标识" + } + }, + "additionalProperties": false, + "description": "开放接口身份输入参数", + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "accessKey" + ] + } + } + }, + { + "name": "OpenAccessOutput", + "displayName": "", + "id": "#/definitions/84275328", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "accessKey", + "accessSecret" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "accessKey": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "身份标识" + }, + "accessSecret": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "密钥" + }, + "bindTenantId": { + "type": "integer", + "description": "绑定租户Id", + "format": "int64" + }, + "bindUserId": { + "type": "integer", + "description": "绑定用户Id", + "format": "int64" + }, + "bindUserAccount": { + "type": [ + "string", + "null" + ], + "description": "绑定用户账号" + }, + "bindTenantName": { + "type": [ + "string", + "null" + ], + "description": "绑定租户名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "accessKey", + "accessSecret", + "bindTenantId", + "bindUserId", + "bindUserAccount", + "bindTenantName" + ] + } + } + }, + { + "name": "PageConfigInput", + "displayName": "", + "id": "#/definitions/84275329", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "groupCode": { + "type": [ + "string", + "null" + ], + "description": "分组编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code", + "groupCode" + ] + } + } + }, + { + "name": "PageDictDataInput", + "displayName": "", + "id": "#/definitions/84275330", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "dictTypeId": { + "type": "integer", + "description": "字典类型Id", + "format": "int64" + }, + "value": { + "type": [ + "string", + "null" + ], + "description": "值" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "dictTypeId", + "value", + "code" + ] + } + } + }, + { + "name": "PageDictTypeInput", + "displayName": "", + "id": "#/definitions/84275331", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code" + ] + } + } + }, + { + "name": "PageFileInput", + "displayName": "", + "id": "#/definitions/84275332", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "fileName": { + "type": [ + "string", + "null" + ], + "description": "文件名称" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "开始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "fileName", + "startTime", + "endTime" + ] + } + } + }, + { + "name": "PageJobDetailInput", + "displayName": "", + "id": "#/definitions/84275333", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "描述信息" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "jobId", + "description" + ] + } + } + }, + { + "name": "PageJobTriggerRecordInput", + "displayName": "", + "id": "#/definitions/84275334", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + }, + "triggerId": { + "type": [ + "string", + "null" + ], + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "PageLogInput", + "displayName": "", + "id": "#/definitions/84275335", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "开始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "startTime", + "endTime" + ] + } + } + }, + { + "name": "PageNoticeInput", + "displayName": "", + "id": "#/definitions/84275336", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "title": { + "type": [ + "string", + "null" + ], + "description": "标题" + }, + "type": { + "$ref": "#/definitions/84275325" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "title", + "type" + ] + } + } + }, + { + "name": "PageOnlineUserInput", + "displayName": "", + "id": "#/definitions/84275337", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "userName": { + "type": [ + "string", + "null" + ], + "description": "账号名称" + }, + "realName": { + "type": [ + "string", + "null" + ], + "description": "真实姓名" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "userName", + "realName" + ] + } + } + }, + { + "name": "PagePluginInput", + "displayName": "", + "id": "#/definitions/84275338", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code" + ] + } + } + }, + { + "name": "PagePrintInput", + "displayName": "", + "id": "#/definitions/84275339", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code" + ] + } + } + }, + { + "name": "PageRegionInput", + "displayName": "", + "id": "#/definitions/84275340", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "pid": { + "type": "integer", + "description": "父节点Id", + "format": "int64" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "pid", + "name", + "code" + ] + } + } + }, + { + "name": "PageRoleInput", + "displayName": "", + "id": "#/definitions/84275341", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code" + ] + } + } + }, + { + "name": "PageTenantInput", + "displayName": "", + "id": "#/definitions/84275342", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "phone" + ] + } + } + }, + { + "name": "PageUserInput", + "displayName": "", + "id": "#/definitions/84275343", + "description": "获取用户分页列表输入参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "account": { + "type": [ + "string", + "null" + ], + "description": "账号" + }, + "realName": { + "type": [ + "string", + "null" + ], + "description": "姓名" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "手机号" + }, + "orgId": { + "type": "integer", + "description": "查询时所选机构Id", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "获取用户分页列表输入参数", + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "account", + "realName", + "phone", + "orgId" + ] + } + } + }, + { + "name": "PlatformTypeEnum", + "displayName": "", + "id": "#/definitions/84275344", + "description": "平台类型枚举
 微信公众号 微信公众号 = 1
 微信小程序 微信小程序 = 2
 QQ QQ = 3
 支付宝 Alipay = 4
 Gitee Gitee = 5
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "description": "平台类型枚举
 微信公众号 微信公众号 = 1
 微信小程序 微信小程序 = 2
 QQ QQ = 3
 支付宝 Alipay = 4
 Gitee Gitee = 5
", + "format": "int32" + } + } + }, + { + "name": "ResetPwdUserInput", + "displayName": "", + "id": "#/definitions/84275345", + "description": "重置用户密码输入参数", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "重置用户密码输入参数", + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "RoleInput", + "displayName": "", + "id": "#/definitions/84275346", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "RoleMenuInput", + "displayName": "", + "id": "#/definitions/84275347", + "description": "授权角色菜单", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "menuIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "菜单Id集合" + } + }, + "additionalProperties": false, + "description": "授权角色菜单", + "x-apifox-orders": [ + "id", + "menuIdList" + ] + } + } + }, + { + "name": "RoleOrgInput", + "displayName": "", + "id": "#/definitions/84275348", + "description": "授权角色机构", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "dataScope": { + "type": "integer", + "description": "数据范围", + "format": "int32" + }, + "orgIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "机构Id集合" + } + }, + "additionalProperties": false, + "description": "授权角色机构", + "x-apifox-orders": [ + "id", + "dataScope", + "orgIdList" + ] + } + } + }, + { + "name": "RoleOutput", + "displayName": "", + "id": "#/definitions/84275349", + "description": "角色列表输出参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Id", + "format": "int64" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "description": "角色列表输出参数", + "x-apifox-orders": [ + "id", + "name", + "code" + ] + } + } + }, + { + "name": "SendSubscribeMessageInput", + "displayName": "", + "id": "#/definitions/84275350", + "description": "发送订阅消息", + "schema": { + "jsonSchema": { + "required": [ + "data", + "templateId", + "toUserOpenId" + ], + "type": "object", + "properties": { + "templateId": { + "minLength": 1, + "type": "string", + "description": "订阅模板Id" + }, + "toUserOpenId": { + "minLength": 1, + "type": "string", + "description": "接收者的OpenId" + }, + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275268" + }, + "description": "模板内容,格式形如 { \"key1\": { \"value\": any }, \"key2\": { \"value\": any } }", + "x-apifox-orders": [ + ] + }, + "miniprogramState": { + "type": [ + "string", + "null" + ], + "description": "跳转小程序类型" + }, + "language": { + "type": [ + "string", + "null" + ], + "description": "语言类型" + }, + "miniProgramPagePath": { + "type": [ + "string", + "null" + ], + "description": "点击模板卡片后的跳转页面(仅限本小程序内的页面),支持带参数(示例pages/app/index?foo=bar)" + } + }, + "additionalProperties": false, + "description": "发送订阅消息", + "x-apifox-orders": [ + "templateId", + "toUserOpenId", + "data", + "miniprogramState", + "language", + "miniProgramPagePath" + ] + } + } + }, + { + "name": "SignatureInput", + "displayName": "", + "id": "#/definitions/84275351", + "description": "获取配置签名", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "url": { + "type": [ + "string", + "null" + ], + "description": "Url" + } + }, + "additionalProperties": false, + "description": "获取配置签名", + "x-apifox-orders": [ + "url" + ] + } + } + }, + { + "name": "SmKeyPairOutput", + "displayName": "", + "id": "#/definitions/84275352", + "description": "国密公钥私钥对输出", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "privateKey": { + "type": [ + "string", + "null" + ], + "description": "私匙" + }, + "publicKey": { + "type": [ + "string", + "null" + ], + "description": "公匙" + } + }, + "additionalProperties": false, + "description": "国密公钥私钥对输出", + "x-apifox-orders": [ + "privateKey", + "publicKey" + ] + } + } + }, + { + "name": "SqlSugarPagedList_JobDetailOutput", + "displayName": "", + "id": "#/definitions/84275353", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275310" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_OpenAccessOutput", + "displayName": "", + "id": "#/definitions/84275354", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275328" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysCodeGen", + "displayName": "", + "id": "#/definitions/84275355", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275376" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysConfig", + "displayName": "", + "id": "#/definitions/84275356", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275378" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysDictData", + "displayName": "", + "id": "#/definitions/84275357", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysDictType", + "displayName": "", + "id": "#/definitions/84275358", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275380" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysFile", + "displayName": "", + "id": "#/definitions/84275359", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275381" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysJobTriggerRecord", + "displayName": "", + "id": "#/definitions/84275360", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275385" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysLogDiff", + "displayName": "", + "id": "#/definitions/84275361", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275386" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysLogEx", + "displayName": "", + "id": "#/definitions/84275362", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275387" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysLogOp", + "displayName": "", + "id": "#/definitions/84275363", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275388" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysLogVis", + "displayName": "", + "id": "#/definitions/84275364", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275389" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysNotice", + "displayName": "", + "id": "#/definitions/84275365", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275392" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysNoticeUser", + "displayName": "", + "id": "#/definitions/84275366", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275393" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysOnlineUser", + "displayName": "", + "id": "#/definitions/84275367", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275394" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysPlugin", + "displayName": "", + "id": "#/definitions/84275368", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275396" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysPrint", + "displayName": "", + "id": "#/definitions/84275369", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275398" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysRegion", + "displayName": "", + "id": "#/definitions/84275370", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysRole", + "displayName": "", + "id": "#/definitions/84275371", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275400" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysWechatUser", + "displayName": "", + "id": "#/definitions/84275372", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275404" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_TenantOutput", + "displayName": "", + "id": "#/definitions/84275373", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275408" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_UserOutput", + "displayName": "", + "id": "#/definitions/84275374", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275434" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "StatusEnum", + "displayName": "", + "id": "#/definitions/84275375", + "description": "通用状态枚举
 启用 Enable = 1
 停用 Disable = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "description": "通用状态枚举
 启用 Enable = 1
 停用 Disable = 2
", + "format": "int32" + } + } + }, + { + "name": "SysCodeGen", + "displayName": "", + "id": "#/definitions/84275376", + "description": "代码生成表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "authorName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "作者姓名" + }, + "tablePrefix": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否移除表前缀" + }, + "generateType": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "生成方式" + }, + "configId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库名(保留字段)" + }, + "dbType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库类型" + }, + "connectionString": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "数据库链接" + }, + "tableName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "数据库表名" + }, + "nameSpace": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "命名空间" + }, + "busName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "业务名" + }, + "menuPid": { + "type": "integer", + "description": "菜单编码", + "format": "int64" + }, + "printType": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "支持打印类型" + }, + "printName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "打印模版名称" + } + }, + "additionalProperties": false, + "description": "代码生成表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "authorName", + "tablePrefix", + "generateType", + "configId", + "dbName", + "dbType", + "connectionString", + "tableName", + "nameSpace", + "busName", + "menuPid", + "printType", + "printName" + ] + } + } + }, + { + "name": "SysCodeGenConfig", + "displayName": "", + "id": "#/definitions/84275377", + "description": "代码生成字段配置表", + "schema": { + "jsonSchema": { + "required": [ + "columnName", + "propertyName" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "codeGenId": { + "type": "integer", + "description": "代码生成主表Id", + "format": "int64" + }, + "columnName": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "数据库字段名" + }, + "propertyName": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "实体属性名" + }, + "columnLength": { + "type": "integer", + "description": "字段数据长度", + "format": "int32" + }, + "columnComment": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "字段描述" + }, + "netType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": ".NET数据类型" + }, + "effectType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "作用类型(字典)" + }, + "fkEntityName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "外键实体名称" + }, + "fkTableName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "外键表名称" + }, + "fkColumnName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "外键显示字段" + }, + "fkColumnNetType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "外键显示字段.NET类型" + }, + "dictTypeCode": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "字典编码" + }, + "whetherRetract": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "列表是否缩进(字典)" + }, + "whetherRequired": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否必填(字典)" + }, + "whetherSortable": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否可排序(字典)" + }, + "queryWhether": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否是查询条件" + }, + "queryType": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "查询方式" + }, + "whetherTable": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "列表显示" + }, + "whetherAddUpdate": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "增改" + }, + "columnKey": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "主键" + }, + "dataType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库中类型(物理类型)" + }, + "whetherCommon": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否通用字段" + }, + "displayColumn": { + "type": [ + "string", + "null" + ], + "description": "显示文本字段" + }, + "valueColumn": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "选中值字段" + }, + "pidColumn": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "父级字段" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "代码生成字段配置表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "codeGenId", + "columnName", + "propertyName", + "columnLength", + "columnComment", + "netType", + "effectType", + "fkEntityName", + "fkTableName", + "fkColumnName", + "fkColumnNetType", + "dictTypeCode", + "whetherRetract", + "whetherRequired", + "whetherSortable", + "queryWhether", + "queryType", + "whetherTable", + "whetherAddUpdate", + "columnKey", + "dataType", + "whetherCommon", + "displayColumn", + "valueColumn", + "pidColumn", + "orderNo" + ] + } + } + }, + { + "name": "SysConfig", + "displayName": "", + "id": "#/definitions/84275378", + "description": "系统参数配置表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "value": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "属性值" + }, + "sysFlag": { + "$ref": "#/definitions/84275444" + }, + "groupCode": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "分组编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "description": "系统参数配置表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "value", + "sysFlag", + "groupCode", + "orderNo", + "remark" + ] + } + } + }, + { + "name": "SysDictData", + "displayName": "", + "id": "#/definitions/84275379", + "description": "系统字典值表", + "schema": { + "jsonSchema": { + "required": [ + "code", + "value" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "dictTypeId": { + "type": "integer", + "description": "字典类型Id", + "format": "int64" + }, + "value": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "值" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "tagType": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "显示样式-标签颜色" + }, + "styleSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Style(控制显示样式)" + }, + "classSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Class(控制显示样式)" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 2048, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "extData": { + "type": [ + "string", + "null" + ], + "description": "拓展数据(保存业务功能的配置项)" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "description": "系统字典值表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "dictTypeId", + "value", + "code", + "tagType", + "styleSetting", + "classSetting", + "orderNo", + "remark", + "extData", + "status" + ] + } + } + }, + { + "name": "SysDictType", + "displayName": "", + "id": "#/definitions/84275380", + "description": "系统字典类型表", + "schema": { + "jsonSchema": { + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "字典值集合" + } + }, + "additionalProperties": false, + "description": "系统字典类型表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "orderNo", + "remark", + "status", + "children" + ] + } + } + }, + { + "name": "SysFile", + "displayName": "", + "id": "#/definitions/84275381", + "description": "系统文件表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "provider": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "提供者" + }, + "bucketName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "仓储名称" + }, + "fileName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "文件名称(源文件名)" + }, + "suffix": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "文件后缀" + }, + "filePath": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "存储路径" + }, + "sizeKb": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "文件大小KB" + }, + "sizeInfo": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "文件大小信息-计算后的" + }, + "url": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "外链地址-OSS上传后生成外链地址方便前端预览" + }, + "fileMd5": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "文件MD5" + } + }, + "additionalProperties": false, + "description": "系统文件表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "provider", + "bucketName", + "fileName", + "suffix", + "filePath", + "sizeKb", + "sizeInfo", + "url", + "fileMd5" + ] + } + } + }, + { + "name": "SysJobCluster", + "displayName": "", + "id": "#/definitions/84275382", + "description": "系统作业集群表", + "schema": { + "jsonSchema": { + "required": [ + "clusterId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "clusterId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "作业集群Id" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "status": { + "$ref": "#/definitions/84275260" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "系统作业集群表", + "x-apifox-orders": [ + "id", + "clusterId", + "description", + "status", + "updatedTime" + ] + } + } + }, + { + "name": "SysJobDetail", + "displayName": "", + "id": "#/definitions/84275383", + "description": "系统作业信息表", + "schema": { + "jsonSchema": { + "required": [ + "jobId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "jobId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "作业Id" + }, + "groupName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组名称" + }, + "jobType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "作业类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "concurrent": { + "type": "boolean", + "description": "是否并行执行" + }, + "includeAnnotations": { + "type": "boolean", + "description": "是否扫描特性触发器" + }, + "properties": { + "type": [ + "string", + "null" + ], + "description": "额外数据" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createType": { + "$ref": "#/definitions/84275308" + }, + "scriptCode": { + "type": [ + "string", + "null" + ], + "description": "脚本代码" + } + }, + "additionalProperties": false, + "description": "系统作业信息表", + "x-apifox-orders": [ + "id", + "jobId", + "groupName", + "jobType", + "assemblyName", + "description", + "concurrent", + "includeAnnotations", + "properties", + "updatedTime", + "createType", + "scriptCode" + ] + } + } + }, + { + "name": "SysJobTrigger", + "displayName": "", + "id": "#/definitions/84275384", + "description": "系统作业触发器表", + "schema": { + "jsonSchema": { + "required": [ + "jobId", + "triggerId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "triggerId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "触发器Id" + }, + "jobId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "作业Id" + }, + "triggerType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "触发器类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "args": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "参数" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "status": { + "$ref": "#/definitions/84275411" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "起始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + }, + "lastRunTime": { + "type": [ + "string", + "null" + ], + "description": "最近运行时间", + "format": "date-time" + }, + "nextRunTime": { + "type": [ + "string", + "null" + ], + "description": "下一次运行时间", + "format": "date-time" + }, + "numberOfRuns": { + "type": "integer", + "description": "触发次数", + "format": "int64" + }, + "maxNumberOfRuns": { + "type": "integer", + "description": "最大触发次数(0:不限制,n:N次)", + "format": "int64" + }, + "numberOfErrors": { + "type": "integer", + "description": "出错次数", + "format": "int64" + }, + "maxNumberOfErrors": { + "type": "integer", + "description": "最大出错次数(0:不限制,n:N次)", + "format": "int64" + }, + "numRetries": { + "type": "integer", + "description": "重试次数", + "format": "int32" + }, + "retryTimeout": { + "type": "integer", + "description": "重试间隔时间(ms)", + "format": "int32" + }, + "startNow": { + "type": "boolean", + "description": "是否立即启动" + }, + "runOnStart": { + "type": "boolean", + "description": "是否启动时执行一次" + }, + "resetOnlyOnce": { + "type": "boolean", + "description": "是否在启动时重置最大触发次数等于一次的作业" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "系统作业触发器表", + "x-apifox-orders": [ + "id", + "triggerId", + "jobId", + "triggerType", + "assemblyName", + "args", + "description", + "status", + "startTime", + "endTime", + "lastRunTime", + "nextRunTime", + "numberOfRuns", + "maxNumberOfRuns", + "numberOfErrors", + "maxNumberOfErrors", + "numRetries", + "retryTimeout", + "startNow", + "runOnStart", + "resetOnlyOnce", + "updatedTime" + ] + } + } + }, + { + "name": "SysJobTriggerRecord", + "displayName": "", + "id": "#/definitions/84275385", + "description": "系统作业触发器运行记录表", + "schema": { + "jsonSchema": { + "required": [ + "jobId", + "triggerId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "jobId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "作业Id" + }, + "triggerId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "触发器Id" + }, + "numberOfRuns": { + "type": "integer", + "description": "当前运行次数", + "format": "int64" + }, + "lastRunTime": { + "type": [ + "string", + "null" + ], + "description": "最近运行时间", + "format": "date-time" + }, + "nextRunTime": { + "type": [ + "string", + "null" + ], + "description": "下一次运行时间", + "format": "date-time" + }, + "status": { + "$ref": "#/definitions/84275411" + }, + "result": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "本次执行结果" + }, + "elapsedTime": { + "type": "integer", + "description": "本次执行耗时", + "format": "int64" + }, + "createdTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "系统作业触发器运行记录表", + "x-apifox-orders": [ + "id", + "jobId", + "triggerId", + "numberOfRuns", + "lastRunTime", + "nextRunTime", + "status", + "result", + "elapsedTime", + "createdTime" + ] + } + } + }, + { + "name": "SysLogDiff", + "displayName": "", + "id": "#/definitions/84275386", + "description": "系统差异日志表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "beforeData": { + "type": [ + "string", + "null" + ], + "description": "操作前记录" + }, + "afterData": { + "type": [ + "string", + "null" + ], + "description": "操作后记录" + }, + "sql": { + "type": [ + "string", + "null" + ], + "description": "Sql" + }, + "parameters": { + "type": [ + "string", + "null" + ], + "description": "参数 手动传入的参数" + }, + "businessData": { + "type": [ + "string", + "null" + ], + "description": "业务对象" + }, + "diffType": { + "type": [ + "string", + "null" + ], + "description": "差异操作" + }, + "elapsed": { + "type": [ + "integer", + "null" + ], + "description": "耗时", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "系统差异日志表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "beforeData", + "afterData", + "sql", + "parameters", + "businessData", + "diffType", + "elapsed" + ] + } + } + }, + { + "name": "SysLogEx", + "displayName": "", + "id": "#/definitions/84275387", + "description": "系统异常日志表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "controllerName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "模块名称" + }, + "actionName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "方法名称" + }, + "displayTitle": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "显示名称" + }, + "status": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "执行状态" + }, + "remoteIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "IP地址" + }, + "location": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "登录地点" + }, + "longitude": { + "type": [ + "number", + "null" + ], + "description": "经度", + "format": "double" + }, + "latitude": { + "type": [ + "number", + "null" + ], + "description": "维度", + "format": "double" + }, + "browser": { + "maxLength": 1024, + "type": [ + "string", + "null" + ], + "description": "浏览器" + }, + "os": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "操作系统" + }, + "elapsed": { + "type": [ + "integer", + "null" + ], + "description": "操作用时", + "format": "int64" + }, + "logDateTime": { + "type": [ + "string", + "null" + ], + "description": "日志时间", + "format": "date-time" + }, + "logLevel": { + "$ref": "#/definitions/84275313" + }, + "account": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "httpMethod": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "请求方式" + }, + "requestUrl": { + "type": [ + "string", + "null" + ], + "description": "请求地址" + }, + "requestParam": { + "type": [ + "string", + "null" + ], + "description": "请求参数" + }, + "returnResult": { + "type": [ + "string", + "null" + ], + "description": "返回结果" + }, + "eventId": { + "type": [ + "integer", + "null" + ], + "description": "事件Id", + "format": "int32" + }, + "threadId": { + "type": [ + "integer", + "null" + ], + "description": "线程Id", + "format": "int32" + }, + "traceId": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "请求跟踪Id" + }, + "exception": { + "type": [ + "string", + "null" + ], + "description": "异常信息" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "日志消息Json" + } + }, + "additionalProperties": false, + "description": "系统异常日志表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "controllerName", + "actionName", + "displayTitle", + "status", + "remoteIp", + "location", + "longitude", + "latitude", + "browser", + "os", + "elapsed", + "logDateTime", + "logLevel", + "account", + "realName", + "httpMethod", + "requestUrl", + "requestParam", + "returnResult", + "eventId", + "threadId", + "traceId", + "exception", + "message" + ] + } + } + }, + { + "name": "SysLogOp", + "displayName": "", + "id": "#/definitions/84275388", + "description": "系统操作日志表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "controllerName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "模块名称" + }, + "actionName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "方法名称" + }, + "displayTitle": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "显示名称" + }, + "status": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "执行状态" + }, + "remoteIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "IP地址" + }, + "location": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "登录地点" + }, + "longitude": { + "type": [ + "number", + "null" + ], + "description": "经度", + "format": "double" + }, + "latitude": { + "type": [ + "number", + "null" + ], + "description": "维度", + "format": "double" + }, + "browser": { + "maxLength": 1024, + "type": [ + "string", + "null" + ], + "description": "浏览器" + }, + "os": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "操作系统" + }, + "elapsed": { + "type": [ + "integer", + "null" + ], + "description": "操作用时", + "format": "int64" + }, + "logDateTime": { + "type": [ + "string", + "null" + ], + "description": "日志时间", + "format": "date-time" + }, + "logLevel": { + "$ref": "#/definitions/84275313" + }, + "account": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "httpMethod": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "请求方式" + }, + "requestUrl": { + "type": [ + "string", + "null" + ], + "description": "请求地址" + }, + "requestParam": { + "type": [ + "string", + "null" + ], + "description": "请求参数" + }, + "returnResult": { + "type": [ + "string", + "null" + ], + "description": "返回结果" + }, + "eventId": { + "type": [ + "integer", + "null" + ], + "description": "事件Id", + "format": "int32" + }, + "threadId": { + "type": [ + "integer", + "null" + ], + "description": "线程Id", + "format": "int32" + }, + "traceId": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "请求跟踪Id" + }, + "exception": { + "type": [ + "string", + "null" + ], + "description": "异常信息" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "日志消息Json" + } + }, + "additionalProperties": false, + "description": "系统操作日志表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "controllerName", + "actionName", + "displayTitle", + "status", + "remoteIp", + "location", + "longitude", + "latitude", + "browser", + "os", + "elapsed", + "logDateTime", + "logLevel", + "account", + "realName", + "httpMethod", + "requestUrl", + "requestParam", + "returnResult", + "eventId", + "threadId", + "traceId", + "exception", + "message" + ] + } + } + }, + { + "name": "SysLogVis", + "displayName": "", + "id": "#/definitions/84275389", + "description": "系统访问日志表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "controllerName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "模块名称" + }, + "actionName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "方法名称" + }, + "displayTitle": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "显示名称" + }, + "status": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "执行状态" + }, + "remoteIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "IP地址" + }, + "location": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "登录地点" + }, + "longitude": { + "type": [ + "number", + "null" + ], + "description": "经度", + "format": "double" + }, + "latitude": { + "type": [ + "number", + "null" + ], + "description": "维度", + "format": "double" + }, + "browser": { + "maxLength": 1024, + "type": [ + "string", + "null" + ], + "description": "浏览器" + }, + "os": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "操作系统" + }, + "elapsed": { + "type": [ + "integer", + "null" + ], + "description": "操作用时", + "format": "int64" + }, + "logDateTime": { + "type": [ + "string", + "null" + ], + "description": "日志时间", + "format": "date-time" + }, + "logLevel": { + "$ref": "#/definitions/84275313" + }, + "account": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + } + }, + "additionalProperties": false, + "description": "系统访问日志表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "controllerName", + "actionName", + "displayTitle", + "status", + "remoteIp", + "location", + "longitude", + "latitude", + "browser", + "os", + "elapsed", + "logDateTime", + "logLevel", + "account", + "realName" + ] + } + } + }, + { + "name": "SysMenu", + "displayName": "", + "id": "#/definitions/84275390", + "description": "系统菜单表", + "schema": { + "jsonSchema": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "type": { + "$ref": "#/definitions/84275319" + }, + "name": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "路由名称" + }, + "path": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "路由地址" + }, + "component": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组件路径" + }, + "redirect": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "重定向" + }, + "permission": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "权限标识" + }, + "title": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "菜单名称" + }, + "icon": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "图标" + }, + "isIframe": { + "type": "boolean", + "description": "是否内嵌" + }, + "outLink": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "外链链接" + }, + "isHide": { + "type": "boolean", + "description": "是否隐藏" + }, + "isKeepAlive": { + "type": "boolean", + "description": "是否缓存" + }, + "isAffix": { + "type": "boolean", + "description": "是否固定" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275390" + }, + "description": "菜单子项" + } + }, + "additionalProperties": false, + "description": "系统菜单表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "pid", + "type", + "name", + "path", + "component", + "redirect", + "permission", + "title", + "icon", + "isIframe", + "outLink", + "isHide", + "isKeepAlive", + "isAffix", + "orderNo", + "status", + "remark", + "children" + ] + } + } + }, + { + "name": "SysMenuMeta", + "displayName": "", + "id": "#/definitions/84275391", + "description": "菜单Meta配置", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "title": { + "type": [ + "string", + "null" + ], + "description": "标题" + }, + "icon": { + "type": [ + "string", + "null" + ], + "description": "图标" + }, + "isIframe": { + "type": "boolean", + "description": "是否内嵌" + }, + "isLink": { + "type": [ + "string", + "null" + ], + "description": "外链链接" + }, + "isHide": { + "type": "boolean", + "description": "是否隐藏" + }, + "isKeepAlive": { + "type": "boolean", + "description": "是否缓存" + }, + "isAffix": { + "type": "boolean", + "description": "是否固定" + } + }, + "additionalProperties": false, + "description": "菜单Meta配置", + "x-apifox-orders": [ + "title", + "icon", + "isIframe", + "isLink", + "isHide", + "isKeepAlive", + "isAffix" + ] + } + } + }, + { + "name": "SysNotice", + "displayName": "", + "id": "#/definitions/84275392", + "description": "系统通知公告表", + "schema": { + "jsonSchema": { + "required": [ + "content", + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "title": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "标题" + }, + "content": { + "minLength": 1, + "type": "string", + "description": "内容" + }, + "type": { + "$ref": "#/definitions/84275325" + }, + "publicUserId": { + "type": "integer", + "description": "发布人Id", + "format": "int64" + }, + "publicUserName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "发布人姓名" + }, + "publicOrgId": { + "type": "integer", + "description": "发布机构Id", + "format": "int64" + }, + "publicOrgName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "发布机构名称" + }, + "publicTime": { + "type": [ + "string", + "null" + ], + "description": "发布时间", + "format": "date-time" + }, + "cancelTime": { + "type": [ + "string", + "null" + ], + "description": "撤回时间", + "format": "date-time" + }, + "status": { + "$ref": "#/definitions/84275324" + } + }, + "additionalProperties": false, + "description": "系统通知公告表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "title", + "content", + "type", + "publicUserId", + "publicUserName", + "publicOrgId", + "publicOrgName", + "publicTime", + "cancelTime", + "status" + ] + } + } + }, + { + "name": "SysNoticeUser", + "displayName": "", + "id": "#/definitions/84275393", + "description": "系统通知公告用户表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "noticeId": { + "type": "integer", + "description": "通知公告Id", + "format": "int64" + }, + "sysNotice": { + "$ref": "#/definitions/84275392" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "readTime": { + "type": [ + "string", + "null" + ], + "description": "阅读时间", + "format": "date-time" + }, + "readStatus": { + "$ref": "#/definitions/84275326" + } + }, + "additionalProperties": false, + "description": "系统通知公告用户表", + "x-apifox-orders": [ + "id", + "noticeId", + "sysNotice", + "userId", + "readTime", + "readStatus" + ] + } + } + }, + { + "name": "SysOnlineUser", + "displayName": "", + "id": "#/definitions/84275394", + "description": "系统在线用户表", + "schema": { + "jsonSchema": { + "required": [ + "userName" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "connectionId": { + "type": [ + "string", + "null" + ], + "description": "连接Id" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "userName": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "time": { + "type": [ + "string", + "null" + ], + "description": "连接时间", + "format": "date-time" + }, + "ip": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "连接IP" + }, + "browser": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "浏览器" + }, + "os": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "操作系统" + } + }, + "additionalProperties": false, + "description": "系统在线用户表", + "x-apifox-orders": [ + "id", + "tenantId", + "connectionId", + "userId", + "userName", + "realName", + "time", + "ip", + "browser", + "os" + ] + } + } + }, + { + "name": "SysOrg", + "displayName": "", + "id": "#/definitions/84275395", + "description": "系统机构表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "level": { + "type": [ + "integer", + "null" + ], + "description": "级别", + "format": "int32" + }, + "type": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "机构类型-数据字典" + }, + "directorId": { + "type": [ + "integer", + "null" + ], + "description": "负责人Id", + "format": "int64" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275395" + }, + "description": "机构子项" + }, + "disabled": { + "type": "boolean", + "description": "是否禁止选中" + } + }, + "additionalProperties": false, + "description": "系统机构表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "pid", + "name", + "code", + "level", + "type", + "directorId", + "orderNo", + "status", + "remark", + "children", + "disabled" + ] + } + } + }, + { + "name": "SysPlugin", + "displayName": "", + "id": "#/definitions/84275396", + "description": "系统动态插件表", + "schema": { + "jsonSchema": { + "required": [ + "csharpCode", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "csharpCode": { + "minLength": 1, + "type": "string", + "description": "C#代码" + }, + "assemblyName": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "程序集名称" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "description": "系统动态插件表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "name", + "csharpCode", + "assemblyName", + "orderNo", + "status", + "remark" + ] + } + } + }, + { + "name": "SysPos", + "displayName": "", + "id": "#/definitions/84275397", + "description": "系统职位表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "description": "系统职位表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "name", + "code", + "orderNo", + "remark", + "status" + ] + } + } + }, + { + "name": "SysPrint", + "displayName": "", + "id": "#/definitions/84275398", + "description": "系统打印模板表", + "schema": { + "jsonSchema": { + "required": [ + "name", + "template" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "template": { + "minLength": 1, + "type": "string", + "description": "打印模板" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "description": "系统打印模板表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "name", + "template", + "orderNo", + "status", + "remark" + ] + } + } + }, + { + "name": "SysRegion", + "displayName": "", + "id": "#/definitions/84275399", + "description": "系统行政地区表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "shortName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "简称" + }, + "mergerName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "组合名" + }, + "code": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "行政代码" + }, + "zipCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "邮政编码" + }, + "cityCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "区号" + }, + "level": { + "type": "integer", + "description": "层级", + "format": "int32" + }, + "pinYin": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "拼音" + }, + "lng": { + "type": "number", + "description": "经度", + "format": "float" + }, + "lat": { + "type": "number", + "description": "维度", + "format": "float" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "机构子项" + } + }, + "additionalProperties": false, + "description": "系统行政地区表", + "x-apifox-orders": [ + "id", + "pid", + "name", + "shortName", + "mergerName", + "code", + "zipCode", + "cityCode", + "level", + "pinYin", + "lng", + "lat", + "orderNo", + "remark", + "children" + ] + } + } + }, + { + "name": "SysRole", + "displayName": "", + "id": "#/definitions/84275400", + "description": "系统角色表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "dataScope": { + "$ref": "#/definitions/84275269" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "description": "系统角色表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "name", + "code", + "orderNo", + "dataScope", + "remark", + "status" + ] + } + } + }, + { + "name": "SysUser", + "displayName": "", + "id": "#/definitions/84275401", + "description": "系统用户表", + "schema": { + "jsonSchema": { + "required": [ + "account" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "account": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "nickName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "sex": { + "$ref": "#/definitions/84275305" + }, + "age": { + "type": "integer", + "description": "年龄", + "format": "int32" + }, + "birthday": { + "type": [ + "string", + "null" + ], + "description": "出生日期", + "format": "date-time" + }, + "nation": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "民族" + }, + "phone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "cardType": { + "$ref": "#/definitions/84275258" + }, + "idCardNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "身份证号" + }, + "email": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "address": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "cultureLevel": { + "$ref": "#/definitions/84275267" + }, + "politicalOutlook": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "政治面貌" + }, + "college": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "毕业院校" + }, + "officePhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "办公电话" + }, + "emergencyContact": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "紧急联系人" + }, + "emergencyPhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "紧急联系人电话" + }, + "emergencyAddress": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "紧急联系人地址" + }, + "introduction": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "orgId": { + "type": "integer", + "description": "直属机构Id", + "format": "int64" + }, + "sysOrg": { + "$ref": "#/definitions/84275395" + }, + "managerUserId": { + "type": [ + "integer", + "null" + ], + "description": "直属主管Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "posTitle": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职称" + }, + "expertise": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "擅长领域" + }, + "officeZone": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公区域" + }, + "office": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公室" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + }, + "lastLoginIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "最新登录Ip" + }, + "lastLoginAddress": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录地点" + }, + "lastLoginTime": { + "type": [ + "string", + "null" + ], + "description": "最新登录时间", + "format": "date-time" + }, + "lastLoginDevice": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录设备" + }, + "signature": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "电子签名" + } + }, + "additionalProperties": false, + "description": "系统用户表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "account", + "realName", + "nickName", + "avatar", + "sex", + "age", + "birthday", + "nation", + "phone", + "cardType", + "idCardNum", + "email", + "address", + "cultureLevel", + "politicalOutlook", + "college", + "officePhone", + "emergencyContact", + "emergencyPhone", + "emergencyAddress", + "introduction", + "orderNo", + "status", + "remark", + "accountType", + "orgId", + "sysOrg", + "managerUserId", + "posId", + "jobNum", + "posLevel", + "posTitle", + "expertise", + "officeZone", + "office", + "joinDate", + "lastLoginIp", + "lastLoginAddress", + "lastLoginTime", + "lastLoginDevice", + "signature" + ] + } + } + }, + { + "name": "SysUserExtOrg", + "displayName": "", + "id": "#/definitions/84275402", + "description": "系统用户扩展机构表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "系统用户扩展机构表", + "x-apifox-orders": [ + "id", + "userId", + "orgId", + "posId", + "jobNum", + "posLevel", + "joinDate" + ] + } + } + }, + { + "name": "SysWechatPay", + "displayName": "", + "id": "#/definitions/84275403", + "description": "系统微信支付表", + "schema": { + "jsonSchema": { + "required": [ + "appId", + "merchantId", + "outTradeNumber", + "transactionId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "merchantId": { + "minLength": 1, + "type": "string", + "description": "微信商户号" + }, + "appId": { + "minLength": 1, + "type": "string", + "description": "服务商AppId" + }, + "outTradeNumber": { + "minLength": 1, + "type": "string", + "description": "商户订单号" + }, + "transactionId": { + "minLength": 1, + "type": "string", + "description": "支付订单号" + }, + "tradeType": { + "type": [ + "string", + "null" + ], + "description": "交易类型" + }, + "tradeState": { + "type": [ + "string", + "null" + ], + "description": "交易状态" + }, + "tradeStateDescription": { + "type": [ + "string", + "null" + ], + "description": "交易状态描述" + }, + "bankType": { + "type": [ + "string", + "null" + ], + "description": "付款银行类型" + }, + "total": { + "type": "integer", + "description": "订单总金额", + "format": "int32" + }, + "payerTotal": { + "type": [ + "integer", + "null" + ], + "description": "用户支付金额", + "format": "int32" + }, + "successTime": { + "type": [ + "string", + "null" + ], + "description": "支付完成时间", + "format": "date-time" + }, + "expireTime": { + "type": [ + "string", + "null" + ], + "description": "交易结束时间", + "format": "date-time" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "商品描述" + }, + "scene": { + "type": [ + "string", + "null" + ], + "description": "场景信息" + }, + "attachment": { + "type": [ + "string", + "null" + ], + "description": "附加数据" + }, + "goodsTag": { + "type": [ + "string", + "null" + ], + "description": "优惠标记" + }, + "settlement": { + "type": [ + "string", + "null" + ], + "description": "结算信息" + }, + "notifyUrl": { + "type": [ + "string", + "null" + ], + "description": "回调通知地址" + }, + "remark": { + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "openId": { + "type": [ + "string", + "null" + ], + "description": "微信OpenId标识" + }, + "subMerchantId": { + "type": [ + "string", + "null" + ], + "description": "子商户号" + }, + "subAppId": { + "type": [ + "string", + "null" + ], + "description": "子商户AppId" + }, + "subOpenId": { + "type": [ + "string", + "null" + ], + "description": "子商户唯一标识" + } + }, + "additionalProperties": false, + "description": "系统微信支付表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "merchantId", + "appId", + "outTradeNumber", + "transactionId", + "tradeType", + "tradeState", + "tradeStateDescription", + "bankType", + "total", + "payerTotal", + "successTime", + "expireTime", + "description", + "scene", + "attachment", + "goodsTag", + "settlement", + "notifyUrl", + "remark", + "openId", + "subMerchantId", + "subAppId", + "subOpenId" + ] + } + } + }, + { + "name": "SysWechatUser", + "displayName": "", + "id": "#/definitions/84275404", + "description": "系统微信用户表", + "schema": { + "jsonSchema": { + "required": [ + "openId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "userId": { + "type": "integer", + "description": "系统用户Id", + "format": "int64" + }, + "platformType": { + "$ref": "#/definitions/84275344" + }, + "openId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "OpenId" + }, + "sessionKey": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "会话密钥" + }, + "unionId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "UnionId" + }, + "nickName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "mobile": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "sex": { + "type": [ + "integer", + "null" + ], + "description": "性别", + "format": "int32" + }, + "language": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "语言" + }, + "city": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "城市" + }, + "province": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "省" + }, + "country": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "国家" + }, + "accessToken": { + "type": [ + "string", + "null" + ], + "description": "AccessToken" + }, + "refreshToken": { + "type": [ + "string", + "null" + ], + "description": "RefreshToken" + }, + "expiresIn": { + "type": [ + "integer", + "null" + ], + "description": "过期时间", + "format": "int32" + }, + "scope": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "用户授权的作用域,使用逗号分隔" + } + }, + "additionalProperties": false, + "description": "系统微信用户表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "userId", + "platformType", + "openId", + "sessionKey", + "unionId", + "nickName", + "avatar", + "mobile", + "sex", + "language", + "city", + "province", + "country", + "accessToken", + "refreshToken", + "expiresIn", + "scope" + ] + } + } + }, + { + "name": "TableOutput", + "displayName": "", + "id": "#/definitions/84275405", + "description": "数据库表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "tableName": { + "type": [ + "string", + "null" + ], + "description": "表名(字母形式的)" + }, + "entityName": { + "type": [ + "string", + "null" + ], + "description": "实体名称" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间" + }, + "tableComment": { + "type": [ + "string", + "null" + ], + "description": "表名称描述(功能名)" + } + }, + "additionalProperties": false, + "description": "数据库表", + "x-apifox-orders": [ + "configId", + "tableName", + "entityName", + "createTime", + "updateTime", + "tableComment" + ] + } + } + }, + { + "name": "TenantIdInput", + "displayName": "", + "id": "#/definitions/84275406", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "tenantId": { + "type": "integer", + "description": "租户Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "tenantId" + ] + } + } + }, + { + "name": "TenantInput", + "displayName": "", + "id": "#/definitions/84275407", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "TenantOutput", + "displayName": "", + "id": "#/definitions/84275408", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "host": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "主机" + }, + "tenantType": { + "$ref": "#/definitions/84275409" + }, + "dbType": { + "$ref": "#/definitions/84275276" + }, + "connection": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "数据库连接" + }, + "configId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库标识" + }, + "slaveConnections": { + "type": [ + "string", + "null" + ], + "description": "从库连接/读写分离" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "租户名称" + }, + "adminAccount": { + "type": [ + "string", + "null" + ], + "description": "管理员账号" + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "电子邮箱" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "userId", + "orgId", + "host", + "tenantType", + "dbType", + "connection", + "configId", + "slaveConnections", + "orderNo", + "remark", + "status", + "name", + "adminAccount", + "email", + "phone" + ] + } + } + }, + { + "name": "TenantTypeEnum", + "displayName": "", + "id": "#/definitions/84275409", + "description": "租户类型枚举
 Id隔离 Id = 0
 库隔离 Db = 1
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "租户类型枚举
 Id隔离 Id = 0
 库隔离 Db = 1
", + "format": "int32" + } + } + }, + { + "name": "TenantUserInput", + "displayName": "", + "id": "#/definitions/84275410", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "userId" + ] + } + } + }, + { + "name": "TriggerStatus", + "displayName": "", + "id": "#/definitions/84275411", + "description": "
  Backlog = 0
  Ready = 1
  Running = 2
  Pause = 3
  Blocked = 4
  ErrorToReady = 5
  Archived = 6
  Panic = 7
  Overrun = 8
  Unoccupied = 9
  NotStart = 10
  Unknown = 11
  Unhandled = 12
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "type": "integer", + "description": "
  Backlog = 0
  Ready = 1
  Running = 2
  Pause = 3
  Blocked = 4
  ErrorToReady = 5
  Archived = 6
  Panic = 7
  Overrun = 8
  Unoccupied = 9
  NotStart = 10
  Unknown = 11
  Unhandled = 12
", + "format": "int32" + } + } + }, + { + "name": "UnlockLoginInput", + "displayName": "", + "id": "#/definitions/84275412", + "description": "解除登录锁定输入参数", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "解除登录锁定输入参数", + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "UpdateCodeGenInput", + "displayName": "", + "id": "#/definitions/84275413", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "authorName": { + "type": [ + "string", + "null" + ], + "description": "作者姓名" + }, + "className": { + "type": [ + "string", + "null" + ], + "description": "类名" + }, + "tablePrefix": { + "type": [ + "string", + "null" + ], + "description": "是否移除表前缀" + }, + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbName": { + "type": [ + "string", + "null" + ], + "description": "数据库名(保留字段)" + }, + "dbType": { + "type": [ + "string", + "null" + ], + "description": "数据库类型" + }, + "connectionString": { + "type": [ + "string", + "null" + ], + "description": "数据库链接" + }, + "generateType": { + "type": [ + "string", + "null" + ], + "description": "生成方式" + }, + "tableName": { + "type": [ + "string", + "null" + ], + "description": "数据库表名" + }, + "nameSpace": { + "type": [ + "string", + "null" + ], + "description": "命名空间" + }, + "busName": { + "type": [ + "string", + "null" + ], + "description": "业务名(业务代码包名称)" + }, + "tableComment": { + "type": [ + "string", + "null" + ], + "description": "功能名(数据库表名称)" + }, + "menuApplication": { + "type": [ + "string", + "null" + ], + "description": "菜单应用分类(应用编码)" + }, + "menuPid": { + "type": "integer", + "description": "菜单父级", + "format": "int64" + }, + "printType": { + "type": [ + "string", + "null" + ], + "description": "支持打印类型" + }, + "printName": { + "type": [ + "string", + "null" + ], + "description": "打印模版名称" + }, + "id": { + "type": "integer", + "description": "代码生成器Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "authorName", + "className", + "tablePrefix", + "configId", + "dbName", + "dbType", + "connectionString", + "generateType", + "tableName", + "nameSpace", + "busName", + "tableComment", + "menuApplication", + "menuPid", + "printType", + "printName", + "id" + ] + } + } + }, + { + "name": "UpdateConfigInput", + "displayName": "", + "id": "#/definitions/84275414", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "value": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "属性值" + }, + "sysFlag": { + "$ref": "#/definitions/84275444" + }, + "groupCode": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "分组编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "value", + "sysFlag", + "groupCode", + "orderNo", + "remark" + ] + } + } + }, + { + "name": "UpdateDbColumnInput", + "displayName": "", + "id": "#/definitions/84275415", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "columnName": { + "type": [ + "string", + "null" + ] + }, + "oldColumnName": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "columnName", + "oldColumnName", + "description" + ] + } + } + }, + { + "name": "UpdateDbTableInput", + "displayName": "", + "id": "#/definitions/84275416", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "oldTableName": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "oldTableName", + "description" + ] + } + } + }, + { + "name": "UpdateDictDataInput", + "displayName": "", + "id": "#/definitions/84275417", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "value" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "dictTypeId": { + "type": "integer", + "description": "字典类型Id", + "format": "int64" + }, + "value": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "值" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "tagType": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "显示样式-标签颜色" + }, + "styleSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Style(控制显示样式)" + }, + "classSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Class(控制显示样式)" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 2048, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "extData": { + "type": [ + "string", + "null" + ], + "description": "拓展数据(保存业务功能的配置项)" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "dictTypeId", + "value", + "code", + "tagType", + "styleSetting", + "classSetting", + "orderNo", + "remark", + "extData", + "status" + ] + } + } + }, + { + "name": "UpdateDictTypeInput", + "displayName": "", + "id": "#/definitions/84275418", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "字典值集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "orderNo", + "remark", + "status", + "children" + ] + } + } + }, + { + "name": "UpdateJobDetailInput", + "displayName": "", + "id": "#/definitions/84275419", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "jobId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "groupName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组名称" + }, + "jobType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "作业类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "concurrent": { + "type": "boolean", + "description": "是否并行执行" + }, + "includeAnnotations": { + "type": "boolean", + "description": "是否扫描特性触发器" + }, + "properties": { + "type": [ + "string", + "null" + ], + "description": "额外数据" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createType": { + "$ref": "#/definitions/84275308" + }, + "scriptCode": { + "type": [ + "string", + "null" + ], + "description": "脚本代码" + }, + "jobId": { + "minLength": 2, + "type": "string", + "description": "作业Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "groupName", + "jobType", + "assemblyName", + "description", + "concurrent", + "includeAnnotations", + "properties", + "updatedTime", + "createType", + "scriptCode", + "jobId" + ] + } + } + }, + { + "name": "UpdateJobTriggerInput", + "displayName": "", + "id": "#/definitions/84275420", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "jobId", + "triggerId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "triggerType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "触发器类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "args": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "参数" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "status": { + "$ref": "#/definitions/84275411" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "起始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + }, + "lastRunTime": { + "type": [ + "string", + "null" + ], + "description": "最近运行时间", + "format": "date-time" + }, + "nextRunTime": { + "type": [ + "string", + "null" + ], + "description": "下一次运行时间", + "format": "date-time" + }, + "numberOfRuns": { + "type": "integer", + "description": "触发次数", + "format": "int64" + }, + "maxNumberOfRuns": { + "type": "integer", + "description": "最大触发次数(0:不限制,n:N次)", + "format": "int64" + }, + "numberOfErrors": { + "type": "integer", + "description": "出错次数", + "format": "int64" + }, + "maxNumberOfErrors": { + "type": "integer", + "description": "最大出错次数(0:不限制,n:N次)", + "format": "int64" + }, + "numRetries": { + "type": "integer", + "description": "重试次数", + "format": "int32" + }, + "retryTimeout": { + "type": "integer", + "description": "重试间隔时间(ms)", + "format": "int32" + }, + "startNow": { + "type": "boolean", + "description": "是否立即启动" + }, + "runOnStart": { + "type": "boolean", + "description": "是否启动时执行一次" + }, + "resetOnlyOnce": { + "type": "boolean", + "description": "是否在启动时重置最大触发次数等于一次的作业" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "jobId": { + "minLength": 2, + "type": "string", + "description": "作业Id" + }, + "triggerId": { + "minLength": 2, + "type": "string", + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "triggerType", + "assemblyName", + "args", + "description", + "status", + "startTime", + "endTime", + "lastRunTime", + "nextRunTime", + "numberOfRuns", + "maxNumberOfRuns", + "numberOfErrors", + "maxNumberOfErrors", + "numRetries", + "retryTimeout", + "startNow", + "runOnStart", + "resetOnlyOnce", + "updatedTime", + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "UpdateMenuInput", + "displayName": "", + "id": "#/definitions/84275421", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "type": { + "$ref": "#/definitions/84275319" + }, + "name": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "路由名称" + }, + "path": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "路由地址" + }, + "component": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组件路径" + }, + "redirect": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "重定向" + }, + "permission": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "权限标识" + }, + "icon": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "图标" + }, + "isIframe": { + "type": "boolean", + "description": "是否内嵌" + }, + "outLink": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "外链链接" + }, + "isHide": { + "type": "boolean", + "description": "是否隐藏" + }, + "isKeepAlive": { + "type": "boolean", + "description": "是否缓存" + }, + "isAffix": { + "type": "boolean", + "description": "是否固定" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275390" + }, + "description": "菜单子项" + }, + "title": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "pid", + "type", + "name", + "path", + "component", + "redirect", + "permission", + "icon", + "isIframe", + "outLink", + "isHide", + "isKeepAlive", + "isAffix", + "orderNo", + "status", + "remark", + "children", + "title" + ] + } + } + }, + { + "name": "UpdateNoticeInput", + "displayName": "", + "id": "#/definitions/84275422", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "content", + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "title": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "标题" + }, + "content": { + "minLength": 1, + "type": "string", + "description": "内容" + }, + "type": { + "$ref": "#/definitions/84275325" + }, + "publicUserId": { + "type": "integer", + "description": "发布人Id", + "format": "int64" + }, + "publicUserName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "发布人姓名" + }, + "publicOrgId": { + "type": "integer", + "description": "发布机构Id", + "format": "int64" + }, + "publicOrgName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "发布机构名称" + }, + "publicTime": { + "type": [ + "string", + "null" + ], + "description": "发布时间", + "format": "date-time" + }, + "cancelTime": { + "type": [ + "string", + "null" + ], + "description": "撤回时间", + "format": "date-time" + }, + "status": { + "$ref": "#/definitions/84275324" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "title", + "content", + "type", + "publicUserId", + "publicUserName", + "publicOrgId", + "publicOrgName", + "publicTime", + "cancelTime", + "status" + ] + } + } + }, + { + "name": "UpdateOpenAccessInput", + "displayName": "", + "id": "#/definitions/84275423", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "accessKey", + "accessSecret", + "bindUserId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "bindTenantId": { + "type": "integer", + "description": "绑定租户Id", + "format": "int64" + }, + "accessKey": { + "minLength": 1, + "type": "string", + "description": "身份标识" + }, + "accessSecret": { + "minLength": 1, + "type": "string", + "description": "密钥" + }, + "bindUserId": { + "type": "integer", + "description": "绑定用户Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "bindTenantId", + "accessKey", + "accessSecret", + "bindUserId" + ] + } + } + }, + { + "name": "UpdateOrgInput", + "displayName": "", + "id": "#/definitions/84275424", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "level": { + "type": [ + "integer", + "null" + ], + "description": "级别", + "format": "int32" + }, + "type": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "机构类型-数据字典" + }, + "directorId": { + "type": [ + "integer", + "null" + ], + "description": "负责人Id", + "format": "int64" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275395" + }, + "description": "机构子项" + }, + "disabled": { + "type": "boolean", + "description": "是否禁止选中" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "pid", + "code", + "level", + "type", + "directorId", + "orderNo", + "status", + "remark", + "children", + "disabled", + "name" + ] + } + } + }, + { + "name": "UpdatePluginInput", + "displayName": "", + "id": "#/definitions/84275425", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "csharpCode", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "csharpCode": { + "minLength": 1, + "type": "string", + "description": "C#代码" + }, + "assemblyName": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "程序集名称" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "csharpCode", + "assemblyName", + "orderNo", + "status", + "remark", + "name" + ] + } + } + }, + { + "name": "UpdatePosInput", + "displayName": "", + "id": "#/definitions/84275426", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "code", + "orderNo", + "remark", + "status", + "name" + ] + } + } + }, + { + "name": "UpdatePrintInput", + "displayName": "", + "id": "#/definitions/84275427", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name", + "template" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "template": { + "minLength": 1, + "type": "string", + "description": "打印模板" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "template", + "orderNo", + "status", + "remark", + "name" + ] + } + } + }, + { + "name": "UpdateRegionInput", + "displayName": "", + "id": "#/definitions/84275428", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "shortName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "简称" + }, + "mergerName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "组合名" + }, + "code": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "行政代码" + }, + "zipCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "邮政编码" + }, + "cityCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "区号" + }, + "level": { + "type": "integer", + "description": "层级", + "format": "int32" + }, + "pinYin": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "拼音" + }, + "lng": { + "type": "number", + "description": "经度", + "format": "float" + }, + "lat": { + "type": "number", + "description": "维度", + "format": "float" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "机构子项" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "pid", + "shortName", + "mergerName", + "code", + "zipCode", + "cityCode", + "level", + "pinYin", + "lng", + "lat", + "orderNo", + "remark", + "children", + "name" + ] + } + } + }, + { + "name": "UpdateRoleInput", + "displayName": "", + "id": "#/definitions/84275429", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "dataScope": { + "$ref": "#/definitions/84275269" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + }, + "menuIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "菜单Id集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "code", + "orderNo", + "dataScope", + "remark", + "status", + "name", + "menuIdList" + ] + } + } + }, + { + "name": "UpdateTenantInput", + "displayName": "", + "id": "#/definitions/84275430", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "adminAccount", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "host": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "主机" + }, + "tenantType": { + "$ref": "#/definitions/84275409" + }, + "dbType": { + "$ref": "#/definitions/84275276" + }, + "connection": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "数据库连接" + }, + "configId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库标识" + }, + "slaveConnections": { + "type": [ + "string", + "null" + ], + "description": "从库连接/读写分离" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "电子邮箱" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + }, + "name": { + "minLength": 2, + "type": "string", + "description": "租户名称" + }, + "adminAccount": { + "minLength": 3, + "type": "string", + "description": "租管账号" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "userId", + "orgId", + "host", + "tenantType", + "dbType", + "connection", + "configId", + "slaveConnections", + "orderNo", + "remark", + "status", + "email", + "phone", + "name", + "adminAccount" + ] + } + } + }, + { + "name": "UpdateUserInput", + "displayName": "", + "id": "#/definitions/84275431", + "description": "更新用户输入参数", + "schema": { + "jsonSchema": { + "required": [ + "account", + "realName" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "nickName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "sex": { + "$ref": "#/definitions/84275305" + }, + "age": { + "type": "integer", + "description": "年龄", + "format": "int32" + }, + "birthday": { + "type": [ + "string", + "null" + ], + "description": "出生日期", + "format": "date-time" + }, + "nation": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "民族" + }, + "phone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "cardType": { + "$ref": "#/definitions/84275258" + }, + "idCardNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "身份证号" + }, + "email": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "address": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "cultureLevel": { + "$ref": "#/definitions/84275267" + }, + "politicalOutlook": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "政治面貌" + }, + "college": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "毕业院校" + }, + "officePhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "办公电话" + }, + "emergencyContact": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "紧急联系人" + }, + "emergencyPhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "紧急联系人电话" + }, + "emergencyAddress": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "紧急联系人地址" + }, + "introduction": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "orgId": { + "type": "integer", + "description": "直属机构Id", + "format": "int64" + }, + "sysOrg": { + "$ref": "#/definitions/84275395" + }, + "managerUserId": { + "type": [ + "integer", + "null" + ], + "description": "直属主管Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "posTitle": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职称" + }, + "expertise": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "擅长领域" + }, + "officeZone": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公区域" + }, + "office": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公室" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + }, + "lastLoginIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "最新登录Ip" + }, + "lastLoginAddress": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录地点" + }, + "lastLoginTime": { + "type": [ + "string", + "null" + ], + "description": "最新登录时间", + "format": "date-time" + }, + "lastLoginDevice": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录设备" + }, + "signature": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "电子签名" + }, + "account": { + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "minLength": 1, + "type": "string", + "description": "真实姓名" + }, + "roleIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "角色集合" + }, + "extOrgIdList": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275402" + }, + "description": "扩展机构集合" + } + }, + "additionalProperties": false, + "description": "更新用户输入参数", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "nickName", + "avatar", + "sex", + "age", + "birthday", + "nation", + "phone", + "cardType", + "idCardNum", + "email", + "address", + "cultureLevel", + "politicalOutlook", + "college", + "officePhone", + "emergencyContact", + "emergencyPhone", + "emergencyAddress", + "introduction", + "orderNo", + "status", + "remark", + "accountType", + "orgId", + "sysOrg", + "managerUserId", + "posId", + "jobNum", + "posLevel", + "posTitle", + "expertise", + "officeZone", + "office", + "joinDate", + "lastLoginIp", + "lastLoginAddress", + "lastLoginTime", + "lastLoginDevice", + "signature", + "account", + "realName", + "roleIdList", + "extOrgIdList" + ] + } + } + }, + { + "name": "UploadFileFromBase64Input", + "displayName": "", + "id": "#/definitions/84275432", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "fileDataBase64": { + "type": [ + "string", + "null" + ], + "description": "文件内容" + }, + "contentType": { + "type": [ + "string", + "null" + ], + "description": "文件类型( \"image/jpeg\",)" + }, + "fileName": { + "type": [ + "string", + "null" + ], + "description": "文件名称" + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "保存路径" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "fileDataBase64", + "contentType", + "fileName", + "path" + ] + } + } + }, + { + "name": "UserInput", + "displayName": "", + "id": "#/definitions/84275433", + "description": "设置用户状态输入参数", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "description": "设置用户状态输入参数", + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "UserOutput", + "displayName": "", + "id": "#/definitions/84275434", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "account" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "account": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "nickName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "sex": { + "$ref": "#/definitions/84275305" + }, + "age": { + "type": "integer", + "description": "年龄", + "format": "int32" + }, + "birthday": { + "type": [ + "string", + "null" + ], + "description": "出生日期", + "format": "date-time" + }, + "nation": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "民族" + }, + "phone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "cardType": { + "$ref": "#/definitions/84275258" + }, + "idCardNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "身份证号" + }, + "email": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "address": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "cultureLevel": { + "$ref": "#/definitions/84275267" + }, + "politicalOutlook": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "政治面貌" + }, + "college": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "毕业院校" + }, + "officePhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "办公电话" + }, + "emergencyContact": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "紧急联系人" + }, + "emergencyPhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "紧急联系人电话" + }, + "emergencyAddress": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "紧急联系人地址" + }, + "introduction": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "orgId": { + "type": "integer", + "description": "直属机构Id", + "format": "int64" + }, + "sysOrg": { + "$ref": "#/definitions/84275395" + }, + "managerUserId": { + "type": [ + "integer", + "null" + ], + "description": "直属主管Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "posTitle": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职称" + }, + "expertise": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "擅长领域" + }, + "officeZone": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公区域" + }, + "office": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公室" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + }, + "lastLoginIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "最新登录Ip" + }, + "lastLoginAddress": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录地点" + }, + "lastLoginTime": { + "type": [ + "string", + "null" + ], + "description": "最新登录时间", + "format": "date-time" + }, + "lastLoginDevice": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录设备" + }, + "signature": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "电子签名" + }, + "orgName": { + "type": [ + "string", + "null" + ], + "description": "机构名称" + }, + "posName": { + "type": [ + "string", + "null" + ], + "description": "职位名称" + }, + "roleName": { + "type": [ + "string", + "null" + ], + "description": "角色名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "account", + "realName", + "nickName", + "avatar", + "sex", + "age", + "birthday", + "nation", + "phone", + "cardType", + "idCardNum", + "email", + "address", + "cultureLevel", + "politicalOutlook", + "college", + "officePhone", + "emergencyContact", + "emergencyPhone", + "emergencyAddress", + "introduction", + "orderNo", + "status", + "remark", + "accountType", + "orgId", + "sysOrg", + "managerUserId", + "posId", + "jobNum", + "posLevel", + "posTitle", + "expertise", + "officeZone", + "office", + "joinDate", + "lastLoginIp", + "lastLoginAddress", + "lastLoginTime", + "lastLoginDevice", + "signature", + "orgName", + "posName", + "roleName" + ] + } + } + }, + { + "name": "UserRoleInput", + "displayName": "", + "id": "#/definitions/84275435", + "description": "授权用户角色", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "roleIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "角色Id集合" + } + }, + "additionalProperties": false, + "description": "授权用户角色", + "x-apifox-orders": [ + "userId", + "roleIdList" + ] + } + } + }, + { + "name": "WechatPayOutput", + "displayName": "", + "id": "#/definitions/84275436", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "openId": { + "type": [ + "string", + "null" + ], + "description": "OpenId" + }, + "total": { + "type": "integer", + "description": "订单金额", + "format": "int32" + }, + "attachment": { + "type": [ + "string", + "null" + ], + "description": "附加数据" + }, + "goodsTag": { + "type": [ + "string", + "null" + ], + "description": "优惠标记" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "openId", + "total", + "attachment", + "goodsTag" + ] + } + } + }, + { + "name": "WechatPayParaInput", + "displayName": "", + "id": "#/definitions/84275437", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "prepayId": { + "type": [ + "string", + "null" + ], + "description": "订单Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "prepayId" + ] + } + } + }, + { + "name": "WechatPayTransactionInput", + "displayName": "", + "id": "#/definitions/84275438", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "openId": { + "type": [ + "string", + "null" + ], + "description": "OpenId" + }, + "total": { + "type": "integer", + "description": "订单金额", + "format": "int32" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "商品描述" + }, + "attachment": { + "type": [ + "string", + "null" + ], + "description": "附加数据" + }, + "goodsTag": { + "type": [ + "string", + "null" + ], + "description": "优惠标记" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "openId", + "total", + "description", + "attachment", + "goodsTag" + ] + } + } + }, + { + "name": "WechatUserInput", + "displayName": "", + "id": "#/definitions/84275439", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "nickName": { + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "phoneNumber": { + "type": [ + "string", + "null" + ], + "description": "手机号码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "nickName", + "phoneNumber" + ] + } + } + }, + { + "name": "WechatUserLogin", + "displayName": "", + "id": "#/definitions/84275440", + "description": "微信用户登录", + "schema": { + "jsonSchema": { + "required": [ + "openId" + ], + "type": "object", + "properties": { + "openId": { + "minLength": 10, + "type": "string", + "description": "OpenId" + } + }, + "additionalProperties": false, + "description": "微信用户登录", + "x-apifox-orders": [ + "openId" + ] + } + } + }, + { + "name": "WxOpenIdLoginInput", + "displayName": "", + "id": "#/definitions/84275441", + "description": "微信小程序登录", + "schema": { + "jsonSchema": { + "required": [ + "openId" + ], + "type": "object", + "properties": { + "openId": { + "minLength": 10, + "type": "string", + "description": "OpenId" + } + }, + "additionalProperties": false, + "description": "微信小程序登录", + "x-apifox-orders": [ + "openId" + ] + } + } + }, + { + "name": "WxOpenIdOutput", + "displayName": "", + "id": "#/definitions/84275442", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "openId": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "openId" + ] + } + } + }, + { + "name": "WxPhoneOutput", + "displayName": "", + "id": "#/definitions/84275443", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "phoneNumber": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "phoneNumber" + ] + } + } + }, + { + "name": "YesNoEnum", + "displayName": "", + "id": "#/definitions/84275444", + "description": "是否枚举
 是 Y = 1
 否 N = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "description": "是否枚举
 是 Y = 1
 否 N = 2
", + "format": "int32" + } + } + } + ] + } + ] + } + ], + "responseCollection": [ + { + "id": 4088603, + "createdAt": "2024-02-29T09:40:10.000Z", + "updatedAt": "2024-02-29T09:40:10.000Z", + "deletedAt": null, + "name": "根目录", + "type": "root", + "description": "", + "children": [ + ], + "auth": { + }, + "projectId": 4084018, + "projectBranchId": 0, + "parentId": 0, + "createdById": 1145276, + "updatedById": 1145276, + "items": [ + ] + } + ], + "environments": [ + { + "name": "开发环境", + "parameters": { + "cookie": [ + ], + "query": [ + ], + "header": [ + ], + "body": [ + ] + }, + "variables": [ + ], + "type": "normal", + "visibility": "protected", + "ordering": 0, + "tags": [ + { + "name": "", + "color": "#9373EE" + } + ], + "id": "18499638", + "baseUrl": "http://localhost:5005", + "baseUrls": { + "default": "http://localhost:5005" + } + } + ], + "commonScripts": [ + ], + "globalVariables": [ + ], + "commonParameters": null, + "projectSetting": { + "id": "4084141", + "auth": { + "type": "bearer", + "bearer": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec" + } + }, + "servers": [ + { + "id": "default", + "name": "默认服务" + } + ], + "gateway": [ + ], + "language": "zh-CN", + "apiStatuses": [ + "developing", + "testing", + "released", + "deprecated" + ], + "mockSettings": { + }, + "preProcessors": [ + ], + "postProcessors": [ + ], + "advancedSettings": { + "responseValidate": false, + "enableJsonc": true, + "isDefaultUrlEncoding": 2, + "enableBigint": false, + "preferredHttpVersion": { + }, + "enableTestScenarioSetting": false, + "enableYAPICompatScript": false + }, + "initialDisabledMockIds": [ + ], + "cloudMock": { + "security": "free", + "enable": false, + "tokenKey": "apifoxToken" + } + }, + "projectAssociations": [ + ] +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Alipay/AlipayErrorCodes.cs b/Admin.NET.Core/Service/Alipay/AlipayErrorCodes.cs new file mode 100644 index 0000000..5affa51 --- /dev/null +++ b/Admin.NET.Core/Service/Alipay/AlipayErrorCodes.cs @@ -0,0 +1,143 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 支付宝支付错误码 +/// +public class AlipayErrorCode +{ + /// + /// 错误代码 + /// + public string Code { get; private set; } + + /// + /// 错误消息 + /// + public string Message { get; private set; } + + /// + /// 解决方案 + /// + public string Solution { get; private set; } + + /// + /// 错误码集 + /// + private static readonly List 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" } + ]; + + /// + /// 根据错误码获取错误信息 + /// + /// + /// + public static AlipayErrorCode Get(string code) + { + return StatusCodes.FirstOrDefault(u => u.Code.EqualIgnoreCase(code)); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Alipay/Dto/AlipayInput.cs b/Admin.NET.Core/Service/Alipay/Dto/AlipayInput.cs new file mode 100644 index 0000000..c70d45e --- /dev/null +++ b/Admin.NET.Core/Service/Alipay/Dto/AlipayInput.cs @@ -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 +{ + /// + /// 用户ID + /// + public long UserId { get; set; } + + /// + /// 商户AppId + /// + public string AppId { get; set; } + + /// + /// 商家订单号 + /// + public string OutBizNo { get; set; } + + /// + /// 转账金额 + /// + public decimal TransAmount { get; set; } + + /// + /// 业务标题 + /// + public string OrderTitle { get; set; } + + /// + /// 备注 + /// + public string Remark { get; set; } + + /// + /// 是否展示付款方别名 + /// + public bool PayerShowNameUseAlias { get; set; } + + /// + /// 收款方证件类型 + /// + public AlipayCertTypeEnum? CertType { get; set; } + + /// + /// 收款方证件号码,条件必填 + /// + public string CertNo { get; set; } + + /// + /// 收款方身份标识 + /// + public string Identity { get; set; } + + /// + /// 收款方真实姓名 + /// + public string Name { get; set; } + + /// + /// 收款方身份标识类型 + /// + public AlipayIdentityTypeEnum? IdentityType { get; set; } +} + +/// +/// 统一收单下单并支付页面接口输入参数 +/// +public class AlipayTradePagePayInput +{ + /// + /// 商户订单号 + /// + [Required(ErrorMessage = "商户订单号不能为空")] + public string OutTradeNo { get; set; } + + /// + /// 订单总金额 + /// + [Required(ErrorMessage = "订单总金额不能为空")] + public string TotalAmount { get; set; } + + /// + /// 订单标题 + /// + [Required(ErrorMessage = "订单标题不能为空")] + public string Subject { get; set; } + + /// + /// + /// + public string Body { get; set; } + + /// + /// 超时时间 + /// + public string TimeoutExpress { get; set; } + + /// + /// 二维码宽度 + /// + [Required(ErrorMessage = "二维码宽度不能为空")] + public int? QrcodeWidth { get; set; } + + /// + /// 业务参数 + /// + public ExtendParams ExtendParams { get; set; } + + /// + /// 商户业务数据 + /// + public Dictionary BusinessParams { get; set; } + + /// + /// 开票信息 + /// + public InvoiceInfo InvoiceInfo { get; set; } + + /// + /// 外部买家信息 + /// + public ExtUserInfo ExtUserInfo { get; set; } +} + +public class AlipayPreCreateInput +{ + /// + /// 商户订单号 + /// + [Required(ErrorMessage = "商户订单号不能为空")] + public string OutTradeNo { get; set; } + + /// + /// 订单总金额 + /// + [Required(ErrorMessage = "订单总金额不能为空")] + public string TotalAmount { get; set; } + + /// + /// 订单标题 + /// + [Required(ErrorMessage = "订单标题不能为空")] + public string Subject { get; set; } + + /// + /// 超时时间 + /// + public string TimeoutExpress { get; set; } +} + +public class AlipayAuthInfoInput +{ + /// + /// 用户Id + /// + + [JsonProperty("user_id")] + [JsonPropertyName("user_id")] + [FromQuery(Name = "user_id")] + public string UserId { get; set; } + + /// + /// 授权码 + /// + [JsonProperty("auth_code")] + [JsonPropertyName("auth_code")] + [FromQuery(Name = "auth_code")] + public string AuthCode { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Alipay/IAlipayNotify.cs b/Admin.NET.Core/Service/Alipay/IAlipayNotify.cs new file mode 100644 index 0000000..90ded5f --- /dev/null +++ b/Admin.NET.Core/Service/Alipay/IAlipayNotify.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Aop.Api.Response; + +namespace Admin.NET.Core.Service; + +/// +/// 支付宝回调接口 +/// +public abstract class IAlipayNotify +{ + /// + /// 充值回调方法 + /// + /// 交易类型 + /// 交易id + public abstract bool TopUpCallback(long type, long tradeNo); + + /// + /// 扫码回调 + /// + /// + /// + /// + /// + public abstract bool ScanCallback(long type, long userId, AlipayUserInfoShareResponse response); +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Alipay/SysAlipayService.cs b/Admin.NET.Core/Service/Alipay/SysAlipayService.cs new file mode 100644 index 0000000..8d727a0 --- /dev/null +++ b/Admin.NET.Core/Service/Alipay/SysAlipayService.cs @@ -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; + +/// +/// 支付宝支付服务 🧩 +/// +[ApiDescriptionSettings(Order = 240)] +public class SysAlipayService : IDynamicApiController, ITransient +{ + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly SysConfigService _sysConfigService; + private readonly List _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) + { + _db = db; + _httpContext = httpContext; + _sysConfigService = sysConfigService; + _option = alipayOptions.Value; + _webHostEnvironment = webHostEnvironment; + + // 初始化支付宝客户端列表 + _alipayClientList = []; + foreach (var account in _option.AccountList) _alipayClientList.Add(_option.GetClient(account)); + } + + /// + /// 获取授权信息 🔖 + /// + /// + /// + [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().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(ConfigConst.AlipayAuthPageUrl + type).Result; + //return new RedirectResult(authPageUrl); + return new RedirectResult(_option.AppAuthUrl + "/index.html"); + } + + /// + /// 支付回调 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("支付回调")] + [ApiDescriptionSettings(Name = "Notify"), HttpPost] + public string Notify() + { + SortedDictionary 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().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"; + } + + /// + /// 统一收单下单并支付页面接口 🔖 + /// + /// + /// + [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}"; + } + + /// + /// 交易预创建 🔖 + /// + /// + /// + [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; + } + + /// + /// 单笔转账到支付宝账户 + /// https://opendocs.alipay.com/open/62987723_alipay.fund.trans.uni.transfer + /// + [NonAction] + public async Task 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; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs b/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs new file mode 100644 index 0000000..c807604 --- /dev/null +++ b/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs @@ -0,0 +1,120 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 用户登录参数 +/// +public class LoginInput +{ + /// + /// 账号 + /// + /// admin + [Required(ErrorMessage = "账号不能为空"), MinLength(2, ErrorMessage = "账号不能少于2个字符")] + public string Account { get; set; } + + /// + /// 密码 + /// + /// 123456 + [Required(ErrorMessage = "密码不能为空"), MinLength(3, ErrorMessage = "密码不能少于3个字符")] + public string Password { get; set; } + + /// + /// 租户 + /// + public long? TenantId { get; set; } + + /// + /// 验证码Id + /// + public long CodeId { get; set; } + + /// + /// 验证码 + /// + public string Code { get; set; } +} + +public class LoginPhoneInput +{ + /// + /// 手机号码 + /// + /// admin + [Required(ErrorMessage = "手机号码不能为空")] + [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")] + public string Phone { get; set; } + + /// + /// 验证码 + /// + /// 123456 + [Required(ErrorMessage = "验证码不能为空"), MinLength(4, ErrorMessage = "验证码不能少于4个字符")] + public string Code { get; set; } + + /// + /// 租户 + /// + [Required(ErrorMessage = "租户不能为空")] + public long? TenantId { get; set; } +} + +/// +/// 用户注册输入参数 +/// +public class UserRegistrationInput +{ + /// + /// 真实姓名 + /// + [Required(ErrorMessage = "真实姓名不能为空"), MinLength(2, ErrorMessage = "真实姓名不能少于2个字符")] + public string RealName { get; set; } + + /// + /// 账号 + /// + [Required(ErrorMessage = "账号不能为空"), MinLength(6, ErrorMessage = "账号不能少于6个字符")] + public string Account { get; set; } + + /// + /// 手机号码 + /// + /// admin + [Required(ErrorMessage = "手机号码不能为空")] + [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")] + public string Phone { get; set; } + + /// + /// 验证码 + /// + /// 123456 + [Required(ErrorMessage = "验证码不能为空")] + public string Code { get; set; } + + /// + /// 验证码Id + /// + public long CodeId { get; set; } + + /// + /// 租户 + /// + [Required(ErrorMessage = "租户不能为空")] + public long TenantId { get; set; } + + /// + /// 密码 + /// + public string Password { get; set; } + + /// + /// 注册方案 + /// + public long WayId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Auth/Dto/LoginOutput.cs b/Admin.NET.Core/Service/Auth/Dto/LoginOutput.cs new file mode 100644 index 0000000..651165d --- /dev/null +++ b/Admin.NET.Core/Service/Auth/Dto/LoginOutput.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 用户登录结果 +/// +public class LoginOutput +{ + /// + /// 令牌Token + /// + public string AccessToken { get; set; } + + /// + /// 刷新Token + /// + public string RefreshToken { get; set; } + + /// + /// 个性化首页 + /// + public string? Homepage { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Auth/Dto/LoginUserOutput.cs b/Admin.NET.Core/Service/Auth/Dto/LoginUserOutput.cs new file mode 100644 index 0000000..a4ff9c6 --- /dev/null +++ b/Admin.NET.Core/Service/Auth/Dto/LoginUserOutput.cs @@ -0,0 +1,118 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 用户登录信息 +/// +public class LoginUserOutput +{ + /// + /// 用户id + /// + public long Id { get; set; } + + /// + /// 账号名称 + /// + public string Account { get; set; } + + /// + /// 真实姓名 + /// + public string RealName { get; set; } + + /// + /// 电话 + /// + public string Phone { get; set; } + + /// + /// 身份证 + /// + public string IdCardNum { get; set; } + + /// + /// 邮箱 + /// + public string Email { get; set; } + + /// + /// 账号类型 + /// + public AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser; + + /// + /// 头像 + /// + public string Avatar { get; set; } + + /// + /// 个人简介 + /// + public string Introduction { get; set; } + + /// + /// 地址 + /// + public string Address { get; set; } + + /// + /// 电子签名 + /// + public string Signature { get; set; } + + /// + /// 机构Id + /// + public long OrgId { get; set; } + + /// + /// 机构名称 + /// + public string OrgName { get; set; } + + /// + /// 机构类型 + /// + public string OrgType { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } + + /// + /// 按钮权限集合 + /// + public List Buttons { get; set; } + + /// + /// 角色集合 + /// + public List RoleIds { get; set; } + + /// + /// 水印文字 + /// + public string WatermarkText { get; set; } + + /// + /// 租户Id + /// + public long? TenantId { get; set; } + + /// + /// 当前切换到的租户Id + /// + public long? CurrentTenantId { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; internal set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Auth/Dto/SysLdapInput.cs b/Admin.NET.Core/Service/Auth/Dto/SysLdapInput.cs new file mode 100644 index 0000000..cb5f8bc --- /dev/null +++ b/Admin.NET.Core/Service/Auth/Dto/SysLdapInput.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统域登录信息配置输入参数 +/// +public class SysLdapInput : BasePageInput +{ + /// + /// 主机 + /// + public string? Host { get; set; } + + /// + /// 租户Id + /// + 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 +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Auth/SysAuthService.cs b/Admin.NET.Core/Service/Auth/SysAuthService.cs new file mode 100644 index 0000000..4157e38 --- /dev/null +++ b/Admin.NET.Core/Service/Auth/SysAuthService.cs @@ -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; + +/// +/// 系统登录授权服务 🧩 +/// +[ApiDescriptionSettings(Order = 500)] +public class SysAuthService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _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 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; + } + + /// + /// 账号密码登录 🔖 + /// + /// + /// 用户名/密码:superadmin/123456 + /// + [AllowAnonymous] + [DisplayName("账号密码登录")] + public virtual async Task Login([Required] LoginInput input) + { + // 判断密码错误次数(缓存30分钟) + var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{input.Account}"; + var passwordErrorTimes = _sysCacheService.Get(keyPasswordErrorTimes); + var passwordMaxErrorTimes = await _sysConfigService.GetConfigValue(ConfigConst.SysPasswordMaxErrorTimes); + // 若未配置或误配置为0、负数, 则默认密码错误次数最大为5次 + if (passwordMaxErrorTimes < 1) passwordMaxErrorTimes = 5; + if (passwordErrorTimes > passwordMaxErrorTimes) throw Oops.Oh(ErrorCodeEnum.D1027); + + // 判断是否开启验证码,其校验验证码 + if (await _sysConfigService.GetConfigValue(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(ConfigConst.SysDomainLogin)) + { + var userLdap = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == tenant.Id); + if (userLdap == null) + { + VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user); + } + else if (!await App.GetRequiredService().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); + } + + /// + /// 获取登录租户和用户 + /// + /// + /// + /// + /// + [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>().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); + } + + /// + /// 验证用户密码 + /// + /// + /// + /// + /// + 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); + } + + /// + /// 验证锁屏密码 🔖 + /// + /// + /// + [DisplayName("验证锁屏密码")] + public virtual async Task 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(keyPasswordErrorTimes); + + // 是否开启域登录验证 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysDomainLogin)) + { + var userLdap = await _sysUserRep.ChangeRepository>().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; + } + + /// + /// 手机号登录 🔖 + /// + /// + /// + [AllowAnonymous] + [DisplayName("手机号登录")] + public virtual async Task 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); + } + + /// + /// 生成Token令牌 🔖 + /// + /// \ + /// \ + /// + [NonAction] + internal virtual async Task CreateToken(SysUser user, SysUserEventTypeEnum sysUserEventTypeEnum = SysUserEventTypeEnum.Login) + { + // 单用户登录 + await _sysOnlineUserService.SingleLogin(user.Id); + + // 生成Token令牌 + var tokenExpire = await _sysConfigService.GetTokenExpire(); + var accessToken = JWTEncryption.Encrypt(new Dictionary + { + { 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; + } + + /// + /// 获取登录账号 🔖 + /// + /// + [DisplayName("获取登录账号")] + public virtual async Task 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>().GetFirstAsync(u => u.Id == user.OrgId); + // 获取职位 + var pos = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.Id == user.PosId); + // 获取按钮集合 + var buttons = await _sysMenuService.GetOwnBtnPermList(); + // 获取角色集合 + var roleIds = await _sysUserRep.ChangeRepository>().AsQueryable() + .Where(u => u.UserId == user.Id).Select(u => u.RoleId).ToListAsync(); + // 获取水印文字(若系统水印为空则全局为空) + var watermarkText = (await _sysUserRep.Context.Queryable().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; + } + + /// + /// 获取刷新Token 🔖 + /// + /// 旧的AccessToken + /// 新的AccessToken和RefreshToken + [DisplayName("获取刷新Token")] + public virtual async Task 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); + } + + /// + /// 退出系统 🔖 + /// + [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(); + } + + /// + /// 获取验证码 🔖 + /// + /// + [AllowAnonymous] + [SuppressMonitor] + [DisplayName("获取验证码")] + public dynamic GetCaptcha() + { + var codeId = YitIdHelper.NextId().ToString(); + var captcha = _captcha.Generate(codeId); + var expirySeconds = App.GetOptions()?.ExpirySeconds ?? 60; + return new { Id = codeId, Img = captcha.Base64, ExpirySeconds = expirySeconds }; + } + + /// + /// 用户注册 🔖 + /// + /// + /// + [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().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().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 { regWay.RoleId }, + }; + addUserInput.Copy(input); + await _sysUserService.RegisterUser(addUserInput); + } + + /// + /// Swagger登录检查 🔖 + /// + /// + [AllowAnonymous] + [HttpPost("/api/swagger/checkUrl"), NonUnify] + [ApiDescriptionSettings(Description = "Swagger登录检查", DisableInherite = true)] + public int SwaggerCheckUrl() + { + return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated ? 200 : 401; + } + + /// + /// Swagger登录提交 🔖 + /// + /// + /// + [AllowAnonymous] + [HttpPost("/api/swagger/submitUrl"), NonUnify] + [ApiDescriptionSettings(Description = "Swagger登录提交", DisableInherite = true)] + public async Task 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; + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Auth/SysLdapService.cs b/Admin.NET.Core/Service/Auth/SysLdapService.cs new file mode 100644 index 0000000..1d35afe --- /dev/null +++ b/Admin.NET.Core/Service/Auth/SysLdapService.cs @@ -0,0 +1,437 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Novell.Directory.Ldap; + +namespace Admin.NET.Core; + +/// +/// 系统域登录配置服务 🧩 +/// +[ApiDescriptionSettings(Order = 496, Description = "域登录配置")] +public class SysLdapService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLdapRep; + + public SysLdapService(SqlSugarRepository sysLdapRep) + { + _sysLdapRep = sysLdapRep; + } + + /// + /// 获取系统域登录配置分页列表 🔖 + /// + /// + /// + [DisplayName("获取系统域登录配置分页列表")] + public async Task> 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); + } + + /// + /// 增加系统域登录配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加系统域登录配置")] + public async Task Add(AddSysLdapInput input) + { + var entity = input.Adapt(); + entity.BindPass = CryptogramUtil.Encrypt(input.BindPass); + await _sysLdapRep.InsertAsync(entity); + return entity.Id; + } + + /// + /// 更新系统域登录配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新系统域登录配置")] + public async Task Update(UpdateSysLdapInput input) + { + var entity = input.Adapt(); + if (!string.IsNullOrEmpty(input.BindPass) && input.BindPass.Length < 32) + { + entity.BindPass = CryptogramUtil.Encrypt(input.BindPass); // 加密 + } + + await _sysLdapRep.AsUpdateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); + } + + /// + /// 删除系统域登录配置 🔖 + /// + /// + /// + [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); // 真删除 + } + + /// + /// 获取系统域登录配置详情 🔖 + /// + /// + /// + [DisplayName("获取系统域登录配置详情")] + public async Task GetDetail([FromQuery] DetailSysLdapInput input) + { + return await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 获取系统域登录配置列表 🔖 + /// + /// + [DisplayName("获取系统域登录配置列表")] + public async Task> GetList() + { + return await _sysLdapRep.AsQueryable().Select().ToListAsync(); + } + + /// + /// 验证账号 + /// + /// 域用户 + /// 密码 + /// 租户 + /// + [NonAction] + public async Task 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; + } + + /// + /// 同步域用户 🔖 + /// + /// + /// + [DisplayName("同步域用户")] + [NonAction] + public async Task> 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); + } + + /// + /// 同步域用户 🔖 + /// + /// + /// + [DisplayName("同步域用户")] + public async Task> SyncUser(SyncSysLdapInput input) + { + var sysLdap = await _sysLdapRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + return await SysLdapService.SyncUser(sysLdap); + } + + /// + /// 同步域用户 🔖 + /// + /// + /// + private static async Task> 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(); + 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().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(); + } + } + + /// + /// 获取部门代码 + /// + /// + /// + /// + private static string GetDepartmentCode(LdapAttributeSet attrs, string bindAttrCode) + { + return bindAttrCode == "objectGUID" + ? new Guid(attrs.GetAttribute(bindAttrCode)?.ByteValue!).ToString() + : attrs.GetAttribute(bindAttrCode)?.StringValue ?? "0"; + } + + /// + /// 创建同步对象 + /// + /// + /// + /// + /// + /// + 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; + } + + /// + /// 遍历查询域用户 + /// + /// + /// + /// + /// + /// + private static async Task SearchDnLdapUser(LdapConnection ldapConn, SysLdap sysLdap, List 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); + } + } + } + + /// + /// 同步域组织 🔖 + /// + /// + /// + [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(); + 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().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(); + } + } + + /// + /// 遍历查询域用户 + /// + /// + /// + /// + /// + /// + private static async Task SearchDnLdapDept(LdapConnection ldapConn, SysLdap sysLdap, List 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); + } + } + + /// + /// 创建架构对象 + /// + /// + /// + /// + /// + /// + private static SysOrg CreateSysOrg(LdapAttributeSet attrs, SysLdap sysLdap, List 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, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/BaseService.cs b/Admin.NET.Core/Service/BaseService.cs new file mode 100644 index 0000000..51a46a6 --- /dev/null +++ b/Admin.NET.Core/Service/BaseService.cs @@ -0,0 +1,90 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 实体操作基服务 +/// +/// +public class BaseService : IDynamicApiController where TEntity : class, new() +{ + private readonly SqlSugarRepository _rep; + + public BaseService(SqlSugarRepository rep) + { + _rep = rep; + } + + /// + /// 获取详情 🔖 + /// + /// + /// + [DisplayName("获取详情")] + public virtual async Task GetDetail(long id) + { + return await _rep.GetByIdAsync(id); + } + + /// + /// 获取集合 🔖 + /// + /// + [DisplayName("获取集合")] + public virtual async Task> GetList() + { + return await _rep.GetListAsync(); + } + + ///// + ///// 获取实体分页 🔖 + ///// + ///// + ///// + //[ApiDescriptionSettings(Name = "Page")] + //[DisplayName("获取实体分页")] + //public async Task> GetPage([FromQuery] BasePageInput input) + //{ + // return await _rep.AsQueryable().ToPagedListAsync(input.Page, input.PageSize); + //} + + /// + /// 增加 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加")] + public virtual async Task Add(TEntity entity) + { + return await _rep.InsertAsync(entity); + } + + /// + /// 更新 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新")] + public virtual async Task Update(TEntity entity) + { + return await _rep.AsUpdateable(entity).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除")] + public virtual async Task Delete(long id) + { + return await _rep.DeleteByIdAsync(id); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Cache/SysCacheService.cs b/Admin.NET.Core/Service/Cache/SysCacheService.cs new file mode 100644 index 0000000..3482b91 --- /dev/null +++ b/Admin.NET.Core/Service/Cache/SysCacheService.cs @@ -0,0 +1,485 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core.Service; + +/// +/// 系统缓存服务 🧩 +/// +[ApiDescriptionSettings(Order = 400, Description = "系统缓存")] +public class SysCacheService : IDynamicApiController, ISingleton +{ + private static ICacheProvider _cacheProvider; + private readonly CacheOptions _cacheOptions; + + public SysCacheService(ICacheProvider cacheProvider, IOptions cacheOptions) + { + _cacheProvider = cacheProvider; + _cacheOptions = cacheOptions.Value; + } + + /// + /// 申请分布式锁 🔖 + /// + /// 要锁定的key + /// 申请锁等待的时间,单位毫秒 + /// 锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒 + /// 失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败 + /// + [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; + } + } + + /// + /// 获取缓存键名集合 🔖 + /// + /// + [DisplayName("获取缓存键名集合")] + public List 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)]; + } + + /// + /// 增加缓存 + /// + /// + /// + /// + [NonAction] + public bool Set(string key, object value) + { + return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value); + } + + /// + /// 增加缓存并设置过期时间 + /// + /// + /// + /// + /// + [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 AdGetAsync(String cacheName, Func> del, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, T2 t2, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1, t2], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, T2 t2, T3 t3, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1, t2, t3], expiry); + } + + private async Task AdGetAsync(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(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(String cacheName, object t1) + { + return Get(cacheName, [t1]); + } + + public T Get(String cacheName, object t1, object t2) + { + return Get(cacheName, [t1, t2]); + } + + public T Get(String cacheName, object t1, object t2, object t3) + { + return Get(cacheName, [t1, t2, t3]); + } + + private T Get(String cacheName, Object[] obs) + { + var key = Key(cacheName, obs); + return Get(key); + } + + private static string Key(string cacheName, object[] obs) + { + if (obs.OfType().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(); + } + + /// + /// 获取缓存的剩余生存时间 + /// + /// + /// + [NonAction] + public TimeSpan GetExpire(string key) + { + return _cacheProvider.Cache.GetExpire(key); + } + + /// + /// 获取缓存 + /// + /// + /// + /// + [NonAction] + public T Get(string key) + { + return _cacheProvider.Cache.Get($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 删除缓存 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除缓存")] + public int Remove(string key) + { + return _cacheProvider.Cache.Remove($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 清空所有缓存 🔖 + /// + /// + [DisplayName("清空所有缓存")] + [ApiDescriptionSettings(Name = "Clear"), HttpPost] + public void Clear() + { + _cacheProvider.Cache.Clear(); + + Cache.Default.Clear(); + } + + /// + /// 检查缓存是否存在 + /// + /// 键 + /// + [NonAction] + public bool ExistKey(string key) + { + return _cacheProvider.Cache.ContainsKey($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 根据键名前缀删除缓存 🔖 + /// + /// 键名前缀 + /// + [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); + } + + /// + /// 根据键名前缀获取键名集合 🔖 + /// + /// 键名前缀 + /// + [DisplayName("根据键名前缀获取键名集合")] + public List 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(); + } + + /// + /// 获取缓存值 🔖 + /// + /// + /// + [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(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(fullKey); + + case "list": + var list = redisCache.GetList(fullKey); + return list?.ToList(); + + case "hash": + var hash = redisCache.GetDictionary(fullKey); + return hash?.ToDictionary(k => k.Key, v => v.Value); + + case "set": + var set = redisCache.GetSet(fullKey); + return set?.ToArray(); + + case "zset": + var sortedSet = redisCache.GetSortedSet(fullKey); + return sortedSet?.Range(0, -1)?.ToList(); + + case "none": + return null; + + default: + // 未知类型或特殊类型 + return new Dictionary + { + { "key", key }, + { "type", keyType ?? "unknown" }, + { "message", "无法使用标准方式获取此类型数据" } + }; + } + } + catch (Exception ex) + { + return new Dictionary + { + { "key", key }, + { "error", ex.Message }, + { "type", "exception" } + }; + } + } + + return _cacheProvider.Cache.Get(fullKey); + } + + /// + /// 获取或添加缓存(在数据不存在时执行委托请求数据) + /// + /// + /// + /// + /// 过期时间,单位秒 + /// + [NonAction] + public T GetOrAdd(string key, Func callback, int expire = -1) + { + if (string.IsNullOrWhiteSpace(key)) return default; + return _cacheProvider.Cache.GetOrAdd($"{_cacheOptions.Prefix}{key}", callback, expire); + } + + /// + /// Hash匹配 + /// + /// + /// + /// + [NonAction] + public IDictionary GetHashMap(string key) + { + return _cacheProvider.Cache.GetDictionary(key); + } + + /// + /// 批量添加HASH + /// + /// + /// + /// + /// + [NonAction] + public bool HashSet(string key, Dictionary dic) + { + var hash = GetHashMap(key); + foreach (var v in dic) + { + hash.Add(v); + } + return true; + } + + /// + /// 添加一条HASH + /// + /// + /// + /// + /// + [NonAction] + public void HashAdd(string key, string hashKey, T value) + { + var hash = GetHashMap(key); + hash.Add(hashKey, value); + } + + /// + /// 添加或更新一条HASH + /// + /// + /// + /// + /// + [NonAction] + public void HashAddOrUpdate(string key, string hashKey, T value) + { + var hash = GetHashMap(key); + if (hash.ContainsKey(hashKey)) + hash[hashKey] = value; + else + hash.Add(hashKey, value); + } + + /// + /// 获取多条HASH + /// + /// + /// + /// + /// + [NonAction] + public List HashGet(string key, params string[] fields) + { + var hash = GetHashMap(key); + return hash.Where(t => fields.Any(c => t.Key == c)).Select(t => t.Value).ToList(); + } + + /// + /// 获取一条HASH + /// + /// + /// + /// + /// + [NonAction] + public T HashGetOne(string key, string field) + { + var hash = GetHashMap(key); + return hash.TryGetValue(field, out T value) ? value : default; + } + + /// + /// 根据KEY获取所有HASH + /// + /// + /// + /// + [NonAction] + public IDictionary HashGetAll(string key) + { + var hash = GetHashMap(key); + return hash; + } + + /// + /// 删除HASH + /// + /// + /// + /// + /// + [NonAction] + public int HashDel(string key, params string[] fields) + { + var hash = GetHashMap(key); + fields.ToList().ForEach(t => hash.Remove(t)); + return fields.Length; + } + + ///// + ///// 搜索HASH + ///// + ///// + ///// + ///// + ///// + //[NonAction] + //public List> HashSearch(string key, SearchModel searchModel) + //{ + // var hash = GetHashMap(key); + // return hash.Search(searchModel).ToList(); + //} + + ///// + ///// 搜索HASH + ///// + ///// + ///// + ///// + ///// + ///// + //[NonAction] + //public List> HashSearch(string key, string pattern, int count) + //{ + // var hash = GetHashMap(key); + // return hash.Search(pattern, count).ToList(); + //} +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/CustomViewEngine.cs b/Admin.NET.Core/Service/CodeGen/CustomViewEngine.cs new file mode 100644 index 0000000..d935511 --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/CustomViewEngine.cs @@ -0,0 +1,184 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 自定义模板引擎 +/// +public class CustomViewEngine : ViewEngineModel +{ + /// + /// 库定位器 + /// + 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 TableField { get; set; } + + public List ImportFieldList { get; set; } + + public List UploadFieldList { get; set; } + + public List QueryWhetherList { get; set; } + + public List ApiTreeFieldList { get; set; } + + public List DropdownFieldList { get; set; } + + public List AddUpdateFieldList { get; set; } + + public List PrimaryKeyFieldList { get; set; } + + public List TableUniqueConfigList { get; set; } + + public List IgnoreUpdateFieldList => TableField.Where(u => u.WhetherAddUpdate == "N" && u.ColumnKey != "True" && u.WhetherCommon != "Y").ToList(); + + /// + /// 格式化主键查询条件 + /// 例: PrimaryKeysFormat(" || ", "u.{0} == input.{0}") + /// 单主键返回 u.Id == input.Id + /// 组合主键返回 u.Id == input.Id || u.FkId == input.FkId + /// + /// 分隔符 + /// 模板字符串 + /// 字段首字母小写 + /// + public string PrimaryKeysFormat(string separator, string format, bool lowerFirstLetter = false) => string.Join(separator, PrimaryKeyFieldList.Select(u => string.Format(format, lowerFirstLetter ? u.LowerPropertyName : u.PropertyName))); + + /// + /// 注入的服务 + /// + /// + public Dictionary InjectServiceMap + { + get + { + var injectMap = new Dictionary(); + 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; + } + } + + /// + /// 服务构造参数 + /// + public string InjectServiceArgs => InjectServiceMap.Count > 0 ? ", " + string.Join(", ", InjectServiceMap.Select(kv => $"{kv.Key} {kv.Value}")) : ""; + + /// + /// 默认值列表 + /// + public List DefaultValueList { get; set; } + + /// + /// 判断字段是否为状态字段 + /// + /// + /// + public bool IsStatus(CodeGenConfig column) => column.NetType == nameof(StatusEnum); + + /// + /// 获取首字母小写字符串 + /// + /// + /// + public string ToLowerFirstLetter(string text) => string.IsNullOrWhiteSpace(text) ? text : text[..1].ToLower() + text[1..]; + + /// + /// 将基本字段类型转为可空类型 + /// + /// + /// + public string GetNullableNetType(string netType) => Regex.IsMatch(netType, "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? netType.TrimEnd('?') + "?" : netType; + + /// + /// 获取前端表格列定义的属性 + /// + /// + /// + 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; + } + + /// + /// 设置默认值 + /// + /// + 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; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/Dto/CodeGenConfig.cs b/Admin.NET.Core/Service/CodeGen/Dto/CodeGenConfig.cs new file mode 100644 index 0000000..e550c9a --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/Dto/CodeGenConfig.cs @@ -0,0 +1,234 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 代码生成详细配置参数 +/// +public class CodeGenConfig +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 代码生成主表ID + /// + public long CodeGenId { get; set; } + + /// + /// 数据库字段名 + /// + public string ColumnName { get; set; } + + /// + /// 主外键 + /// + public string ColumnKey { get; set; } + + /// + /// 实体属性名 + /// + public string PropertyName { get; set; } + + /// + /// 字段数据长度 + /// + public int ColumnLength { get; set; } + + /// + /// 数据库字段名(首字母小写) + /// + public string LowerPropertyName => string.IsNullOrWhiteSpace(PropertyName) ? null : PropertyName[..1].ToLower() + PropertyName[1..]; + + /// + /// 字段描述 + /// + public string ColumnComment { get; set; } + + /// + /// .NET类型 + /// + public string NetType { get; set; } + + /// + /// 数据库中类型(物理类型) + /// + public string DataType { get; set; } + + /// + /// 字段数据默认值 + /// + public string DefaultValue { get; set; } + + /// + /// 可空.NET类型 + /// + public string NullableNetType => Regex.IsMatch(NetType ?? "", "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? NetType.TrimEnd('?') + "?" : NetType; + + /// + /// 作用类型(字典) + /// + public string EffectType { get; set; } + + /// + /// 外键库标识 + /// + public string FkConfigId { get; set; } + + /// + /// 外键实体名称 + /// + public string FkEntityName { get; set; } + + /// + /// 外键表名称 + /// + public string FkTableName { get; set; } + + /// + /// 外键实体名称(首字母小写) + /// + public string LowerFkEntityName => string.IsNullOrWhiteSpace(FkEntityName) ? null : FkEntityName[..1].ToLower() + FkEntityName[1..]; + + /// + /// 外键链接字段 + /// + public string FkLinkColumnName { get; set; } + + /// + /// 外键显示字段 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string FkDisplayColumns { get; set; } + + /// + /// 外键显示字段 + /// + public List FkDisplayColumnList { get; set; } + + /// + /// 外键显示字段(首字母小写) + /// + public List LowerFkDisplayColumnsList => FkDisplayColumnList?.Select(name => name[..1].ToLower() + name[1..]).ToList(); + + /// + /// 外键显示字段.NET类型 + /// + public string FkColumnNetType { get; set; } + + /// + /// 父级字段 + /// + public string PidColumn { get; set; } + + /// + /// 字典code + /// + public string DictTypeCode { get; set; } + + /// + /// 查询方式 + /// + public string QueryType { get; set; } + + /// + /// 是否是查询条件 + /// + public string WhetherQuery { get; set; } + + /// + /// 列表是否缩进(字典) + /// + public string WhetherRetract { get; set; } + + /// + /// 是否必填(字典) + /// + public string WhetherRequired { get; set; } + + /// + /// 是否可排序(字典) + /// + public string WhetherSortable { get; set; } + + /// + /// 列表显示 + /// + public string WhetherTable { get; set; } + + /// + /// 增改 + /// + public string WhetherAddUpdate { get; set; } + + /// + /// 导入 + /// + public string WhetherImport { get; set; } + + /// + /// 是否是通用字段 + /// + public string WhetherCommon { get; set; } + + /// + /// 排序 + /// + public int OrderNo { get; set; } + + /// + /// 是否是选择器控件 + /// + public bool IsSelectorEffectType => Regex.IsMatch(EffectType ?? "", "Selector$|ForeignKey", RegexOptions.IgnoreCase); + + /// + /// 去掉尾部Id的属性名 + /// + public string PropertyNameTrimEndId => PropertyName.TrimEnd("Id"); + + /// + /// 去掉尾部Id的属性名 + /// + public string LowerPropertyNameTrimEndId => LowerPropertyName.TrimEnd("Id"); + + /// + /// 扩展属性名称 + /// + 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 + }; + + /// + /// 首字母小写的扩展属性名称 + /// + public string LowerExtendedPropertyName + { + get + { + var displayPropertyName = ExtendedPropertyName; + if (string.IsNullOrWhiteSpace(displayPropertyName)) return null; + return displayPropertyName[..1].ToLower() + displayPropertyName[1..]; + } + } + + /// + /// 获取外键显示值语句 + /// + /// 表别名 + /// 多字段时的连接符 + /// + public string GetDisplayColumn(string tableAlias, string separator = "-") => "$\"" + string.Join(separator, FkDisplayColumnList?.Select(name => $"{{{tableAlias}.{name}}}") ?? new List()) + "\""; +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/Dto/CodeGenInput.cs b/Admin.NET.Core/Service/CodeGen/Dto/CodeGenInput.cs new file mode 100644 index 0000000..4598d98 --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/Dto/CodeGenInput.cs @@ -0,0 +1,199 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 代码生成参数类 +/// +public class CodeGenInput : BasePageInput +{ + /// + /// 作者姓名 + /// + public virtual string AuthorName { get; set; } + + /// + /// 类名 + /// + public virtual string ClassName { get; set; } + + /// + /// 是否移除表前缀 + /// + public virtual string TablePrefix { get; set; } + + /// + /// 库定位器名 + /// + public virtual string ConfigId { get; set; } + + /// + /// 数据库名(保留字段) + /// + public virtual string DbName { get; set; } + + /// + /// 数据库类型 + /// + public virtual string DbType { get; set; } + + /// + /// 数据库链接 + /// + public virtual string ConnectionString { get; set; } + + /// + /// 生成方式 + /// + public virtual string GenerateType { get; set; } + + /// + /// 数据库表名 + /// + public virtual string TableName { get; set; } + + /// + /// 命名空间 + /// + public virtual string NameSpace { get; set; } + + /// + /// 业务名(业务代码包名称) + /// + public virtual string BusName { get; set; } + + /// + /// 功能名(数据库表名称) + /// + public virtual string TableComment { get; set; } + + /// + /// 表唯一字段 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string TableUniqueConfig { get; set; } + + /// + /// 表唯一字段列表 + /// + public virtual List TableUniqueList { get; set; } + + /// + /// 菜单应用分类(应用编码) + /// + public virtual string MenuApplication { get; set; } + + /// + /// 是否生成菜单 + /// + public virtual bool GenerateMenu { get; set; } + + /// + /// 菜单父级 + /// + public virtual long? MenuPid { get; set; } + + /// + /// 菜单图标 + /// + public virtual string MenuIcon { get; set; } + + /// + /// 页面目录 + /// + public virtual string PagePath { get; set; } + + /// + /// 支持打印类型 + /// + public virtual string PrintType { get; set; } + + /// + /// 打印模版名称 + /// + public virtual string PrintName { get; set; } +} + +public class AddCodeGenInput : CodeGenInput +{ + /// + /// 数据库表名 + /// + [Required(ErrorMessage = "数据库表名不能为空")] + public override string TableName { get; set; } + + /// + /// 业务名(业务代码包名称) + /// + [Required(ErrorMessage = "业务名不能为空")] + public override string BusName { get; set; } + + /// + /// 命名空间 + /// + [Required(ErrorMessage = "命名空间不能为空")] + public override string NameSpace { get; set; } + + /// + /// 作者姓名 + /// + [Required(ErrorMessage = "作者姓名不能为空")] + public override string AuthorName { get; set; } + + ///// + ///// 类名 + ///// + //[Required(ErrorMessage = "类名不能为空")] + //public override string ClassName { get; set; } + + ///// + ///// 是否移除表前缀 + ///// + //[Required(ErrorMessage = "是否移除表前缀不能为空")] + //public override string TablePrefix { get; set; } + + /// + /// 生成方式 + /// + [Required(ErrorMessage = "生成方式不能为空")] + public override string GenerateType { get; set; } + + ///// + ///// 功能名(数据库表名称) + ///// + //[Required(ErrorMessage = "数据库表名不能为空")] + //public override string TableComment { get; set; } + + /// + /// 是否生成菜单 + /// + [Required(ErrorMessage = "是否生成菜单不能为空")] + public override bool GenerateMenu { get; set; } +} + +public class DeleteCodeGenInput +{ + /// + /// 代码生成器Id + /// + [Required(ErrorMessage = "代码生成器Id不能为空")] + public long Id { get; set; } +} + +public class UpdateCodeGenInput : AddCodeGenInput +{ + /// + /// 代码生成器Id + /// + [Required(ErrorMessage = "代码生成器Id不能为空")] + public long Id { get; set; } +} + +public class QueryCodeGenInput : DeleteCodeGenInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/Dto/CodeGenOutput.cs b/Admin.NET.Core/Service/CodeGen/Dto/CodeGenOutput.cs new file mode 100644 index 0000000..aee96f6 --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/Dto/CodeGenOutput.cs @@ -0,0 +1,83 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 代码生成参数类 +/// +public class CodeGenOutput +{ + /// + /// 代码生成器Id + /// + public long Id { get; set; } + + /// + /// 作者姓名 + /// + public string AuthorName { get; set; } + + /// + /// 类名 + /// + public string ClassName { get; set; } + + /// + /// 是否移除表前缀 + /// + public string TablePrefix { get; set; } + + /// + /// 生成方式 + /// + public string GenerateType { get; set; } + + /// + /// 数据库表名 + /// + public string TableName { get; set; } + + /// + /// 包名 + /// + public string PackageName { get; set; } + + /// + /// 业务名(业务代码包名称) + /// + public string BusName { get; set; } + + /// + /// 功能名(数据库表名称) + /// + public string TableComment { get; set; } + + /// + /// 菜单应用分类(应用编码) + /// + public string MenuApplication { get; set; } + + /// + /// 是否生成菜单 + /// + public bool GenerateMenu { get; set; } + + /// + /// 菜单父级 + /// + public long? MenuPid { get; set; } + + /// + /// 支持打印类型 + /// + public string PrintType { get; set; } + + /// + /// 打印模版名称 + /// + public string PrintName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/Dto/ColumnOuput.cs b/Admin.NET.Core/Service/CodeGen/Dto/ColumnOuput.cs new file mode 100644 index 0000000..01047e2 --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/Dto/ColumnOuput.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 数据库表列 +/// +public class ColumnOuput +{ + /// + /// 字段名 + /// + public string ColumnName { get; set; } + + /// + /// 实体的Property名 + /// + public string PropertyName { get; set; } + + /// + /// 字段数据长度 + /// + public int ColumnLength { get; set; } + + /// + /// 数据库中类型 + /// + public string DataType { get; set; } + + /// + /// 字段数据默认值 + /// + public string DefaultValue { get; set; } + + /// + /// 是否为主键 + /// + public bool IsPrimarykey { get; set; } + + /// + /// 是否允许为空 + /// + public bool IsNullable { get; set; } + + /// + /// .NET字段类型 + /// + public string NetType { get; set; } + + /// + /// 字典编码 + /// + public string DictTypeCode { get; set; } + + /// + /// 字段描述 + /// + public string ColumnComment { get; set; } + + /// + /// 主外键 + /// + public string ColumnKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/Dto/DatabaseOutput.cs b/Admin.NET.Core/Service/CodeGen/Dto/DatabaseOutput.cs new file mode 100644 index 0000000..dae6db3 --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/Dto/DatabaseOutput.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 数据库 +/// +public class DatabaseOutput +{ + /// + /// 库定位器名 + /// + public string ConfigId { get; set; } + + /// + /// 库名 + /// + public string DbNickName { get; set; } + + /// + /// 数据库类型 + /// + public SqlSugar.DbType DbType { get; set; } + + /// + /// 数据库连接字符串 + /// + public string ConnectionString { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/Dto/TableOutput.cs b/Admin.NET.Core/Service/CodeGen/Dto/TableOutput.cs new file mode 100644 index 0000000..1bb8e81 --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/Dto/TableOutput.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 数据库表 +/// +public class TableOutput +{ + /// + /// 库定位器名 + /// + public string ConfigId { get; set; } + + /// + /// 表名(字母形式的) + /// + public string TableName { get; set; } + + /// + /// 实体名称 + /// + public string EntityName { get; set; } + + /// + /// 创建时间 + /// + public string CreateTime { get; set; } + + /// + /// 更新时间 + /// + public string UpdateTime { get; set; } + + /// + /// 表名称描述(功能名) + /// + public string TableComment { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/Dto/TableUniqueConfigItem.cs b/Admin.NET.Core/Service/CodeGen/Dto/TableUniqueConfigItem.cs new file mode 100644 index 0000000..0cac927 --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/Dto/TableUniqueConfigItem.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 表唯一配置项 +/// +public class TableUniqueConfigItem +{ + /// + /// 字段列表 + /// + public List Columns { get; set; } + + /// + /// 描述信息 + /// + public string Message { get; set; } + + /// + /// 格式化查询条件 + /// + /// 分隔符 + /// 模板字符串 + /// + public string Format(string separator, string format) => string.Join(separator, Columns.Select(name => string.Format(format, name))); +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs b/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs new file mode 100644 index 0000000..44e42cd --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs @@ -0,0 +1,256 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统代码生成配置服务 🧩 +/// +[ApiDescriptionSettings(Order = 260)] +public class SysCodeGenConfigService : IDynamicApiController, ITransient +{ + private readonly ISqlSugarClient _db; + + public SysCodeGenConfigService(ISqlSugarClient db) + { + _db = db; + } + + /// + /// 获取代码生成配置列表 🔖 + /// + /// + /// + [DisplayName("获取代码生成配置列表")] + public async Task> GetList([FromQuery] CodeGenConfig input) + { + return await _db.Queryable() + .Where(u => u.CodeGenId == input.CodeGenId) + .Select() + .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(); + } + + /// + /// 更新代码生成配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新代码生成配置")] + public async Task UpdateCodeGenConfig(List 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>()) + .IgnoreColumns(u => new { u.ColumnLength, u.ColumnName, u.PropertyName }) + .ExecuteCommandAsync(); + } + + /// + /// 删除代码生成配置 + /// + /// + /// + [NonAction] + public async Task DeleteCodeGenConfig(long codeGenId) + { + await _db.Deleteable().Where(u => u.CodeGenId == codeGenId).ExecuteCommandAsync(); + } + + /// + /// 获取代码生成配置详情 🔖 + /// + /// + /// + [DisplayName("获取代码生成配置详情")] + public async Task GetDetail([FromQuery] CodeGenConfig input) + { + return await _db.Queryable().FirstAsync(u => u.Id == input.Id); + } + + /// + /// 批量增加代码生成配置 + /// + /// + /// + [NonAction] + public void AddList(List tableColumnOutputList, SysCodeGen codeGenerate) + { + if (tableColumnOutputList == null) return; + + var codeGenConfigs = new List(); + 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(); + } + + /// + /// 批量更新代码字段:先删除再新增,会保留历史字段操作类型 + /// + /// + /// + [NonAction] + public async Task UpdateList(List tableColumnOutputList, long codeGenId) + { + if (tableColumnOutputList == null) return; + + //获取历史数据 + var oldList = await GetList(new CodeGenConfig() { CodeGenId = codeGenId, }); + //删除历史数据 + await DeleteCodeGenConfig(codeGenId); + + var codeGenConfigs = new List(); + 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(); + } + + /// + /// 默认查询类型 + /// + /// + /// + private static string GetDefaultQueryType(SysCodeGenConfig codeGenConfig) + { + return (codeGenConfig.NetType?.TrimEnd('?')) switch + { + "string" => "like", + "DateTime" => "~", + _ => "==", + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs b/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs new file mode 100644 index 0000000..d5d5793 --- /dev/null +++ b/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs @@ -0,0 +1,825 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.IO.Compression; + +namespace Admin.NET.Core.Service; + +/// +/// 系统代码生成器服务 🧩 +/// +[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, + SysCodeGenConfigService codeGenConfigService, + IOptions codeGenOptions, + SysMenuService sysMenuService, + UserManager userManager, + IViewEngine viewEngine) + { + _db = db; + _viewEngine = viewEngine; + _userManager = userManager; + _sysMenuService = sysMenuService; + _codeGenOptions = codeGenOptions.Value; + _codeGenConfigService = codeGenConfigService; + _dbConnectionOptions = dbConnectionOptions.Value; + } + + /// + /// 获取代码生成分页列表 🔖 + /// + /// + /// + [DisplayName("获取代码生成分页列表")] + public async Task> Page(CodeGenInput input) + { + return await _db.Queryable() + .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); + } + + /// + /// 增加代码生成 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加代码生成")] + public async Task AddCodeGen(AddCodeGenInput input) + { + var isExist = await _db.Queryable().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(); + var newCodeGen = await _db.Insertable(codeGen).ExecuteReturnEntityAsync(); + + // 增加配置表 + _codeGenConfigService.AddList(GetColumnList(input), newCodeGen); + } + + /// + /// 更新代码生成 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新代码生成")] + public async Task UpdateCodeGen(UpdateCodeGenInput input) + { + var isExist = await _db.Queryable().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400); + + var oldRecord = await _db.Queryable().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().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(); + await _db.Updateable(codeGen).ExecuteCommandAsync(); + + // 仅当数据表名称发生了变化,才更新配置表 + //if (oldRecord.TableName != input.TableName) + //{ + await _codeGenConfigService.DeleteCodeGenConfig(codeGen.Id); + _codeGenConfigService.AddList(GetColumnList(input.Adapt()), codeGen); + //} + _db.AsTenant().CommitTran(); + } + catch (Exception ex) + { + _db.AsTenant().RollbackTran(); + throw Oops.Oh(ex); + } + } + + /// + /// 同步代码字段(保留历史作用类型) 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "SyncField"), HttpPost] + [DisplayName("同步代码字段")] + public async Task SyncCodeFieldGen(UpdateCodeGenInput input) + { + var isExist = await _db.Queryable().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()), input.Id); + _db.AsTenant().CommitTran(); + } + catch (Exception ex) + { + _db.AsTenant().RollbackTran(); + throw Oops.Oh(ex); + } + } + + /// + /// 删除代码生成 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除代码生成")] + public async Task DeleteCodeGen(List inputs) + { + if (inputs == null || inputs.Count < 1) return; + + var codeGenConfigTaskList = new List(); + inputs.ForEach(u => + { + _db.Deleteable().In(u.Id).ExecuteCommand(); + + // 删除配置表 + codeGenConfigTaskList.Add(_codeGenConfigService.DeleteCodeGenConfig(u.Id)); + }); + await Task.WhenAll(codeGenConfigTaskList); + } + + /// + /// 获取代码生成详情 🔖 + /// + /// + /// + [DisplayName("获取代码生成详情")] + public async Task GetDetail([FromQuery] QueryCodeGenInput input) + { + return await _db.Queryable().SingleAsync(u => u.Id == input.Id); + } + + /// + /// 获取数据库库集合 🔖 + /// + /// + [DisplayName("获取数据库库集合")] + public async Task> GetDatabaseList() + { + var dbConfigs = _dbConnectionOptions.ConnectionConfigs; + return await Task.FromResult(dbConfigs.Adapt>()); + } + + /// + /// 获取数据库表(实体)集合 🔖 + /// + /// + [DisplayName("获取数据库表(实体)集合")] + public async Task> 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 entityInfos = await GetEntityInfos(configId); + + var tableOutputList = new List(); + foreach (var item in entityInfos) + { + var tbConfigId = item.Type.GetCustomAttribute()?.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; + } + + /// + /// 根据表名获取列集合 🔖 + /// + /// + [DisplayName("根据表名获取列集合")] + public List 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()?.IsIgnore == false).Select(e => new + { + PropertyName = e.Name, + ColumnComment = e.GetCustomAttribute()?.ColumnDescription, + ColumnName = e.GetCustomAttribute()?.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; + } + + /// + /// 获取数据表列(实体属性)集合 + /// + /// + private List 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()?.ColumnName ?? ""), columnOutput.ColumnName, StringComparison.CurrentCultureIgnoreCase)) ?? + entityProperties.FirstOrDefault(u => u.GetCustomAttribute() != 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()!.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(); + if (dict != null) columnOutput.DictTypeCode = dict.DictTypeCode; + } + } + else + { + result.RemoveAt(i); // 移除没有定义此属性的字段 + } + } + return result; + } + + /// + /// 获取库表信息 + /// + /// + private async Task> GetEntityInfos(string configId) + { + var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId) ?? throw Oops.Oh(ErrorCodeEnum.D1401); + var entityInfos = new List(); + + var type = typeof(SugarTable); + var types = new List(); + 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); + } + + /// + /// 获取程序保存位置 🔖 + /// + /// + [DisplayName("获取程序保存位置")] + public List GetApplicationNamespaces() + { + return _codeGenOptions.BackendApplicationNamespaces; + } + + /// + /// 代码生成到本地 🔖 + /// + /// + [UnitOfWork] + [DisplayName("代码生成到本地")] + public async Task RunLocal(SysCodeGen input) + { + if (string.IsNullOrEmpty(input.GenerateType)) + input.GenerateType = "200"; + + // 先删除该表已生成的菜单列表 + List 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" }; + } + + /// + /// 获取代码生成预览 🔖 + /// + /// + [DisplayName("获取代码生成预览")] + public async Task> Preview(SysCodeGen input) + { + var (_, result) = await RenderTemplateAsync(input); + return result; + } + + /// + /// 渲染模板 + /// + /// + /// + private async Task<(List tableFieldList, Dictionary 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(); + 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); + } + + /// + /// 添加或更新菜单 + /// + /// + /// + /// + /// + /// + /// + /// + private async Task AddOrUpdateMenu(string className, string busName, long pid, string menuIcon, string pagePath, List tableFieldList) + { + var title = $"{busName}管理"; + var lowerClassName = className.ToFirstLetterLowerCase(); + var menuType = pid == 0 ? MenuTypeEnum.Dir : MenuTypeEnum.Menu; + + // 查询是否已有主菜单 + var existingMenu = await _db.Queryable() + .Where(m => m.Title == title && m.Type == menuType && m.Pid == pid) + .FirstAsync(); + + string parentPath = ""; + if (pid != 0) + { + var parent = await _db.Queryable().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()); + } + else + { + // 存在则更新 + existingMenu.Icon = menuIcon; + existingMenu.Path = (pid == 0 ? "/" : parentPath + "/") + className.ToLower(); + existingMenu.Component = pid == 0 ? "Layout" : $"/{pagePath}/{lowerClassName}/index"; + await _sysMenuService.UpdateMenu(existingMenu.Adapt()); + menuId = existingMenu.Id; + } + + // 定义应有的按钮 + var orderNo = 100; + var newButtons = new List + { + 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() + .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()); + } + else + { + match.Title = btn.Title; + match.OrderNo = btn.OrderNo; + await _sysMenuService.UpdateMenu(match.Adapt()); + } + } + + // 删除多余的旧按钮 + var toDelete = existingButtons.Where(b => !newPermissions.Contains(b.Permission)).ToList(); + foreach (var del in toDelete) + await _sysMenuService.DeleteMenu(new DeleteMenuInput { Id = del.Id }); + } + + /// + /// 增加菜单 + /// + /// + /// + /// + /// + /// + /// + /// + private async Task AddMenu(string className, string busName, long pid, string menuIcon, string pagePath, List 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()); + } + else + { + var parentMenu = await _db.Queryable().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()); + + var orderNo = 100; + var menuList = new List + { + 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()); + } + + /// + /// 根据菜单名称和类型删除关联的菜单树 + /// + /// + /// + private async Task DeleteMenuTree(string title, MenuTypeEnum type) + { + var menuList = await _db.Queryable().Where(u => u.Title == title && u.Type == type).ToListAsync() ?? new(); + foreach (var menu in menuList) await _sysMenuService.DeleteMenu(new DeleteMenuInput { Id = menu.Id }); + } + + /// + /// 获取模板文件路径集合 + /// + /// + private static List 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" }; + } + + /// + /// 获取模板文件路径集合 + /// + /// + private static List GetTemplatePathList() => new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" }; + + /// + /// 设置生成文件路径 + /// + /// + /// + private List 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 + { + indexPath, + formModalPath, + apiJsPath + }; + } + + if (input.GenerateType.Substring(1, 1).Contains('2')) + { + // 生成到本项目(后端) + return new List + { + servicePath, + inputPath, + outputPath, + viewPath, + }; + } + // 前后端同时生成到本项目 + return new List + { + servicePath, + inputPath, + outputPath, + viewPath, + indexPath, + formModalPath, + apiJsPath + }; + } + + /// + /// 设置生成文件路径 + /// + /// + /// + private List 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 + { + indexPath, + formModalPath, + apiJsPath + }; + } + + if (input.GenerateType.StartsWith("12")) + { + return new List + { + servicePath, + inputPath, + outputPath, + viewPath + }; + } + + return new List + { + servicePath, + inputPath, + outputPath, + viewPath, + indexPath, + formModalPath, + apiJsPath + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Common/Dto/ApiOutput.cs b/Admin.NET.Core/Service/Common/Dto/ApiOutput.cs new file mode 100644 index 0000000..efd7901 --- /dev/null +++ b/Admin.NET.Core/Service/Common/Dto/ApiOutput.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 接口/动态API输出 +/// +public class ApiOutput +{ + /// + /// 组名称 + /// + public string GroupName { get; set; } + + /// + /// 接口名称 + /// + public string DisplayName { get; set; } + + /// + /// 路由名称 + /// + public string RouteName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Common/Dto/ProcDto.cs b/Admin.NET.Core/Service/Common/Dto/ProcDto.cs new file mode 100644 index 0000000..6acbea6 --- /dev/null +++ b/Admin.NET.Core/Service/Common/Dto/ProcDto.cs @@ -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; + +/// +/// 基础存储过程输入类 +/// +public class BaseProcInput +{ + /// + /// ProcId + /// + public string ProcId { get; set; } + + /// + /// 数据库配置Id + /// + public string ConfigId { get; set; } = SqlSugarConst.MainConfigId; + + /// + /// 存储过程输入参数 + /// + /// {"id":"351060822794565"} + public Dictionary ProcParams { get; set; } +} + +/// +/// 带表头名称存储过程输入类 +/// +public class ExportProcByTMPInput : BaseProcInput +{ + /// + /// 模板名称 + /// + public string Template { get; set; } +} + +/// +/// 带表头名称存储过程输入类 +/// +public class ExportProcInput : BaseProcInput +{ + public Dictionary EHeader { get; set; } +} + +/// +/// 指定导出类名(有排序)存储过程输入类 +/// +public class ExportProcInput2 : BaseProcInput +{ + public List EHeader { get; set; } +} + +/// +/// 前端指定列 +/// +public class ProcExporterHeaderFilter : IExporterHeaderFilter +{ + private Dictionary> _includeHeader; + + public ProcExporterHeaderFilter(Dictionary> 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; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Common/Dto/SmKeyPairOutput.cs b/Admin.NET.Core/Service/Common/Dto/SmKeyPairOutput.cs new file mode 100644 index 0000000..cc97d39 --- /dev/null +++ b/Admin.NET.Core/Service/Common/Dto/SmKeyPairOutput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 国密公钥私钥对输出 +/// +public class SmKeyPairOutput +{ + /// + /// 私匙 + /// + public string PrivateKey { get; set; } + + /// + /// 公匙 + /// + public string PublicKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Common/Dto/SysCommonInput.cs b/Admin.NET.Core/Service/Common/Dto/SysCommonInput.cs new file mode 100644 index 0000000..d4e7e16 --- /dev/null +++ b/Admin.NET.Core/Service/Common/Dto/SysCommonInput.cs @@ -0,0 +1,69 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 接口压测输入参数 +/// +public class StressTestInput +{ + /// + /// 接口请求地址 + /// + /// https://gitee.com/zuohuaijun/Admin.NET + [Required(ErrorMessage = "接口请求地址不能为空")] + public string RequestUri { get; set; } + + /// + /// 请求方式 + /// + [Required(ErrorMessage = "请求方式不能为空")] + public string RequestMethod { get; set; } = nameof(HttpMethod.Get); + + /// + /// 每轮请求量 + /// + /// 100 + [Required(ErrorMessage = "每轮请求量不能为空")] + [Range(1, 100000, ErrorMessage = "每轮请求量必须为1-100000")] + public int? NumberOfRequests { get; set; } + + /// + /// 压测轮数 + /// + /// 5 + [Required(ErrorMessage = "压测轮数不能为空")] + [Range(1, 10000, ErrorMessage = "压测轮数必须为1-10000")] + public int? NumberOfRounds { get; set; } + + /// + /// 最大并行量(默认为当前主机逻辑处理器的数量) + /// + /// 500 + [Range(0, 10000, ErrorMessage = "最大并行量必须为0-10000")] + public int? MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount; + + /// + /// 请求参数 + /// + public List> RequestParameters { get; set; } = new(); + + /// + /// 请求头参数 + /// + public Dictionary Headers { get; set; } = new(); + + /// + /// 路径参数 + /// + public Dictionary PathParameters { get; set; } = new(); + + /// + /// Query参数 + /// + public Dictionary QueryParameters { get; set; } = new(); +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Common/Dto/SysCommonOutput.cs b/Admin.NET.Core/Service/Common/Dto/SysCommonOutput.cs new file mode 100644 index 0000000..d366de0 --- /dev/null +++ b/Admin.NET.Core/Service/Common/Dto/SysCommonOutput.cs @@ -0,0 +1,88 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 接口压测输出参数 +/// +public class StressTestOutput +{ + /// + /// 总请求次数 + /// + public long TotalRequests { get; set; } + + /// + /// 总用时(秒) + /// + public double TotalTimeInSeconds { get; set; } + + /// + /// 成功请求次数 + /// + public long SuccessfulRequests { get; set; } + + /// + /// 失败请求次数 + /// + public long FailedRequests { get; set; } + + /// + /// 每秒查询率(QPS) + /// + public double QueriesPerSecond { get; set; } + + /// + /// 最小响应时间(毫秒) + /// + public double MinResponseTime { get; set; } + + /// + /// 最大响应时间(毫秒) + /// + public double MaxResponseTime { get; set; } + + /// + /// 平均响应时间(毫秒) + /// + public double AverageResponseTime { get; set; } + + /// + /// P10 响应时间(毫秒) + /// + public double Percentile10ResponseTime { get; set; } + + /// + /// P25 响应时间(毫秒) + /// + public double Percentile25ResponseTime { get; set; } + + /// + /// P50 响应时间(毫秒) + /// + public double Percentile50ResponseTime { get; set; } + + /// + /// P75 响应时间(毫秒) + /// + public double Percentile75ResponseTime { get; set; } + + /// + /// P90 响应时间(毫秒) + /// + public double Percentile90ResponseTime { get; set; } + + /// + /// P99 响应时间(毫秒) + /// + public double Percentile99ResponseTime { get; set; } + + /// + /// P999 响应时间(毫秒) + /// + public double Percentile999ResponseTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Common/SysCommonService.cs b/Admin.NET.Core/Service/Common/SysCommonService.cs new file mode 100644 index 0000000..8ab02a2 --- /dev/null +++ b/Admin.NET.Core/Service/Common/SysCommonService.cs @@ -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; + +/// +/// 系统通用服务 🧩 +/// +[ApiDescriptionSettings(Order = 101)] +[AllowAnonymous] +public class SysCommonService : IDynamicApiController, ITransient +{ + private readonly IApiDescriptionGroupCollectionProvider _apiProvider; + private readonly SqlSugarRepository _sysUserRep; + private readonly CDConfigOptions _cdConfigOptions; + private readonly UserManager _userManager; + private readonly HttpClient _httpClient; + + public SysCommonService(IApiDescriptionGroupCollectionProvider apiProvider, + SqlSugarRepository sysUserRep, + IOptions giteeOptions, + IHttpClientFactory httpClientFactory, + UserManager userManager) + { + _sysUserRep = sysUserRep; + _apiProvider = apiProvider; + _userManager = userManager; + _cdConfigOptions = giteeOptions.Value; + _httpClient = httpClientFactory.CreateClient(); + } + + /// + /// 获取国密公钥私钥对 🏆 + /// + /// + [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, + }; + } + + /// + /// 获取所有接口/动态API 🔖 + /// + /// + [DisplayName("获取所有接口/动态API")] + public List GetApiList() + { + var apiList = new List(); + foreach (var item in _apiProvider.ApiDescriptionGroups.Items) + { + foreach (var apiDescription in item.Items) + { + var displayName = apiDescription.TryGetMethodInfo(out MethodInfo apiMethodInfo) ? apiMethodInfo.GetCustomAttribute(true)?.DisplayName : ""; + + apiList.Add(new ApiOutput + { + GroupName = item.GroupName, + DisplayName = displayName, + RouteName = apiDescription.RelativePath + }); + } + } + return apiList; + } + + /// + /// 下载标记错误的临时Excel(全局) + /// + /// + [DisplayName("下载标记错误的临时Excel(全局)")] + public async Task DownloadErrorExcelTemp([FromQuery] string fileName = null) + { + var userId = App.User?.FindFirst(ClaimConst.UserId)?.Value; + var resultStream = App.GetRequiredService().Get(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" + }); + } + + /// + /// 加密字符串 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("加密字符串")] + public dynamic EncryptPlainText([Required] string plainText) + { + return CryptogramUtil.Encrypt(plainText); + } + + /// + /// 接口压测 🔖 + /// + /// + [DisplayName("接口压测")] + public async Task StressTest(StressTestInput input) + { + // 限制仅超管用户才能使用此功能 + if (!_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.SA001); + + var stopwatch = new Stopwatch(); + var responseTimes = new List(); //响应时间集合 + 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) + }; + } + + /// + /// 创建请求消息 + /// + /// 输入参数 + /// url + /// + 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; + } + + /// + /// 计算百分位请求耗时 + /// + /// 请求耗时列表 + /// 百分位 + /// + private double CalculatePercentile(List 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]; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Common/SysProcService.cs b/Admin.NET.Core/Service/Common/SysProcService.cs new file mode 100644 index 0000000..2bbdc42 --- /dev/null +++ b/Admin.NET.Core/Service/Common/SysProcService.cs @@ -0,0 +1,101 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统存储过程服务 🧩 +/// +[ApiDescriptionSettings(Order = 102)] +public class SysProcService : IDynamicApiController, ITransient +{ + private readonly ISqlSugarClient _db; + + public SysProcService(ISqlSugarClient db) + { + _db = db; + } + + /// + /// 导出存储过程数据-指定列,没有指定的字段会被隐藏 🔖 + /// + /// + public async Task 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>(); + var index = 1; + foreach (var val in input.EHeader) + { + headers.Add(val.Key.ToUpper(), new Tuple(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" }; + } + + /// + /// 根据模板导出存储过程数据 🔖 + /// + /// + /// + public async Task 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" }; + } + + /// + /// 获取存储过程返回表-Oracle、达梦参数顺序不能错 🔖 + /// + /// + /// + public async Task ProcTable(BaseProcInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + return await db.Ado.UseStoredProcedure().GetDataTableAsync(input.ProcId, input.ProcParams); + } + + /// + /// 获取存储过程返回数据集-Oracle、达梦参数顺序不能错 + /// Oracle 返回table、table1,其他返回table1、table2。适用于报表、复杂详细页面等 🔖 + /// + /// + /// + [HttpPost] + public async Task CommonDataSet(BaseProcInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + return await db.Ado.UseStoredProcedure().GetDataSetAllAsync(input.ProcId, input.ProcParams); + } + + ///// + ///// 根据配置表获取对映存储过程 + ///// + ///// + ///// + //public async Task ProcEnitybyConfig(BaseProcInput input) + //{ + // var key = "ProcConfig"; + // var ds = _sysCacheService.Get>(key); + // if (ds == null || ds.Count == 0 || !ds.ContainsKey(input.ProcId)) + // { + // var datas = await _db.Queryable().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); + //} +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Config/Dto/ConfigInput.cs b/Admin.NET.Core/Service/Config/Dto/ConfigInput.cs new file mode 100644 index 0000000..d5c0b3e --- /dev/null +++ b/Admin.NET.Core/Service/Config/Dto/ConfigInput.cs @@ -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 +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 分组编码 + /// + public string GroupCode { get; set; } +} + +public class AddConfigInput : SysConfig +{ +} + +public class UpdateConfigInput : AddConfigInput +{ +} + +public class DeleteConfigInput : BaseIdInput +{ +} + +/// +/// 批量配置参数输入 +/// +public class BatchConfigInput +{ + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 属性值 + /// + public string Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Config/Dto/InfoInput.cs b/Admin.NET.Core/Service/Config/Dto/InfoInput.cs new file mode 100644 index 0000000..6089b77 --- /dev/null +++ b/Admin.NET.Core/Service/Config/Dto/InfoInput.cs @@ -0,0 +1,84 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统信息保存输入参数 +/// +public class InfoSaveInput +{ + /// + /// 系统图标(Data URI scheme base64 编码) + /// + public string LogoBase64 { get; set; } + + /// + /// 系统图标文件名 + /// + public string LogoFileName { get; set; } + + /// + /// 水印内容 + /// + public string Watermark { get; set; } + + /// + /// 系统主标题 + /// + [Required(ErrorMessage = "系统主标题不能为空")] + public string Title { get; set; } + + /// + /// 系统副标题 + /// + [Required(ErrorMessage = "系统副标题不能为空")] + public string ViceTitle { get; set; } + + /// + /// 系统描述 + /// + [Required(ErrorMessage = "系统描述不能为空")] + public string ViceDesc { get; set; } + + /// + /// 版权说明 + /// + [Required(ErrorMessage = "版权说明不能为空")] + public string Copyright { get; set; } + + /// + /// ICP备案号 + /// + [Required(ErrorMessage = "ICP备案号不能为空")] + public string Icp { get; set; } + + /// + /// ICP地址 + /// + [Required(ErrorMessage = "ICP地址不能为空")] + public string IcpUrl { get; set; } + + /// + /// 启用注册功能 + /// + public YesNoEnum EnableReg { get; set; } + + /// + /// 登录二次验证 + /// + public YesNoEnum SecondVer { get; set; } + + /// + /// 图形验证码 + /// + public YesNoEnum Captcha { get; set; } + + /// + /// 默认注册方案Id + /// + public virtual long RegWayId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Config/SysConfigService.cs b/Admin.NET.Core/Service/Config/SysConfigService.cs new file mode 100644 index 0000000..c7d643d --- /dev/null +++ b/Admin.NET.Core/Service/Config/SysConfigService.cs @@ -0,0 +1,307 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using NewLife.Reflection; + +namespace Admin.NET.Core.Service; + +/// +/// 平台参数配置服务 🧩 +/// +[ApiDescriptionSettings(Order = 440)] +public class SysConfigService : IDynamicApiController, ITransient +{ + private static readonly SysTenantService SysTenantService = App.GetService(); + private readonly SqlSugarRepository _sysConfigRep; + private readonly SqlSugarRepository _sysTenantRep; + private readonly SysCacheService _sysCacheService; + private readonly UserManager _userManager; + + public SysConfigService( + SqlSugarRepository sysTenantRep, + SqlSugarRepository sysConfigRep, + SysCacheService sysCacheService, + UserManager userManager) + { + _sysCacheService = sysCacheService; + _sysConfigRep = sysConfigRep; + _sysTenantRep = sysTenantRep; + _userManager = userManager; + } + + /// + /// 获取参数配置分页列表 🔖 + /// + /// + /// + [DisplayName("获取参数配置分页列表")] + public async Task> 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); + } + + /// + /// 获取参数配置列表 🔖 + /// + /// + [DisplayName("获取参数配置列表")] + public async Task> List(PageConfigInput input) + { + return await _sysConfigRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .ToListAsync(); + } + + /// + /// 增加参数配置 🔖 + /// + /// + /// + [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()); + } + + /// + /// 更新参数配置 🔖 + /// + /// + /// + [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(); + await _sysConfigRep.AsUpdateable(config).IgnoreColumns(true).ExecuteCommandAsync(); + + Remove(config); + } + + /// + /// 删除参数配置 🔖 + /// + /// + /// + [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); + } + + /// + /// 批量删除参数配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + [DisplayName("批量删除参数配置")] + public async Task BatchDeleteConfig(List 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); + } + } + + /// + /// 获取参数配置详情 🔖 + /// + /// + /// + [DisplayName("获取参数配置详情")] + public async Task GetDetail([FromQuery] ConfigInput input) + { + return await _sysConfigRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 获取参数配置值 + /// + /// + /// + [NonAction] + public async Task GetConfigValue(string code) + { + if (string.IsNullOrWhiteSpace(code)) return default; + + var value = _sysCacheService.Get($"{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)); + } + + /// + /// 更新参数配置值 + /// + /// + /// + /// + [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); + } + + /// + /// 获取分组列表 🔖 + /// + /// + [DisplayName("获取分组列表")] + public async Task> GetGroupList() + { + return await _sysConfigRep.AsQueryable() + .GroupBy(u => u.GroupCode) + .Select(u => u.GroupCode).ToListAsync(); + } + + /// + /// 获取 Token 过期时间 + /// + /// + [NonAction] + public async Task GetTokenExpire() + { + var tokenExpireStr = await GetConfigValue(ConfigConst.SysTokenExpire); + _ = int.TryParse(tokenExpireStr, out var tokenExpire); + return tokenExpire == 0 ? 20 : tokenExpire; + } + + /// + /// 获取 RefreshToken 过期时间 + /// + /// + [NonAction] + public async Task GetRefreshTokenExpire() + { + var refreshTokenExpireStr = await GetConfigValue(ConfigConst.SysRefreshTokenExpire); + _ = int.TryParse(refreshTokenExpireStr, out var refreshTokenExpire); + return refreshTokenExpire == 0 ? 40 : refreshTokenExpire; + } + + /// + /// 批量更新参数配置值 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost] + [DisplayName("批量更新参数配置值")] + public async Task BatchUpdateConfig(List 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); + } + } + + /// + /// 获取系统信息 🔖 + /// + /// + [SuppressMonitor] + [AllowAnonymous] + [DisplayName("获取系统信息")] + public async Task GetSysInfo() + { + var tenant = await SysTenantService.GetCurrentTenantSysInfo(); + var wayList = await _sysConfigRep.Context.Queryable() + .Where(u => u.TenantId == tenant.Id) + .Select(u => new { Label = u.Name, Value = u.Id }) + .ToListAsync(); + + var captcha = await GetConfigValue(ConfigConst.SysCaptcha); + var secondVer = await GetConfigValue(ConfigConst.SysSecondVer); + var hideTenantForLogin = await GetConfigValue(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 + }; + } + + /// + /// 保存系统信息 🔖 + /// + /// + [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}"); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Config/SysTenantConfigService.cs b/Admin.NET.Core/Service/Config/SysTenantConfigService.cs new file mode 100644 index 0000000..b92e4ef --- /dev/null +++ b/Admin.NET.Core/Service/Config/SysTenantConfigService.cs @@ -0,0 +1,289 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统租户配置参数服务 🧩 +/// +[ApiDescriptionSettings(Order = 440)] +public class SysTenantConfigService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly SqlSugarRepository _sysConfigRep; + private readonly SqlSugarRepository _sysConfigDataRep; + public readonly ISugarQueryable VSysConfig; + private readonly UserManager _userManager; + + public SysTenantConfigService(SysCacheService sysCacheService, + SqlSugarRepository sysConfigRep, + SqlSugarRepository 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().MergeTable(); + // 解决PostgreSQL下并启用驼峰转下划线时,报字段不存在,SqlSugar在Select后生成的sql, as后没转下划线导致. + .SelectMergeTable((c, cv) => new SysConfig { Id = c.Id.SelectAll(), Value = cv.Value }); + } + + /// + /// 获取配置参数分页列表 🔖 + /// + /// + /// + [DisplayName("获取配置参数分页列表")] + public async Task> 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); + } + + /// + /// 获取配置参数列表 🔖 + /// + /// + [DisplayName("获取配置参数列表")] + public async Task> List(PageConfigInput input) + { + return await VSysConfig + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .ToListAsync(); + } + + /// + /// 增加配置参数 🔖 + /// + /// + /// + [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()); + await _sysConfigDataRep.InsertAsync(new SysTenantConfigData() + { + ConfigId = configId, + Value = input.Value + }); + } + + /// + /// 更新配置参数 🔖 + /// + /// + /// + [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(); + 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); + } + + /// + /// 删除配置参数 🔖 + /// + /// + /// + [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); + } + + /// + /// 批量删除配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + [DisplayName("批量删除配置参数")] + public async Task BatchDeleteConfig(List 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); + } + } + + /// + /// 获取配置参数详情 🔖 + /// + /// + /// + [DisplayName("获取配置参数详情")] + public async Task GetDetail([FromQuery] ConfigInput input) + { + return await VSysConfig.FirstAsync(u => u.Id == input.Id); + } + + /// + /// 根据Code获取配置参数 🔖 + /// + /// + /// + [NonAction] + public async Task GetConfig(string code) + { + return await VSysConfig.FirstAsync(u => u.Code == code); + } + + /// + /// 根据Code获取配置参数值 🔖 + /// + /// 编码 + /// + [DisplayName("根据Code获取配置参数值")] + public async Task GetConfigValueByCode(string code) + { + return await GetConfigValueByCode(code); + } + + /// + /// 获取配置参数值 + /// + /// 编码 + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, string defaultValue = default) + { + return await GetConfigValueByCode(code, defaultValue); + } + + /// + /// 获取配置参数值 + /// + /// 类型 + /// 编码 + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, T defaultValue = default) + { + return await GetConfigValueByCode(code, _userManager.TenantId, defaultValue); + } + + /// + /// 获取配置参数值 + /// + /// 类型 + /// 编码 + /// 租户Id + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, long tenantId, T defaultValue = default) + { + if (string.IsNullOrWhiteSpace(code)) return defaultValue; + + var value = _sysCacheService.Get($"{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)); + } + + /// + /// 更新配置参数值 + /// + /// + /// + /// + [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); + } + + /// + /// 获取分组列表 🔖 + /// + /// + [DisplayName("获取分组列表")] + public async Task> GetGroupList() + { + return await VSysConfig + .GroupBy(u => u.GroupCode) + .Select(u => u.GroupCode).ToListAsync(); + } + + /// + /// 批量更新配置参数值 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost] + [DisplayName("批量更新配置参数值")] + public async Task BatchUpdateConfig(List input) + { + foreach (var config in input) + { + await UpdateConfigValue(config.Code, config.Value); + } + } + + /// + /// 清除配置缓存 + /// + /// + private void RemoveConfigCache(SysTenantConfig config) + { + _sysCacheService.Remove($"{CacheConst.KeyTenantConfig}{_userManager.TenantId}{config.Code}"); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Config/SysUserConfigService.cs b/Admin.NET.Core/Service/Config/SysUserConfigService.cs new file mode 100644 index 0000000..897f31b --- /dev/null +++ b/Admin.NET.Core/Service/Config/SysUserConfigService.cs @@ -0,0 +1,290 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户配置参数服务 🧩 +/// +[ApiDescriptionSettings(Order = 440)] +public class SysUserConfigService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly SqlSugarRepository _sysConfigRep; + private readonly SqlSugarRepository _sysConfigDataRep; + public readonly ISugarQueryable VSysConfig; + private readonly UserManager _userManager; + + public SysUserConfigService(SysCacheService sysCacheService, + SqlSugarRepository sysConfigRep, + SqlSugarRepository 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().MergeTable(); + // 解决PostgreSQL下并启用驼峰转下划线时,报字段不存在,SqlSugar在Select后生成的sql, as后没转下划线导致. + .SelectMergeTable((c, cv) => new SysConfig { Id = c.Id.SelectAll(), Value = cv.Value }); + } + + /// + /// 获取配置参数分页列表 🔖 + /// + /// + /// + [DisplayName("获取配置参数分页列表")] + public async Task> 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); + } + + /// + /// 获取配置参数列表 🔖 + /// + /// + [DisplayName("获取配置参数列表")] + public async Task> List(PageConfigInput input) + { + return await VSysConfig + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .ToListAsync(); + } + + /// + /// 增加配置参数 🔖 + /// + /// + /// + [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()); + await _sysConfigDataRep.InsertAsync(new SysUserConfigData() + { + UserId = _userManager.UserId, + ConfigId = configId, + Value = input.Value + }); + } + + /// + /// 更新配置参数 🔖 + /// + /// + /// + [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(); + 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); + } + + /// + /// 删除配置参数 🔖 + /// + /// + /// + [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); + } + + /// + /// 批量删除配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + [DisplayName("批量删除配置参数")] + public async Task BatchDeleteConfig(List 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); + } + } + + /// + /// 获取配置参数详情 🔖 + /// + /// + /// + [DisplayName("获取配置参数详情")] + public async Task GetDetail([FromQuery] ConfigInput input) + { + return await VSysConfig.FirstAsync(u => u.Id == input.Id); + } + + /// + /// 根据Code获取配置参数 🔖 + /// + /// + /// + [NonAction] + public async Task GetConfig(string code) + { + return await VSysConfig.FirstAsync(u => u.Code == code); + } + + /// + /// 根据Code获取配置参数值 🔖 + /// + /// 编码 + /// + [DisplayName("根据Code获取配置参数值")] + public async Task GetConfigValueByCode(string code) + { + return await GetConfigValueByCode(code); + } + + /// + /// 获取配置参数值 + /// + /// 编码 + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, string defaultValue = default) + { + return await GetConfigValueByCode(code, defaultValue); + } + + /// + /// 获取配置参数值 + /// + /// 类型 + /// 编码 + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, T defaultValue = default) + { + return await GetConfigValueByCode(code, _userManager.UserId, defaultValue); + } + + /// + /// 获取配置参数值 + /// + /// 类型 + /// 编码 + /// 用户Id + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, long userId, T defaultValue = default) + { + if (string.IsNullOrWhiteSpace(code)) return defaultValue; + + var value = _sysCacheService.Get($"{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)); + } + + /// + /// 更新配置参数值 + /// + /// + /// + /// + [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); + } + + /// + /// 获取分组列表 🔖 + /// + /// + [DisplayName("获取分组列表")] + public async Task> GetGroupList() + { + return await _sysConfigRep.AsQueryable() + .GroupBy(u => u.GroupCode) + .Select(u => u.GroupCode).ToListAsync(); + } + + /// + /// 批量更新配置参数值 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost] + [DisplayName("批量更新配置参数值")] + public async Task BatchUpdateConfig(List input) + { + foreach (var config in input) + { + await UpdateConfigValue(config.Code, config.Value); + } + } + + /// + /// 清除配置缓存 + /// + /// + private void RemoveConfigCache(SysUserConfig config) + { + _sysCacheService.Remove($"{CacheConst.KeyUserConfig}{_userManager.UserId}{config.Code}"); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Const/Dto/ConstOutput.cs b/Admin.NET.Core/Service/Const/Dto/ConstOutput.cs new file mode 100644 index 0000000..698f957 --- /dev/null +++ b/Admin.NET.Core/Service/Const/Dto/ConstOutput.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class ConstOutput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public dynamic Code { get; set; } + + /// + /// 扩展字段 + /// + public dynamic Data { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Const/SysConstService.cs b/Admin.NET.Core/Service/Const/SysConstService.cs new file mode 100644 index 0000000..f69c264 --- /dev/null +++ b/Admin.NET.Core/Service/Const/SysConstService.cs @@ -0,0 +1,83 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统常量服务 🧩 +/// +[ApiDescriptionSettings(Order = 280)] +public class SysConstService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + + public SysConstService(SysCacheService sysCacheService) + { + _sysCacheService = sysCacheService; + } + + /// + /// 获取所有常量列表 🔖 + /// + /// + [DisplayName("获取所有常量列表")] + public async Task> GetList() + { + var key = $"{CacheConst.KeyConst}list"; + var constList = _sysCacheService.Get>(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); + } + + /// + /// 根据类名获取常量数据 🔖 + /// + /// + /// + [DisplayName("根据类名获取常量数据")] + public async Task> GetData([Required] string typeName) + { + var key = $"{CacheConst.KeyConst}{typeName.ToUpper()}"; + var constList = _sysCacheService.Get>(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); + } + + /// + /// 获取常量特性类型列表 + /// + /// + private List GetConstAttributeList() + { + return App.EffectiveTypes.Where(u => u.CustomAttributes.Any(c => c.AttributeType == typeof(ConstAttribute))).ToList(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/DataBase/Dto/CreateEntityInput.cs b/Admin.NET.Core/Service/DataBase/Dto/CreateEntityInput.cs new file mode 100644 index 0000000..ffc3045 --- /dev/null +++ b/Admin.NET.Core/Service/DataBase/Dto/CreateEntityInput.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class CreateEntityInput +{ + /// + /// 表名 + /// + /// student + public string TableName { get; set; } + + /// + /// 实体名 + /// + /// Student + public string EntityName { get; set; } + + /// + /// 基类名 + /// + /// AutoIncrementEntity + public string BaseClassName { get; set; } + + /// + /// 导出位置 + /// + /// Web.Application + public string Position { get; set; } + + /// + /// 库标识 + /// + public string ConfigId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs b/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs new file mode 100644 index 0000000..f124f6d --- /dev/null +++ b/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class CreateSeedDataInput +{ + /// + /// 库标识 + /// + public string ConfigId { get; set; } + + /// + /// 表名 + /// + /// student + public string TableName { get; set; } + + /// + /// 实体名称 + /// + /// Student + public string EntityName { get; set; } + + /// + /// 种子名称 + /// + /// Student + public string SeedDataName { get; set; } + + /// + /// 导出位置 + /// + /// Web.Application + public string Position { get; set; } + + /// + /// 后缀 + /// + /// Web.Application + public string Suffix { get; set; } + + /// + /// 过滤已有数据 + /// + /// + /// 如果数据在其它不同名的已有的种子类型的数据中出现过,就不生成这个数据 + /// 主要用于生成菜单功能,菜单功能往往与子项目绑定,如果生成完整数据就会导致菜单项多处理重复。 + /// + public bool FilterExistingData { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/DataBase/Dto/DbColumnInput.cs b/Admin.NET.Core/Service/DataBase/Dto/DbColumnInput.cs new file mode 100644 index 0000000..24e26a5 --- /dev/null +++ b/Admin.NET.Core/Service/DataBase/Dto/DbColumnInput.cs @@ -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 +{ + /// + /// 数据库配置ID + /// + public string ConfigId { get; set; } + + /// + /// 目标表名 + /// + public string TableName { get; set; } + + /// + ///要移动的列名 + /// + public string ColumnName { get; set; } + + /// + /// 移动到该列后方(为空时移动到首列) + /// + public string AfterColumnName { get; set; } +} + +public class DeleteDbColumnInput +{ + public string ConfigId { get; set; } + + public string TableName { get; set; } + + public string DbColumnName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/DataBase/Dto/DbColumnOutput.cs b/Admin.NET.Core/Service/DataBase/Dto/DbColumnOutput.cs new file mode 100644 index 0000000..87e30b9 --- /dev/null +++ b/Admin.NET.Core/Service/DataBase/Dto/DbColumnOutput.cs @@ -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; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/DataBase/Dto/DbTableInput.cs b/Admin.NET.Core/Service/DataBase/Dto/DbTableInput.cs new file mode 100644 index 0000000..9c64d5d --- /dev/null +++ b/Admin.NET.Core/Service/DataBase/Dto/DbTableInput.cs @@ -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 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; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs b/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs new file mode 100644 index 0000000..d77a9c7 --- /dev/null +++ b/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs @@ -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; } +} + +/// +/// 库表可视化 +/// +public class VisualDbTable +{ + public List VisualTableList { get; set; } + + public List VisualColumnList { get; set; } + + public List 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; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/DataBase/Dto/JsonIgnoredPropertyData.cs b/Admin.NET.Core/Service/DataBase/Dto/JsonIgnoredPropertyData.cs new file mode 100644 index 0000000..4727626 --- /dev/null +++ b/Admin.NET.Core/Service/DataBase/Dto/JsonIgnoredPropertyData.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 保存标注了JsonIgnore的Property的值信息 +/// +public class JsonIgnoredPropertyData +{ + /// + /// 记录索引 + /// + public int RecordIndex { get; set; } + + /// + /// 属性名 + /// + public string Name { get; set; } + + /// + /// 属性值描述 + /// + public string Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs b/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs new file mode 100644 index 0000000..cdf2cd3 --- /dev/null +++ b/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs @@ -0,0 +1,776 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Npgsql; + +namespace Admin.NET.Core.Service; + +/// +/// 系统数据库管理服务 🧩 +/// +[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) + { + _db = db; + _viewEngine = viewEngine; + _codeGenOptions = codeGenOptions.Value; + } + + /// + /// 获取库列表 🔖 + /// + /// + [DisplayName("获取库列表")] + public List GetList() + { + return App.GetOptions().ConnectionConfigs.Select(u => new VisualDb { ConfigId = u.ConfigId.ToString(), DbNickName = u.DbNickName }).ToList(); + } + + /// + /// 获取可视化库表结构 🔖 + /// + /// + [DisplayName("获取可视化库表结构")] + public VisualDbTable GetVisualDbTable() + { + var visualTableList = new List(); + var visualColumnList = new List(); + var columnRelationList = new List(); + var dbOptions = App.GetOptions().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 }; + } + + /// + /// 获取字段列表 🔖 + /// + /// 表名 + /// ConfigId + /// + [DisplayName("获取字段列表")] + public List GetColumnList(string tableName, string configId = SqlSugarConst.MainConfigId) + { + var db = _db.AsTenant().GetConnectionScope(configId); + return string.IsNullOrWhiteSpace(tableName) ? new List() : db.DbMaintenance.GetColumnInfosByTableName(tableName, false).Adapt>(); + } + + /// + /// 获取数据库数据类型列表 🔖 + /// + /// + /// + [DisplayName("获取数据库数据类型列表")] + public List GetDbTypeList(string configId = SqlSugarConst.MainConfigId) + { + var db = _db.AsTenant().GetConnectionScope(configId); + return db.DbMaintenance.GetDbTypes().OrderBy(u => u).ToList(); + } + + /// + /// 增加列 🔖 + /// + /// + [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); + } + + /// + /// 删除列 🔖 + /// + /// + [ApiDescriptionSettings(Name = "DeleteColumn"), HttpPost] + [DisplayName("删除列")] + public void DeleteColumn(DeleteDbColumnInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + db.DbMaintenance.DropColumn(input.TableName, input.DbColumnName); + } + + /// + /// 编辑列 🔖 + /// + /// + [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); + } + + /// + /// 移动列位置 🔖 + /// + /// + [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} 数据库的列移动操作"); + } + } + + /// + /// 获取列定义 + /// + /// + /// + /// + /// + /// + /// + private string GetColumnDefinitionInMySQL(ISqlSugarClient db, string tableName, string columnName, bool noDefault = false) + { + var columnDef = db.Ado.SqlQuery( + $"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(); + } + + /// + /// MySQL 列移动实现 + /// + /// + /// + /// + /// + 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()); + } + + /// + /// 获取表列表 🔖 + /// + /// ConfigId + /// + [DisplayName("获取表列表")] + public List GetTableList(string configId = SqlSugarConst.MainConfigId) + { + var db = _db.AsTenant().GetConnectionScope(configId); + return db.DbMaintenance.GetTableInfoList(false); + } + + /// + /// 增加表 🔖 + /// + /// + [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().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()); + } + + /// + /// 删除表 🔖 + /// + /// + [ApiDescriptionSettings(Name = "DeleteTable"), HttpPost] + [DisplayName("删除表")] + public void DeleteTable(DeleteDbTableInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + db.DbMaintenance.DropTable(input.TableName); + } + + /// + /// 编辑表 🔖 + /// + /// + [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()); + } + } + + /// + /// 创建实体 🔖 + /// + /// + [ApiDescriptionSettings(Name = "CreateEntity"), HttpPost] + [DisplayName("创建实体")] + public void CreateEntity(CreateEntityInput input) + { + var config = App.GetOptions().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(); + // 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 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); + } + + /// + /// 创建种子数据 🔖 + /// + /// + [ApiDescriptionSettings(Name = "CreateSeedData"), HttpPost] + [DisplayName("创建种子数据")] + public async Task CreateSeedData(CreateSeedDataInput input) + { + var config = App.GetOptions().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 dbColumnInfos = db.DbMaintenance.GetColumnInfosByTableName(input.TableName, false); // 所有字段 + IEnumerable 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().Any()) + .ToList(); + if (entityTypes.Count == 1) // 只有一个实体匹配才能过滤 + { + // 获取实体的主键对应的属性名称 + var pkInfo = entityTypes[0].GetProperties().FirstOrDefault(u => u.GetCustomAttribute()?.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(); + if (seedData == null) continue; + + List 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() != null || + // p.GetAttribute() != null) && p.GetAttribute() != null).ToList(); + // var jsonIgnoreInfo = new List>(); + // if (jsonIgnoreProperties.Count > 0) + // { + // int recordIndex = 0; + // foreach (var r in (IEnumerable)records) + // { + // List 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()?.IsIgnore ?? false)).ToList(); + for (var i = 0; i < propertyList.Count; i++) + { + if (propertyList[i].Name != nameof(EntityBaseId.Id) || !(propertyList[i].GetCustomAttribute()?.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); + } + + /// + /// 获取库表信息 + /// + /// + private async Task> GetEntityInfos() + { + var entityInfos = new List(); + + var type = typeof(SugarTable); + var types = new List(); + 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); + } + } + + /// + /// 获取实体模板文件路径 + /// + /// + private static string GetEntityTemplatePath() + { + var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template"); + return Path.Combine(templatePath, "Entity.cs.vm"); + } + + /// + /// 获取种子数据模板文件路径 + /// + /// + private static string GetSeedDataTemplatePath() + { + var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template"); + return Path.Combine(templatePath, "SeedData.cs.vm"); + } + + /// + /// 设置生成实体文件路径 + /// + /// + /// + 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("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"); + } + + /// + /// 设置生成种子数据文件路径 + /// + /// + /// + 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"); + } + + /// + /// 备份数据库(PostgreSQL)🔖 + /// + /// + [HttpPost, NonUnify] + [DisplayName("备份数据库(PostgreSQL)")] + public async Task 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 + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs b/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs new file mode 100644 index 0000000..ecff3bf --- /dev/null +++ b/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs @@ -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 +{ + /// + /// 字典类型Id + /// + public long DictTypeId { get; set; } + + /// + /// 文本 + /// + public string Label { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } +} + +public class AddDictDataInput : SysDictData +{ +} + +public class UpdateDictDataInput : AddDictDataInput +{ +} + +public class DeleteDictDataInput : BaseIdInput +{ +} + +public class GetDataDictDataInput +{ + /// + /// 字典类型Id + /// + [Required(ErrorMessage = "字典类型Id不能为空"), DataValidation(ValidationTypes.Numeric)] + public long DictTypeId { get; set; } +} + +public class QueryDictDataInput +{ + /// + /// 字典值 + /// + [Required(ErrorMessage = "字典值不能为空")] + public string Value { get; set; } + + /// + /// 状态 + /// + public int? Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Dict/Dto/DictDataOutput.cs b/Admin.NET.Core/Service/Dict/Dto/DictDataOutput.cs new file mode 100644 index 0000000..cc83939 --- /dev/null +++ b/Admin.NET.Core/Service/Dict/Dto/DictDataOutput.cs @@ -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; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs b/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs new file mode 100644 index 0000000..acbd5be --- /dev/null +++ b/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs @@ -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 +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } +} + +public class AddDictTypeInput : SysDictType +{ + /// + /// 是否是租户字典(Y-是,N-否) + /// + public override YesNoEnum IsTenant { get; set; } = YesNoEnum.Y; + + /// + /// 是否是内置字典(Y-是,N-否) + /// + public override YesNoEnum SysFlag { get; set; } = YesNoEnum.N; +} + +public class UpdateDictTypeInput : AddDictTypeInput +{ +} + +public class DeleteDictTypeInput : BaseIdInput +{ +} + +public class GetDataDictTypeInput +{ + /// + /// 编码 + /// + [Required(ErrorMessage = "字典类型编码不能为空")] + public string Code { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Dict/SysDictDataService.cs b/Admin.NET.Core/Service/Dict/SysDictDataService.cs new file mode 100644 index 0000000..7487e96 --- /dev/null +++ b/Admin.NET.Core/Service/Dict/SysDictDataService.cs @@ -0,0 +1,331 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统字典值服务 🧩 +/// +[ApiDescriptionSettings(Order = 420, Description = "系统字典值")] +public class SysDictDataService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysDictDataRep; + public readonly ISugarQueryable VSysDictData; + private readonly SysCacheService _sysCacheService; + private readonly UserManager _userManager; + private readonly SysLangTextCacheService _sysLangTextCacheService; + + public SysDictDataService(SqlSugarRepository sysDictDataRep, + SysCacheService sysCacheService, + UserManager userManager, + SysLangTextCacheService sysLangTextCacheService) + { + _userManager = userManager; + _sysDictDataRep = sysDictDataRep; + _sysCacheService = sysCacheService; + _sysLangTextCacheService = sysLangTextCacheService; + VSysDictData = _sysDictDataRep.Context.UnionAll( + _sysDictDataRep.AsQueryable(), + _sysDictDataRep.Change().AsQueryable() + //.WhereIF(_userManager.SuperAdmin, d => d.TenantId == _userManager.TenantId) + .Select()); + } + + /// + /// 获取字典值分页列表 🔖 + /// + /// + /// + [DisplayName("获取字典值分页列表")] + public async Task> 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; + } + + /// + /// 获取字典值列表 🔖 + /// + /// + [DisplayName("获取字典值列表")] + public async Task> 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); + } + + /// + /// 增加字典值 🔖 + /// + /// + /// + [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().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() : input.Adapt(); + await _sysDictDataRep.Context.Insertable(dictData).ExecuteCommandAsync(); + } + + /// + /// 更新字典值 🔖 + /// + /// + /// + [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().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() : input.Adapt(); + await _sysDictDataRep.Context.Updateable(dictData).ExecuteCommandAsync(); + } + + /// + /// 删除字典值 🔖 + /// + /// + /// + [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().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() : input.Adapt(); + await _sysDictDataRep.Context.Deleteable(entity).ExecuteCommandAsync(); + } + + /// + /// 获取字典值详情 🔖 + /// + /// + /// + [DisplayName("获取字典值详情")] + public async Task GetDetail([FromQuery] DictDataInput input) + { + return (await VSysDictData.FirstAsync(u => u.Id == input.Id))?.Adapt(); + } + + /// + /// 修改字典值状态 🔖 + /// + /// + /// + [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().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() : input.Adapt(); + await _sysDictDataRep.Context.Updateable(entity).ExecuteCommandAsync(); + } + + /// + /// 根据字典类型Id获取字典值集合 + /// + /// + /// + [NonAction] + public async Task> GetDictDataListByDictTypeId(long dictTypeId) + { + return await GetDataListByIdOrCode(dictTypeId, null); + } + + /// + /// 根据字典类型编码获取字典值集合 🔖 + /// + /// + /// + [DisplayName("根据字典类型编码获取字典值集合")] + public async Task> GetDataList(string code) + { + return await GetDataListByIdOrCode(null, code); + } + + /// + /// 获取字典值集合 🔖 + /// + /// + /// + /// + [NonAction] + public async Task> 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().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>(dicKey); + if (dictDataList == null) + { + //平台字典和租户字典分开缓存 + if (dictType.IsTenant == YesNoEnum.Y) + { + dictDataList = await _sysDictDataRep.Change().AsQueryable() + .Where(u => u.DictTypeId == dictType.Id) + .Where(u => u.Status == StatusEnum.Enable) + .WhereIF(_userManager.SuperAdmin, d => d.TenantId == _userManager.TenantId).Select() + .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; + } + + /// + /// 根据查询条件获取字典值集合 🔖 + /// + /// + /// + [DisplayName("根据查询条件获取字典值集合")] + public async Task> 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; + } + + /// + /// 根据字典类型Id删除字典值 + /// + /// + /// + [NonAction] + public async Task DeleteDictData(long dictTypeId) + { + var dictType = await _sysDictDataRep.Change().AsQueryable().Where(u => u.Id == dictTypeId).FirstAsync(); + Remove(dictType); + + if (dictType?.IsTenant == YesNoEnum.Y) + await _sysDictDataRep.Change().DeleteAsync(u => u.DictTypeId == dictTypeId); + else + await _sysDictDataRep.DeleteAsync(u => u.DictTypeId == dictTypeId); + } + + /// + /// 通过字典数据Value查询显示文本Label + /// 适用于列表中根据字典数据值找文本的子查询 _sysDictDataService.MapDictValueToLabel(() =>obj.Type, "org_type",obj); + /// + /// + /// + /// + /// + /// + public string MapDictValueToLabel(Expression> mappingFiled, string dictTypeCode, T parameter) + { + return VSysDictData.InnerJoin((d, dt) => d.DictTypeId.Equals(dt.Id) && dt.Code == dictTypeCode) + .SetContext(d => d.Value, mappingFiled, parameter).FirstOrDefault()?.Label; + } + + /// + /// 通过字典数据显示文本Label查询Value + /// 适用于列表数据导入根据字典数据文本找值的子查询 _sysDictDataService.MapDictLabelToValue(() => obj.Type, "org_type",obj); + /// + /// + /// + /// + /// + /// + public string MapDictLabelToValue(Expression> mappingFiled, string dictTypeCode, T parameter) + { + return VSysDictData.InnerJoin((d, dt) => d.DictTypeId.Equals(dt.Id) && dt.Code == dictTypeCode) + .SetContext(d => d.Label, mappingFiled, parameter).FirstOrDefault()?.Value; + } + + /// + /// 清理字典数据缓存 + /// + /// + private void Remove(SysDictType dictType) + { + _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType?.Code}"); + _sysCacheService.Remove($"{CacheConst.KeyTenantDict}{_userManager}:{dictType?.Code}"); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Dict/SysDictTypeService.cs b/Admin.NET.Core/Service/Dict/SysDictTypeService.cs new file mode 100644 index 0000000..ce1157e --- /dev/null +++ b/Admin.NET.Core/Service/Dict/SysDictTypeService.cs @@ -0,0 +1,262 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统字典类型服务 🧩 +/// +[ApiDescriptionSettings(Order = 430, Description = "系统字典类型")] +public class SysDictTypeService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysDictTypeRep; + private readonly SysDictDataService _sysDictDataService; + private readonly SysCacheService _sysCacheService; + private readonly UserManager _userManager; + private readonly SysLangTextCacheService _sysLangTextCacheService; + + public SysDictTypeService(SqlSugarRepository sysDictTypeRep, + SysDictDataService sysDictDataService, + SysCacheService sysCacheService, + UserManager userManager, + SysLangTextCacheService sysLangTextCacheService) + { + _sysDictTypeRep = sysDictTypeRep; + _sysDictDataService = sysDictDataService; + _sysCacheService = sysCacheService; + _userManager = userManager; + _sysLangTextCacheService = sysLangTextCacheService; + } + + /// + /// 获取字典类型分页列表 🔖 + /// + /// + [DisplayName("获取字典类型分页列表")] + public async Task> 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; + } + + /// + /// 获取字典类型列表 🔖 + /// + /// + [DisplayName("获取字典类型列表")] + public async Task> 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; + } + + /// + /// 获取字典类型-值列表 🔖 + /// + /// + /// + [DisplayName("获取字典类型-值列表")] + public async Task> 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; + } + + /// + /// 添加字典类型 🔖 + /// + /// + /// + [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()); + } + + /// + /// 更新字典类型 🔖 + /// + /// + /// + [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()); + } + + /// + /// 删除字典类型 🔖 + /// + /// + /// + [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); + } + + /// + /// 获取字典类型详情 🔖 + /// + /// + /// + [DisplayName("获取字典类型详情")] + public async Task GetDetail([FromQuery] DictTypeInput input) + { + return await _sysDictTypeRep.GetByIdAsync(input.Id); + } + + /// + /// 修改字典类型状态 🔖 + /// + /// + /// + [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(); + } + + /// + /// 获取所有字典集合 🔖 + /// + /// + [DisplayName("获取所有字典集合")] + public async Task 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 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; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Enum/Dto/EnumInput.cs b/Admin.NET.Core/Service/Enum/Dto/EnumInput.cs new file mode 100644 index 0000000..64506bb --- /dev/null +++ b/Admin.NET.Core/Service/Enum/Dto/EnumInput.cs @@ -0,0 +1,37 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 枚举输入参数 +/// +public class EnumInput +{ + /// + /// 枚举类型名称 + /// + /// AccountTypeEnum + [Required(ErrorMessage = "枚举类型不能为空")] + public string EnumName { get; set; } +} + +public class QueryEnumDataInput +{ + /// + /// 实体名称 + /// + /// SysUser + [Required(ErrorMessage = "实体名称不能为空")] + public string EntityName { get; set; } + + /// + /// 字段名称 + /// + /// AccountType + [Required(ErrorMessage = "字段名称不能为空")] + public string FieldName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Enum/Dto/EnumOutput.cs b/Admin.NET.Core/Service/Enum/Dto/EnumOutput.cs new file mode 100644 index 0000000..396149a --- /dev/null +++ b/Admin.NET.Core/Service/Enum/Dto/EnumOutput.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 枚举类型输出参数 +/// +public class EnumTypeOutput +{ + /// + /// 枚举类型描述 + /// + public string TypeDescribe { get; set; } + + /// + /// 枚举类型名称 + /// + public string TypeName { get; set; } + + /// + /// 枚举类型全名称 + /// + public string TypeFullName { get; set; } + + /// + /// 枚举类型备注 + /// + public string TypeRemark { get; set; } + + /// + /// 枚举实体 + /// + public List EnumEntities { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Enum/SysEnumService.cs b/Admin.NET.Core/Service/Enum/SysEnumService.cs new file mode 100644 index 0000000..b0ab79b --- /dev/null +++ b/Admin.NET.Core/Service/Enum/SysEnumService.cs @@ -0,0 +1,102 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统枚举服务 🧩 +/// +[ApiDescriptionSettings(Order = 275)] +public class SysEnumService : IDynamicApiController, ITransient +{ + private readonly EnumOptions _enumOptions; + + public SysEnumService(IOptions enumOptions) + { + _enumOptions = enumOptions.Value; + } + + /// + /// 获取所有枚举类型 🔖 + /// + /// + [DisplayName("获取所有枚举类型")] + public List 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; + } + + /// + /// 获取字典描述 + /// + /// + /// + 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() + }; + } + + /// + /// 通过枚举类型获取枚举值集合 🔖 + /// + /// + /// + [DisplayName("通过枚举类型获取枚举值集合")] + public List 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(); + } + + /// + /// 通过实体的字段名获取相关枚举值集合(目前仅支持枚举类型) 🔖 + /// + /// + /// + [DisplayName("通过实体的字段名获取相关枚举值集合")] + public static List 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(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/ExtendService/BaiDuTranslationService.cs b/Admin.NET.Core/Service/ExtendService/BaiDuTranslationService.cs new file mode 100644 index 0000000..77163ac --- /dev/null +++ b/Admin.NET.Core/Service/ExtendService/BaiDuTranslationService.cs @@ -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; + +/// +/// 百度翻译 +/// +[ApiDescriptionSettings("Extend", Module = "Extend", Order = 200)] +public class BaiDuTranslationService : IDynamicApiController, ITransient +{ + // http远程请求 + private readonly IHttpRemoteService _httpRemoteService; + + /// + /// 百度翻译appId + /// + private static readonly string _appId = "xxxxxxxxxxx"; + + /// + /// 百度翻译appKey + /// + private static readonly string _appKey = "xxxxxxxxxxx"; + + /// + /// 百度翻译api地址 + /// + private static readonly string _baseUrl = "https://fanyi-api.baidu.com/api/trans/vip/translate?"; + + // 语言映射字典 + private static readonly Dictionary langMap = new Dictionary + { + ["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" + }; + + /// + /// 初始化一个类型的新实例. + /// + /// + public BaiDuTranslationService(IHttpRemoteService httpRemoteService) + { + _httpRemoteService = httpRemoteService; + } + + /// + /// 百度在线翻译 + /// + /// 翻译源语种 + /// 翻译目标语种 + /// 文本内容 + /// + ///源语种和目标语种支持: + ///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 + /// + /// 翻译后的文本内容 + [DisplayName("百度在线翻译")] + [HttpGet] + public async Task 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(url); + + if (!res.error_code.Equals("0")) + { + throw Oops.Bah($"翻译失败,错误码:{res.error_code},错误信息:{res.error_msg}"); + } + + return res; + } + +#if DEBUG + + /// + /// 生成前端页面i18n文件 + /// + [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(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); + } + } + + /// + /// 生成前端菜单i18n文件 + /// + [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(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> 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(); + 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> ReadLanguageFile(string filePath) + { + if (!File.Exists(filePath)) + { + throw Oops.Bah($"【{filePath.Split('/').Last()}】文件未找到"); + } + + var dic = new Dictionary(); + 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(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/ExtendService/Models/BaiDuMapResult.cs b/Admin.NET.Core/Service/ExtendService/Models/BaiDuMapResult.cs new file mode 100644 index 0000000..67fbefe --- /dev/null +++ b/Admin.NET.Core/Service/ExtendService/Models/BaiDuMapResult.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 百度翻译结果 +/// +public class BaiDuTranslationResult +{ + /// + /// 源语种 + /// + public string From { get; set; } + + /// + /// 目标语种 + /// + public string To { get; set; } + + /// + /// 翻译结果 + /// + public List trans_result { get; set; } + + /// + /// 错误码 正常为0 + /// + public string error_code { get; set; } = "0"; + + /// + /// 错误信息 + /// + public string error_msg { get; set; } = String.Empty; +} + +/// +/// 翻译结果 +/// +public class TransResult +{ + /// + /// 源字符 + /// + public string Src { get; set; } + + /// + /// 目标字符 + /// + public string Dst { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/Dto/FileInput.cs b/Admin.NET.Core/Service/File/Dto/FileInput.cs new file mode 100644 index 0000000..9cc37f5 --- /dev/null +++ b/Admin.NET.Core/Service/File/Dto/FileInput.cs @@ -0,0 +1,136 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 文件分页查询 +/// +public class PageFileInput : BasePageInput +{ + /// + /// 文件名称 + /// + public string FileName { get; set; } + + /// + /// 文件路径 + /// + public string FilePath { get; set; } + + /// + /// 文件后缀 + /// + public string? Suffix { get; set; } + + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } +} + +/// +/// 上传文件 +/// +public class UploadFileInput +{ + /// + /// 文件 + /// + [Required] + public IFormFile File { get; set; } + + /// + /// 文件类别 + /// + public string FileType { get; set; } + + /// + /// 是否公开 + /// + public bool IsPublic { get; set; } = false; + + /// + /// 允许格式:.jpeg.jpg.png.bmp.gif.tif + /// + public string AllowSuffix { get; set; } + + /// + /// 指定存储桶名称 + /// + public string? BucketName { get; set; } + + /// + /// 指定存储提供者ID + /// + public long? ProviderId { get; set; } + + /// + /// 业务数据Id + /// + public long? DataId { get; set; } +} + +/// +/// 上传文件Base64 +/// +public class UploadFileFromBase64Input +{ + /// + /// 文件名 + /// + public string FileName { get; set; } + + /// + /// 文件内容 + /// + public string FileDataBase64 { get; set; } + + /// + /// 文件类型( "image/jpeg",) + /// + public string ContentType { get; set; } +} + +/// +/// 查询关联查询输入 +/// +public class RelationQueryInput +{ + /// + /// 关联对象名称 + /// + public string RelationName { get; set; } + + /// + /// 关联对象Id + /// + public long? RelationId { get; set; } + + /// + /// 文件类型:多个以","分割 + /// + public string FileTypes { get; set; } + + /// + /// 所属Id + /// + public long? BelongId { get; set; } + + /// + /// 文件类型分割 + /// + /// + public string[] GetFileTypeBS() + { + return FileTypes.Split(','); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/Dto/FileProviderInput.cs b/Admin.NET.Core/Service/File/Dto/FileProviderInput.cs new file mode 100644 index 0000000..b00b9dd --- /dev/null +++ b/Admin.NET.Core/Service/File/Dto/FileProviderInput.cs @@ -0,0 +1,182 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 文件存储提供者分页查询输入参数 +/// +public class PageFileProviderInput : BasePageInput +{ + /// + /// 存储提供者 + /// + public string? Provider { get; set; } + + /// + /// 存储桶名称 + /// + public string? BucketName { get; set; } + + /// + /// 是否启用 + /// + public bool? IsEnable { get; set; } +} + +/// +/// 增加文件存储提供者输入参数 +/// +public class AddFileProviderInput +{ + /// + /// 存储提供者 + /// + [Required(ErrorMessage = "存储提供者不能为空")] + public string Provider { get; set; } + + /// + /// 存储桶名称 + /// + [Required(ErrorMessage = "存储桶名称不能为空")] + public string BucketName { get; set; } + + /// + /// 访问密钥ID(所有云服务商统一使用此字段) + /// + public string? AccessKey { get; set; } + + /// + /// 密钥 + /// + public string? SecretKey { get; set; } + + /// + /// 地域 + /// + public string? Region { get; set; } + + /// + /// 端点地址 + /// + public string? Endpoint { get; set; } + + /// + /// 是否启用HTTPS + /// + public bool? IsEnableHttps { get; set; } = true; + + /// + /// 是否启用缓存 + /// + public bool? IsEnableCache { get; set; } = true; + + /// + /// 是否启用 + /// + public bool? IsEnable { get; set; } = true; + + /// + /// 是否默认提供者 + /// + public bool? IsDefault { get; set; } = false; + + /// + /// 自定义域名 + /// + public string? SinceDomain { get; set; } + + /// + /// 排序号 + /// + public int? OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 支持的业务类型(JSON格式) + /// + public string? BusinessTypes { get; set; } + + /// + /// 优先级 + /// + public int Priority { get; set; } = 100; +} + +/// +/// 更新文件存储提供者输入参数 +/// +public class UpdateFileProviderInput : AddFileProviderInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long Id { get; set; } +} + +/// +/// 删除文件存储提供者输入参数 +/// +public class DeleteFileProviderInput : BaseIdInput +{ +} + +/// +/// 查询文件存储提供者输入参数 +/// +public class QueryFileProviderInput : BaseIdInput +{ +} + +/// +/// 测试连接输入参数 +/// +public class TestConnectionInput : BaseIdInput +{ +} + +/// +/// 设置默认存储提供者输入参数 +/// +public class SetDefaultProviderInput +{ + /// + /// 存储提供者ID + /// + [Required(ErrorMessage = "存储提供者ID不能为空")] + public long Id { get; set; } +} + +/// +/// 文件上传选择存储提供者输入参数 +/// +public class SelectProviderInput +{ + /// + /// 文件类型 + /// + public string? FileType { get; set; } + + /// + /// 业务类型 + /// + public string? BusinessType { get; set; } + + /// + /// 指定提供者ID + /// + public long? ProviderId { get; set; } + + /// + /// 指定存储桶名称 + /// + public string? BucketName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/FileProvider/DefaultFileProvider.cs b/Admin.NET.Core/Service/File/FileProvider/DefaultFileProvider.cs new file mode 100644 index 0000000..c4618ef --- /dev/null +++ b/Admin.NET.Core/Service/File/FileProvider/DefaultFileProvider.cs @@ -0,0 +1,85 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class DefaultFileProvider : ICustomFileProvider, ITransient +{ + /// + /// 构建文件的完整物理路径 + /// + /// + /// + private string BuildFullFilePath(SysFile sysFile) + { + return Path.Combine(App.WebHostEnvironment.WebRootPath, sysFile.FilePath ?? "", $"{sysFile.Id}{sysFile.Suffix}"); + } + + /// + /// 构建目录的完整物理路径 + /// + /// + /// + private string BuildFullDirectoryPath(string relativePath) + { + return Path.Combine(App.WebHostEnvironment.WebRootPath, relativePath); + } + + /// + /// 确保目录存在 + /// + /// + 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 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 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 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; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/FileProvider/ICustomFileProvider.cs b/Admin.NET.Core/Service/File/FileProvider/ICustomFileProvider.cs new file mode 100644 index 0000000..d78c64e --- /dev/null +++ b/Admin.NET.Core/Service/File/FileProvider/ICustomFileProvider.cs @@ -0,0 +1,45 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 自定义文件提供器接口 +/// +public interface ICustomFileProvider +{ + /// + /// 获取文件流 + /// + /// + /// + /// + public Task GetFileStreamResultAsync(SysFile sysFile, string fileName); + + /// + /// 下载指定文件Base64格式 + /// + /// + /// + public Task DownloadFileBase64Async(SysFile sysFile); + + /// + /// 删除文件 + /// + /// + /// + public Task DeleteFileAsync(SysFile sysFile); + + /// + /// 上传文件 + /// + /// 文件 + /// + /// 文件存储位置 + /// 文件最终名称 + /// + public Task UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName); +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/FileProvider/MultiOSSFileProvider.cs b/Admin.NET.Core/Service/File/FileProvider/MultiOSSFileProvider.cs new file mode 100644 index 0000000..dfa5e2a --- /dev/null +++ b/Admin.NET.Core/Service/File/FileProvider/MultiOSSFileProvider.cs @@ -0,0 +1,255 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 多OSS文件提供者 +/// +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) + { + _fileProviderService = fileProviderService; + _ossServiceManager = ossServiceManager; + _ossProviderOptions = ossProviderOptions.Value; + } + + /// + /// 上传文件 + /// + /// 文件 + /// 系统文件信息 + /// 文件存储位置 + /// 文件最终名称 + /// + public async Task 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; + } + + /// + /// 删除文件 + /// + /// 系统文件信息 + /// + 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); + } + + /// + /// 获取文件流 + /// + /// 系统文件信息 + /// 文件名 + /// + public async Task 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(); + var stream = await httpRemoteService.GetAsStreamAsync(await ossService.PresignedGetObjectAsync(provider.BucketName, filePath, 5)); + + return new FileStreamResult(stream, "application/octet-stream") { FileDownloadName = fileName + sysFile.Suffix }; + } + + /// + /// 下载文件Base64格式 + /// + /// 系统文件信息 + /// + public async Task 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}"); + } + + /// + /// 获取文件提供者配置(统一方法,支持上传、删除、下载场景) + /// + /// 系统文件信息 + /// 上传的文件(可选,仅上传时传入) + /// + private async Task 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(); + } + + /// + /// 选择合适的OSS存储提供者(内联版本) + /// + /// 上传的文件 + /// 上传输入参数 + /// + private async Task 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(); + } + + /// + /// 生成文件URL(内联版本) + /// + /// 存储提供者配置 + /// 存储桶名称 + /// 文件路径 + /// + 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}") + }; + } + + /// + /// 从配置文件创建默认提供者(兜底机制) + /// + /// + private Task CreateProviderFromConfiguration() + { + try + { + // 检查是否启用了OSS配置 + if (!_ossProviderOptions.Enabled && !App.Configuration["MultiOSS:Enabled"].ToBoolean()) + return Task.FromResult(null); + + // 验证必要配置 + if (string.IsNullOrWhiteSpace(_ossProviderOptions.AccessKey) || + string.IsNullOrWhiteSpace(_ossProviderOptions.SecretKey) || + string.IsNullOrWhiteSpace(_ossProviderOptions.Bucket)) + { + return Task.FromResult(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(provider); + } + catch (Exception) + { + // 配置读取失败,返回null + return Task.FromResult(null); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/FileProvider/OSSFileProvider.cs b/Admin.NET.Core/Service/File/FileProvider/OSSFileProvider.cs new file mode 100644 index 0000000..8bb161d --- /dev/null +++ b/Admin.NET.Core/Service/File/FileProvider/OSSFileProvider.cs @@ -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, 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 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 GetFileStreamResultAsync(SysFile file, string fileName) + { + var filePath = Path.Combine(file.FilePath ?? "", file.Id + file.Suffix); + var httpRemoteService = App.GetRequiredService(); + 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 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; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/FileProvider/SSHFileProvider.cs b/Admin.NET.Core/Service/File/FileProvider/SSHFileProvider.cs new file mode 100644 index 0000000..745b783 --- /dev/null +++ b/Admin.NET.Core/Service/File/FileProvider/SSHFileProvider.cs @@ -0,0 +1,65 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class SSHFileProvider : ICustomFileProvider, ITransient +{ + /// + /// 创建SSH连接助手 + /// + /// + private SSHHelper CreateSSHHelper() + { + return new SSHHelper( + App.Configuration["SSHProvider:Host"], + App.Configuration["SSHProvider:Port"].ToInt(), + App.Configuration["SSHProvider:Username"], + App.Configuration["SSHProvider:Password"]); + } + + /// + /// 构建文件完整路径 + /// + /// + /// + 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 DownloadFileBase64Async(SysFile sysFile) + { + using var helper = CreateSSHHelper(); + return Task.FromResult(Convert.ToBase64String(helper.ReadAllBytes(sysFile.FilePath))); + } + + public Task 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 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); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/IOSSServiceManager.cs b/Admin.NET.Core/Service/File/IOSSServiceManager.cs new file mode 100644 index 0000000..b1597d0 --- /dev/null +++ b/Admin.NET.Core/Service/File/IOSSServiceManager.cs @@ -0,0 +1,211 @@ +using OnceMi.AspNetCore.OSS; + +namespace Admin.NET.Core.Service; + +/// +/// OSS服务管理器接口 +/// +public interface IOSSServiceManager : IDisposable +{ + /// + /// 获取OSS服务实例 + /// + /// 存储提供者配置 + /// + Task GetOSSServiceAsync(SysFileProvider provider); + + /// + /// 清除缓存 + /// + void ClearCache(); +} + +/// +/// OSS服务管理器实现 +/// +public class OSSServiceManager : IOSSServiceManager, ITransient +{ + private readonly IServiceProvider _serviceProvider; + private readonly ConcurrentDictionary _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(); + } + + /// + /// 获取OSS服务实例(带缓存) + /// + /// 存储提供者配置 + /// + public async Task 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; + } + } + + /// + /// 创建OSS服务实例 + /// + /// OSS配置选项 + /// + 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 + { + [$"{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(tempConfig); + services.AddLogging(); + services.AddOSSService(providerName, configSectionName); + + // 构建服务提供者并创建OSS服务 + var tempServiceProvider = services.BuildServiceProvider(); + var ossServiceFactory = tempServiceProvider.GetRequiredService(); + var ossService = ossServiceFactory.Create(providerName); + + // 注意:不要释放tempServiceProvider,因为ossService可能依赖它 + // 这里我们接受这个内存开销,因为缓存会减少创建频率 + + return ossService; + } + catch (Exception ex) + { + throw Oops.Oh($"创建OSS服务失败: {ex.Message}"); + } + } + + /// + /// 验证配置 + /// + /// 存储提供者配置 + /// + private Task 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); + } + + /// + /// 将SysFileProvider转换为OSSOptions + /// + /// + /// + private OSSOptions ConvertToOSSOptions(SysFileProvider provider) + { + if (provider == null) + throw new ArgumentNullException(nameof(provider)); + + var ossOptions = new OSSOptions + { + Provider = Enum.Parse(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; + } + + /// + /// 清除缓存 + /// + public void ClearCache() + { + lock (_lockObject) + { + _ossServiceCache.Clear(); + } + } + + /// + /// 释放资源 + /// + public void Dispose() + { + if (!_disposed) + { + lock (_lockObject) + { + _ossServiceCache.Clear(); + } + _disposed = true; + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/SysFileProviderController.cs b/Admin.NET.Core/Service/File/SysFileProviderController.cs new file mode 100644 index 0000000..518d058 --- /dev/null +++ b/Admin.NET.Core/Service/File/SysFileProviderController.cs @@ -0,0 +1,199 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 文件存储提供者管理控制器 🧩 +/// +[ApiDescriptionSettings(Order = 412, Description = "文件存储提供者管理")] +public class SysFileProviderController : IDynamicApiController, ITransient +{ + private readonly SysFileProviderService _fileProviderService; + + public SysFileProviderController(SysFileProviderService fileProviderService) + { + _fileProviderService = fileProviderService; + } + + /// + /// 获取存储提供者列表 🔖 + /// + /// + [DisplayName("获取存储提供者列表")] + public async Task> GetProviderList() + { + return await _fileProviderService.GetFileProviderList(); + } + + /// + /// 获取存储提供者分页列表 🔖 + /// + /// + /// + [DisplayName("获取存储提供者分页列表")] + public async Task> GetProviderPage(PageFileProviderInput input) + { + return await _fileProviderService.GetFileProviderPage(input); + } + + /// + /// 获取存储提供者详情 🔖 + /// + /// + /// + [DisplayName("获取存储提供者详情")] + public async Task GetProvider([FromQuery] QueryFileProviderInput input) + { + return await _fileProviderService.GetFileProvider(input); + } + + /// + /// 添加存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("添加存储提供者")] + public async Task AddProvider(AddFileProviderInput input) + { + await _fileProviderService.AddFileProvider(input); + } + + /// + /// 更新存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新存储提供者")] + public async Task UpdateProvider(UpdateFileProviderInput input) + { + await _fileProviderService.UpdateFileProvider(input); + } + + /// + /// 删除存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除存储提供者")] + public async Task DeleteProvider(DeleteFileProviderInput input) + { + await _fileProviderService.DeleteFileProvider(input); + } + + /// + /// 根据存储桶名称获取存储提供者 🔖 + /// + /// 存储桶名称 + /// + [DisplayName("根据存储桶名称获取存储提供者")] + public async Task GetProviderByBucketName(string bucketName) + { + return await _fileProviderService.GetProviderByBucketName(bucketName); + } + + /// + /// 清除存储提供者缓存 🔖 + /// + /// + [DisplayName("清除存储提供者缓存")] + public async Task ClearCache() + { + await _fileProviderService.ClearCache(); + } + + /// + /// 批量启用/禁用存储提供者 🔖 + /// + /// + /// + [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); + } + } + } + + /// + /// 获取存储提供者统计信息 🔖 + /// + /// + [DisplayName("获取存储提供者统计信息")] + public async Task 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; + } + + /// + /// 获取所有可用的存储桶列表 🔖 + /// + /// + [DisplayName("获取所有可用的存储桶列表")] + public async Task> GetAvailableBuckets() + { + return await _fileProviderService.GetAvailableBuckets(); + } + + /// + /// 获取存储桶和提供者的映射关系 🔖 + /// + /// + [DisplayName("获取存储桶和提供者的映射关系")] + public async Task>> GetBucketProviderMapping() + { + return await _fileProviderService.GetBucketProviderMapping(); + } +} + +/// +/// 批量启用/禁用存储提供者输入参数 +/// +public class BatchEnableProviderInput +{ + /// + /// 存储提供者ID列表 + /// + [Required] + public List Ids { get; set; } + + /// + /// 是否启用 + /// + public bool IsEnable { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/SysFileProviderService.cs b/Admin.NET.Core/Service/File/SysFileProviderService.cs new file mode 100644 index 0000000..2832fef --- /dev/null +++ b/Admin.NET.Core/Service/File/SysFileProviderService.cs @@ -0,0 +1,520 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OnceMi.AspNetCore.OSS; + +namespace Admin.NET.Core.Service; + +/// +/// 系统文件存储提供者服务 🧩 +/// +[ApiDescriptionSettings(Order = 411, Description = "文件存储提供者")] +public class SysFileProviderService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _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 sysFileProviderRep, + SysCacheService sysCacheService, + IOSSServiceFactory ossServiceFactory, + IOSSServiceManager ossServiceManager) + { + _userManager = userManager; + _sysFileProviderRep = sysFileProviderRep; + _sysCacheService = sysCacheService; + _ossServiceFactory = ossServiceFactory; + _ossServiceManager = ossServiceManager; + } + + /// + /// 获取文件存储提供者分页列表 🔖 + /// + /// + /// + [DisplayName("获取文件存储提供者分页列表")] + [NonAction] + public async Task> 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); + } + + /// + /// 获取文件存储提供者列表 🔖 + /// + /// + [DisplayName("获取文件存储提供者列表")] + [NonAction] + public async Task> GetFileProviderList() + { + return await _sysFileProviderRep.AsQueryable() + .Where(u => u.IsEnable == true) + .OrderBy(u => u.OrderNo) + .OrderBy(u => u.Id) + .ToListAsync(); + } + + /// + /// 增加文件存储提供者 🔖 + /// + /// + /// + [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(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(); + + // 验证配置完整性 + await ValidateProviderConfiguration(fileProvider); + + // 处理默认提供者逻辑 + await HandleDefaultProviderLogic(fileProvider); + + await _sysFileProviderRep.InsertAsync(fileProvider); + + // 清除缓存 + await ClearCache(); + + // 清除OSS服务缓存 + _ossServiceManager?.ClearCache(); + } + + /// + /// 更新文件存储提供者 🔖 + /// + /// + /// + [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(); + + // 验证配置完整性 + await ValidateProviderConfiguration(fileProvider); + + // 处理默认提供者逻辑 + await HandleDefaultProviderLogic(fileProvider); + + await _sysFileProviderRep.AsUpdateable(fileProvider).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); + + // 清除缓存 + await ClearCache(); + + // 清除OSS服务缓存 + _ossServiceManager?.ClearCache(); + } + + /// + /// 删除文件存储提供者 🔖 + /// + /// + /// + [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(); + } + + /// + /// 获取文件存储提供者详情 🔖 + /// + /// + /// + [DisplayName("获取文件存储提供者详情")] + [NonAction] + public async Task GetFileProvider([FromQuery] QueryFileProviderInput input) + { + return await _sysFileProviderRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 根据提供者和存储桶获取配置 + /// + /// + /// + /// + [NonAction] + public async Task GetFileProviderByBucket(string provider, string bucketName) + { + var providers = await GetCachedFileProviders(); + return providers.FirstOrDefault(x => x.Provider == provider && x.BucketName == bucketName && x.IsEnable == true); + } + + /// + /// 根据ID获取配置 + /// + /// + /// + [NonAction] + public async Task GetFileProviderById(long id) + { + var providers = await GetCachedFileProviders(); + return providers.FirstOrDefault(x => x.Id == id && x.IsEnable == true); + } + + /// + /// 根据存储桶名称获取存储提供者 + /// + /// 存储桶名称 + /// + [NonAction] + public async Task GetProviderByBucketName(string bucketName) + { + if (string.IsNullOrWhiteSpace(bucketName)) + return null; + + var providers = await GetCachedFileProviders(); + return providers.FirstOrDefault(p => p.BucketName == bucketName); + } + + /// + /// 获取默认存储提供者 + /// + /// + [NonAction] + public async Task GetDefaultProvider() + { + var providers = await GetCachedFileProviders(); + + // 优先返回标记为默认的提供者 + var defaultProvider = providers.FirstOrDefault(p => p.IsDefault == true); + if (defaultProvider != null) + return defaultProvider; + + // 如果没有标记为默认的,返回第一个启用的提供者(兼容旧逻辑) + return providers.FirstOrDefault(); + } + + /// + /// 获取默认存储提供者信息 🔖 + /// + /// + [DisplayName("获取默认存储提供者信息")] + [NonAction] + public async Task GetDefaultProviderInfo() + { + return await GetDefaultProvider(); + } + + /// + /// 设置默认存储提供者 🔖 + /// + /// + /// + [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; + } + } + + /// + /// 获取缓存的文件提供者列表 + /// + /// + [NonAction] + public async Task> 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)); + } + + /// + /// 清除缓存 + /// + /// + [NonAction] + public async Task ClearCache() + { + _sysCacheService.Remove(CacheKey); + await Task.CompletedTask; + } + + /// + /// 获取所有可用的存储桶列表 + /// + /// + [NonAction] + public async Task> GetAvailableBuckets() + { + var providers = await GetCachedFileProviders(); + return providers.Select(p => p.BucketName).Distinct().OrderBy(b => b).ToList(); + } + + /// + /// 获取存储桶和提供者的映射关系 + /// + /// + [NonAction] + public async Task>> GetBucketProviderMapping() + { + var providers = await GetCachedFileProviders(); + var mapping = new Dictionary>(); + + foreach (var provider in providers) + { + if (!mapping.TryGetValue(provider.BucketName, out List value)) + { + value = new List(); + mapping[provider.BucketName] = value; + } + + value.Add(provider); + } + + return mapping; + } + + /// + /// 验证存储提供者配置 + /// + /// 存储提供者配置 + /// + [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); + } + + /// + /// 验证存储桶名称格式 + /// + /// 存储提供者类型 + /// 存储桶名称 + /// + [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; + } + + /// + /// 处理默认提供者逻辑 + /// + /// 存储提供者配置 + /// + [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; + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/File/SysFileService.cs b/Admin.NET.Core/Service/File/SysFileService.cs new file mode 100644 index 0000000..7c68dd6 --- /dev/null +++ b/Admin.NET.Core/Service/File/SysFileService.cs @@ -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; + +/// +/// 系统文件服务 🧩 +/// +[ApiDescriptionSettings(Order = 410, Description = "系统文件")] +public class SysFileService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _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 _namedServiceProvider; + private readonly ICustomFileProvider _customFileProvider; + + public SysFileService(UserManager userManager, + SqlSugarRepository sysFileRep, + IOptions oSSProviderOptions, + IOptions uploadOptions, + INamedServiceProvider 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(nameof(MultiOSSFileProvider)); + } + else if (_configuration["SSHProvider:Enabled"].ToBoolean()) + { + _customFileProvider = _namedServiceProvider.GetService(nameof(SSHFileProvider)); + } + else + { + _customFileProvider = _namedServiceProvider.GetService(nameof(DefaultFileProvider)); + } + } + + /// + /// 获取文件分页列表 🔖 + /// + /// + /// + [DisplayName("获取文件分页列表")] + public async Task> 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); + } + + /// + /// 上传文件Base64 🔖 + /// + /// + /// + [DisplayName("上传文件Base64")] + public async Task UploadFileFromBase64(UploadFileFromBase64Input input) + { + var pattern = @"data:(?.+?);base64,(?[^""]+)"; + 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.File = formFile; + return await UploadFile(uploadFileInput); + } + + /// + /// 上传多文件 🔖 + /// + /// + /// + [DisplayName("上传多文件")] + public async Task> UploadFiles([Required] List files) + { + var fileList = new List(); + foreach (var file in files) + { + var uploadedFile = await UploadFile(new UploadFileInput { File = file }); + fileList.Add(uploadedFile); + } + return fileList; + } + + /// + /// 根据文件Id或Url下载 🔖 + /// + /// + /// + [DisplayName("根据文件Id或Url下载")] + public async Task 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); + } + + /// + /// 文件预览 🔖 + /// + /// + /// + [DisplayName("文件预览")] + public async Task 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 + ""); + } + + /// + /// 获取文件流 + /// + /// + /// + /// + private async Task GetFileStreamResult(SysFile file, string fileName) + { + return await _customFileProvider.GetFileStreamResultAsync(file, fileName); + } + + /// + /// 获取文件流 + /// + [NonAction] + public async Task GetFileStream(SysFile file) + { + var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); + var result = await _customFileProvider.GetFileStreamResultAsync(file, fileName); + return result.FileStream; + } + + /// + /// 下载指定文件Base64格式 🔖 + /// + /// + /// + [DisplayName("下载指定文件Base64格式")] + public async Task DownloadFileBase64([FromBody] string url) + { + var sysFile = await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在"); + return await _customFileProvider.DownloadFileBase64Async(sysFile); + } + + /// + /// 删除文件 🔖 + /// + /// + /// + [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); + } + + /// + /// 更新文件 🔖 + /// + /// + /// + [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); + } + + /// + /// 获取文件 🔖 + /// + /// + /// + [DisplayName("获取文件")] + public async Task GetFile([FromQuery] long id) + { + var file = await _sysFileRep.CopyNew().GetByIdAsync(id); + return file ?? throw Oops.Oh(ErrorCodeEnum.D8000); + } + + /// + /// 根据文件Id集合获取文件 🔖 + /// + /// + /// + [DisplayName("根据文件Id集合获取文件")] + public async Task> GetFileByIds([FromQuery][FlexibleArray] List ids) + { + return await _sysFileRep.AsQueryable().Where(u => ids.Contains(u.Id)).ToListAsync(); + } + + /// + /// 获取文件路径 🔖 + /// + /// + [DisplayName("获取文件路径")] + public async Task> GetFolder() + { + // 优化:直接在数据库层面获取不重复的文件路径 + var folders = await _sysFileRep.AsQueryable() + .Select(u => u.FilePath) + .Distinct() + .ToListAsync(); + + var pathTreeBuilder = new PathTreeBuilder(); + var tree = pathTreeBuilder.BuildTree(folders); + return tree.Children; + } + + /// + /// 上传文件 🔖 + /// + /// + /// 存储目标路径 + /// + [DisplayName("上传文件")] + public async Task 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(); + 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; + } + + /// + /// 上传头像 🔖 + /// + /// + /// + [DisplayName("上传头像")] + public async Task UploadAvatar([Required] IFormFile file) + { + var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/avatar"); + + var sysUserRep = _sysFileRep.ChangeRepository>(); + 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; + } + + /// + /// 上传电子签名 🔖 + /// + /// + /// + [DisplayName("上传电子签名")] + public async Task UploadSignature([Required] IFormFile file) + { + var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/signature"); + + var sysUserRep = _sysFileRep.ChangeRepository>(); + 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集合导航属性,业务增加和更新、删除分别调用即可 + + /// + /// 更新文件的业务数据Id + /// + /// + /// + /// + [NonAction] + public async Task UpdateFileByDataId(long dataId, List 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)); + } + + /// + /// 删除业务数据对应的文件 + /// + /// + /// + [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集合导航属性,业务增加和更新、删除分别调用即可 +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Job/DbJobPersistence.cs b/Admin.NET.Core/Service/Job/DbJobPersistence.cs new file mode 100644 index 0000000..5611835 --- /dev/null +++ b/Admin.NET.Core/Service/Job/DbJobPersistence.cs @@ -0,0 +1,188 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 作业持久化(数据库) +/// +public class DbJobPersistence : IJobPersistence +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + + public DbJobPersistence(IServiceScopeFactory serviceScopeFactory) + { + _serviceScopeFactory = serviceScopeFactory; + } + + /// + /// 作业调度服务启动时 + /// + /// + /// + /// + public async Task> PreloadAsync(CancellationToken stoppingToken) + { + using var scope = _serviceScopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService().CopyNew(); + var dynamicJobCompiler = scope.ServiceProvider.GetRequiredService(); + + // 获取所有定义的作业 + var allJobs = App.EffectiveTypes.ScanToBuilders().ToList(); + // 若数据库不存在任何作业,则直接返回 + if (!await db.Queryable().AnyAsync(u => true, stoppingToken)) return allJobs; + + // 遍历所有定义的作业 + foreach (var schedulerBuilder in allJobs) + { + // 获取作业信息构建器 + var jobBuilder = schedulerBuilder.GetJobBuilder(); + + // 加载数据库数据 + var dbDetail = await db.Queryable().FirstAsync(u => u.JobId == jobBuilder.JobId, stoppingToken); + if (dbDetail == null) continue; + + // 同步数据库数据 + jobBuilder.LoadFrom(dbDetail); + + // 获取作业的所有数据库的触发器 + var dbTriggers = await db.Queryable().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().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().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; + } + + /// + /// 作业计划初始化通知 + /// + /// + /// + /// + public Task OnLoadingAsync(SchedulerBuilder builder, CancellationToken stoppingToken) + { + return Task.FromResult(builder); + } + + /// + /// 作业计划Scheduler的JobDetail变化时 + /// + /// + /// + public async Task OnChangedAsync(PersistenceContext context) + { + using var scope = _serviceScopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService().CopyNew(); + + var jobDetail = context.JobDetail.Adapt(); + 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().Where(u => u.JobId == jobDetail.JobId).ExecuteCommandAsync(); + break; + } + } + + /// + /// 作业计划Scheduler的触发器Trigger变化时 + /// + /// + /// + public async Task OnTriggerChangedAsync(PersistenceTriggerContext context) + { + using var scope = _serviceScopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService().CopyNew(); + + var jobTrigger = context.Trigger.Adapt(); + 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().Where(u => u.TriggerId == jobTrigger.TriggerId && u.JobId == jobTrigger.JobId).ExecuteCommandAsync(); + break; + } + } + + /// + /// 作业触发器运行记录 + /// + /// + /// + public async Task OnExecutionRecordAsync(PersistenceExecutionRecordContext context) + { + using var scope = _serviceScopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService().CopyNew(); + + var jobTriggerRecord = context.Timeline.Adapt(); + await db.Insertable(jobTriggerRecord).ExecuteCommandAsync(); + + await scope.ServiceProvider.GetRequiredService().ClearExpireJobTriggerRecord(jobTriggerRecord); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Job/Dto/JobDetailInput.cs b/Admin.NET.Core/Service/Job/Dto/JobDetailInput.cs new file mode 100644 index 0000000..24ff520 --- /dev/null +++ b/Admin.NET.Core/Service/Job/Dto/JobDetailInput.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class JobDetailInput +{ + /// + /// 作业Id + /// + public string JobId { get; set; } +} + +public class PageJobDetailInput : BasePageInput +{ + /// + /// 作业Id + /// + public string JobId { get; set; } + + /// + /// 组名称 + /// + public string GroupName { get; set; } + + /// + /// 描述信息 + /// + public string Description { get; set; } +} + +public class AddJobDetailInput : SysJobDetail +{ + /// + /// 作业Id + /// + [Required(ErrorMessage = "作业Id不能为空"), MinLength(2, ErrorMessage = "作业Id不能少于2个字符")] + public override string JobId { get; set; } +} + +public class UpdateJobDetailInput : AddJobDetailInput +{ +} + +public class DeleteJobDetailInput : JobDetailInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Job/Dto/JobDetailOutput.cs b/Admin.NET.Core/Service/Job/Dto/JobDetailOutput.cs new file mode 100644 index 0000000..a6efa85 --- /dev/null +++ b/Admin.NET.Core/Service/Job/Dto/JobDetailOutput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class JobDetailOutput +{ + /// + /// 作业信息 + /// + public SysJobDetail JobDetail { get; set; } + + /// + /// 触发器集合 + /// + public List JobTriggers { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Job/Dto/JobTriggerInput.cs b/Admin.NET.Core/Service/Job/Dto/JobTriggerInput.cs new file mode 100644 index 0000000..334f2ba --- /dev/null +++ b/Admin.NET.Core/Service/Job/Dto/JobTriggerInput.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class JobTriggerInput +{ + /// + /// 作业Id + /// + public string JobId { get; set; } + + /// + /// 触发器Id + /// + public string TriggerId { get; set; } +} + +public class AddJobTriggerInput : SysJobTrigger +{ + /// + /// 作业Id + /// + [Required(ErrorMessage = "作业Id不能为空"), MinLength(2, ErrorMessage = "作业Id不能少于2个字符")] + public override string JobId { get; set; } + + /// + /// 触发器Id + /// + [Required(ErrorMessage = "触发器Id不能为空"), MinLength(2, ErrorMessage = "触发器Id不能少于2个字符")] + public override string TriggerId { get; set; } +} + +public class UpdateJobTriggerInput : AddJobTriggerInput +{ +} + +public class DeleteJobTriggerInput : JobTriggerInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Job/Dto/JobTriggerRecordInput.cs b/Admin.NET.Core/Service/Job/Dto/JobTriggerRecordInput.cs new file mode 100644 index 0000000..2b909b2 --- /dev/null +++ b/Admin.NET.Core/Service/Job/Dto/JobTriggerRecordInput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageJobTriggerRecordInput : BasePageInput +{ + /// + /// 作业Id + /// + public string JobId { get; set; } + + /// + /// 触发器Id + /// + public string TriggerId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Job/JobClusterServer.cs b/Admin.NET.Core/Service/Job/JobClusterServer.cs new file mode 100644 index 0000000..a7156b1 --- /dev/null +++ b/Admin.NET.Core/Service/Job/JobClusterServer.cs @@ -0,0 +1,98 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 作业集群控制 +/// +public class JobClusterServer : IJobClusterServer +{ + private static readonly SqlSugarRepository _sysJobClusterRep = App.GetRequiredService>(); + private readonly Random _random = new(DateTime.Now.Millisecond); + + /// + /// 当前作业调度器启动通知 + /// + /// 作业集群服务上下文 + 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(); + } + } + + /// + /// 等待被唤醒 + /// + /// 作业集群服务上下文 + /// + 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().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 { } + } + } + + /// + /// 当前作业调度器停止通知 + /// + /// 作业集群服务上下文 + public async void Stop(JobClusterContext context) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed + await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId); + } + + /// + /// 当前作业调度器宕机 + /// + /// 作业集群服务上下文 + public async void Crash(JobClusterContext context) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed + await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId); + } + + /// + /// 指示集群可以工作 + /// + /// 集群 Id + /// + private static async Task WorkNowAsync(string clusterId) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working + await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Working }, u => u.ClusterId == clusterId); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Job/JobMonitor.cs b/Admin.NET.Core/Service/Job/JobMonitor.cs new file mode 100644 index 0000000..3585ba8 --- /dev/null +++ b/Admin.NET.Core/Service/Job/JobMonitor.cs @@ -0,0 +1,45 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 作业执行监视器 +/// +public class JobMonitor : IJobMonitor +{ + private readonly SysConfigService _sysConfigService; + private readonly IEventPublisher _eventPublisher; + private readonly ILogger _logger; + + public JobMonitor(IServiceScopeFactory serviceScopeFactory, IEventPublisher eventPublisher, ILogger logger) + { + var serviceScope = serviceScopeFactory.CreateScope(); + _sysConfigService = serviceScope.ServiceProvider.GetRequiredService(); + _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(ConfigConst.SysErrorMail)) + { + // 将作业异常信息发送到邮件 + await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, exception, stoppingToken); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Job/SysJobService.cs b/Admin.NET.Core/Service/Job/SysJobService.cs new file mode 100644 index 0000000..aad0bb7 --- /dev/null +++ b/Admin.NET.Core/Service/Job/SysJobService.cs @@ -0,0 +1,398 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统作业任务服务 🧩 +/// +[ApiDescriptionSettings(Order = 320, Description = "作业任务")] +public class SysJobService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysJobDetailRep; + private readonly SqlSugarRepository _sysJobTriggerRep; + private readonly SqlSugarRepository _sysJobTriggerRecordRep; + private readonly SqlSugarRepository _sysJobClusterRep; + private readonly ISchedulerFactory _schedulerFactory; + private readonly DynamicJobCompiler _dynamicJobCompiler; + + public SysJobService(SqlSugarRepository sysJobDetailRep, + SqlSugarRepository sysJobTriggerRep, + SqlSugarRepository sysJobTriggerRecordRep, + SqlSugarRepository sysJobClusterRep, + ISchedulerFactory schedulerFactory, + DynamicJobCompiler dynamicJobCompiler) + { + _sysJobDetailRep = sysJobDetailRep; + _sysJobTriggerRep = sysJobTriggerRep; + _sysJobTriggerRecordRep = sysJobTriggerRecordRep; + _sysJobClusterRep = sysJobClusterRep; + _schedulerFactory = schedulerFactory; + _dynamicJobCompiler = dynamicJobCompiler; + } + + /// + /// 获取作业分页列表 ⏰ + /// + [DisplayName("获取作业分页列表")] + public async Task> 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; + } + + /// + /// 获取作业组名称集合 ⏰ + /// + [DisplayName("获取作业组名称集合")] + public async Task> ListJobGroup() + { + return await _sysJobDetailRep.AsQueryable().Distinct().Select(e => e.GroupName).ToListAsync(); + } + + /// + /// 添加作业 ⏰ + /// + /// + [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()).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(); + } + + /// + /// 更新作业 ⏰ + /// + /// + [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); + } + + /// + /// 删除作业 ⏰ + /// + /// + [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); + } + + /// + /// 获取触发器列表 ⏰ + /// + [DisplayName("获取触发器列表")] + public async Task> GetJobTriggerList([FromQuery] JobDetailInput input) + { + return await _sysJobTriggerRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId.Contains(input.JobId)) + .ToListAsync(); + } + + /// + /// 添加触发器 ⏰ + /// + /// + [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(); + jobTrigger.Args = "[" + jobTrigger.Args + "]"; + + var scheduler = _schedulerFactory.GetJob(input.JobId); + scheduler?.AddTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger)); + } + + /// + /// 更新触发器 ⏰ + /// + /// + [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(); + 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)); + } + + /// + /// 删除触发器 ⏰ + /// + /// + [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); + } + + /// + /// 暂停所有作业 ⏰ + /// + /// + [DisplayName("暂停所有作业")] + public void PauseAllJob() + { + _schedulerFactory.PauseAll(); + } + + /// + /// 启动所有作业 ⏰ + /// + /// + [DisplayName("启动所有作业")] + public void StartAllJob() + { + _schedulerFactory.StartAll(); + } + + /// + /// 暂停作业 ⏰ + /// + [DisplayName("暂停作业")] + public void PauseJob(JobDetailInput input) + { + _schedulerFactory.TryPauseJob(input.JobId, out _); + } + + /// + /// 启动作业 ⏰ + /// + [DisplayName("启动作业")] + public void StartJob(JobDetailInput input) + { + _schedulerFactory.TryStartJob(input.JobId, out _); + } + + /// + /// 取消作业 ⏰ + /// + [DisplayName("取消作业")] + public void CancelJob(JobDetailInput input) + { + _schedulerFactory.TryCancelJob(input.JobId, out _); + } + + /// + /// 执行作业 ⏰ + /// + /// + [DisplayName("执行作业")] + public void RunJob(JobDetailInput input) + { + if (_schedulerFactory.TryRunJob(input.JobId, out _) != ScheduleResult.Succeed) throw Oops.Oh(ErrorCodeEnum.D1705); + } + + /// + /// 暂停触发器 ⏰ + /// + [DisplayName("暂停触发器")] + public void PauseTrigger(JobTriggerInput input) + { + var scheduler = _schedulerFactory.GetJob(input.JobId); + scheduler?.PauseTrigger(input.TriggerId); + } + + /// + /// 启动触发器 ⏰ + /// + [DisplayName("启动触发器")] + public void StartTrigger(JobTriggerInput input) + { + var scheduler = _schedulerFactory.GetJob(input.JobId); + scheduler?.StartTrigger(input.TriggerId); + } + + /// + /// 强制唤醒作业调度器 ⏰ + /// + [DisplayName("强制唤醒作业调度器")] + public void CancelSleep() + { + _schedulerFactory.CancelSleep(); + } + + /// + /// 强制触发所有作业持久化 ⏰ + /// + [DisplayName("强制触发所有作业持久化")] + public void PersistAll() + { + _schedulerFactory.PersistAll(); + } + + /// + /// 获取集群列表 ⏰ + /// + [DisplayName("获取集群列表")] + public async Task> GetJobClusterList() + { + return await _sysJobClusterRep.GetListAsync(); + } + + /// + /// 获取作业触发器运行记录分页列表 ⏰ + /// + [DisplayName("获取作业触发器运行记录分页列表")] + public async Task> 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); + } + + /// + /// 清空作业触发器运行记录 🔖 + /// + /// + [ApiDescriptionSettings(Name = "ClearJobTriggerRecord"), HttpPost] + [DisplayName("清空作业触发器运行记录")] + public void ClearJobTriggerRecord() + { + _sysJobTriggerRecordRep.AsSugarClient().DbMaintenance.TruncateTable(); + } + + /// + /// 清空不保留的作业触发器运行记录 🔖 + /// + /// + [NonAction] + [DisplayName("清空过期的作业触发器运行记录")] + public async Task ClearExpireJobTriggerRecord(SysJobTriggerRecord input) + { + int keepRecords = 30;//保留记录条数 + // 使用CopyNew()创建新的数据库连接实例,避免连接冲突 + var db = _sysJobTriggerRecordRep.AsSugarClient().CopyNew(); + await db.Deleteable().In(it => it.Id, + db.Queryable() + .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(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Lang/Dto/SysLangDto.cs b/Admin.NET.Core/Service/Lang/Dto/SysLangDto.cs new file mode 100644 index 0000000..07f20e1 --- /dev/null +++ b/Admin.NET.Core/Service/Lang/Dto/SysLangDto.cs @@ -0,0 +1,108 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 语言输出参数 +/// +public class SysLangDto +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 语言名称 + /// + public string Name { get; set; } + + /// + /// 语言代码 + /// + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + public WeekEnum WeekStart { get; set; } + + /// + /// 分组符号 + /// + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + public bool Active { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Lang/Dto/SysLangInput.cs b/Admin.NET.Core/Service/Lang/Dto/SysLangInput.cs new file mode 100644 index 0000000..19b1e58 --- /dev/null +++ b/Admin.NET.Core/Service/Lang/Dto/SysLangInput.cs @@ -0,0 +1,413 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 语言基础输入参数 +/// +public class SysLangBaseInput +{ + /// + /// 主键Id + /// + public virtual long? Id { get; set; } + + /// + /// 语言名称 + /// + [Required(ErrorMessage = "语言名称不能为空")] + public virtual string Name { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + public virtual string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [Required(ErrorMessage = "ISO 语言代码不能为空")] + public virtual string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [Required(ErrorMessage = "URL 语言代码不能为空")] + public virtual string UrlCode { get; set; } + + /// + /// 书写方向 + /// + [Required(ErrorMessage = "书写方向不能为空")] + public virtual DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + [Required(ErrorMessage = "日期格式不能为空")] + public virtual string DateFormat { get; set; } + + /// + /// 时间格式 + /// + [Required(ErrorMessage = "时间格式不能为空")] + public virtual string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + [Required(ErrorMessage = "每周起始日不能为空")] + public virtual WeekEnum? WeekStart { get; set; } + + /// + /// 分组符号 + /// + [Required(ErrorMessage = "分组符号不能为空")] + public virtual string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [Required(ErrorMessage = "小数点符号不能为空")] + public virtual string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + public virtual string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [Required(ErrorMessage = "是否启用不能为空")] + public virtual bool? Active { get; set; } +} + +/// +/// 多语言分页查询输入参数 +/// +public class PageSysLangInput : BasePageInput +{ + /// + /// 语言名称 + /// + public string Name { get; set; } + + /// + /// 语言代码 + /// + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + public string UrlCode { get; set; } + + /// + /// 是否启用 + /// + public bool? Active { get; set; } + + /// + /// 选中主键列表 + /// + public List SelectKeyList { get; set; } +} + +/// +/// 多语言增加输入参数 +/// +public class AddSysLangInput +{ + /// + /// 语言名称 + /// + [Required(ErrorMessage = "语言名称不能为空")] + [MaxLength(255, ErrorMessage = "语言名称字符长度不能超过255")] + public string Name { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")] + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [Required(ErrorMessage = "ISO 语言代码不能为空")] + [MaxLength(255, ErrorMessage = "ISO 语言代码字符长度不能超过255")] + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [Required(ErrorMessage = "URL 语言代码不能为空")] + [MaxLength(255, ErrorMessage = "URL 语言代码字符长度不能超过255")] + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + [Required(ErrorMessage = "书写方向不能为空")] + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + [Required(ErrorMessage = "日期格式不能为空")] + [MaxLength(255, ErrorMessage = "日期格式字符长度不能超过255")] + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + [Required(ErrorMessage = "时间格式不能为空")] + [MaxLength(255, ErrorMessage = "时间格式字符长度不能超过255")] + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + [Required(ErrorMessage = "每周起始日不能为空")] + public WeekEnum? WeekStart { get; set; } + + /// + /// 分组符号 + /// + [Required(ErrorMessage = "分组符号不能为空")] + [MaxLength(255, ErrorMessage = "分组符号字符长度不能超过255")] + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [Required(ErrorMessage = "小数点符号不能为空")] + [MaxLength(255, ErrorMessage = "小数点符号字符长度不能超过255")] + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + [MaxLength(255, ErrorMessage = "千分位分隔符字符长度不能超过255")] + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [Required(ErrorMessage = "是否启用不能为空")] + public bool? Active { get; set; } +} + +/// +/// 多语言删除输入参数 +/// +public class DeleteSysLangInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long? Id { get; set; } +} + +/// +/// 多语言更新输入参数 +/// +public class UpdateSysLangInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long? Id { get; set; } + + /// + /// 语言名称 + /// + [Required(ErrorMessage = "语言名称不能为空")] + [MaxLength(255, ErrorMessage = "语言名称字符长度不能超过255")] + public string Name { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")] + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [Required(ErrorMessage = "ISO 语言代码不能为空")] + [MaxLength(255, ErrorMessage = "ISO 语言代码字符长度不能超过255")] + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [Required(ErrorMessage = "URL 语言代码不能为空")] + [MaxLength(255, ErrorMessage = "URL 语言代码字符长度不能超过255")] + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + [Required(ErrorMessage = "书写方向不能为空")] + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + [Required(ErrorMessage = "日期格式不能为空")] + [MaxLength(255, ErrorMessage = "日期格式字符长度不能超过255")] + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + [Required(ErrorMessage = "时间格式不能为空")] + [MaxLength(255, ErrorMessage = "时间格式字符长度不能超过255")] + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + [Required(ErrorMessage = "每周起始日不能为空")] + public WeekEnum? WeekStart { get; set; } + + /// + /// 分组符号 + /// + [Required(ErrorMessage = "分组符号不能为空")] + [MaxLength(255, ErrorMessage = "分组符号字符长度不能超过255")] + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [Required(ErrorMessage = "小数点符号不能为空")] + [MaxLength(255, ErrorMessage = "小数点符号字符长度不能超过255")] + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + [MaxLength(255, ErrorMessage = "千分位分隔符字符长度不能超过255")] + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [Required(ErrorMessage = "是否启用不能为空")] + public bool? Active { get; set; } +} + +/// +/// 多语言主键查询输入参数 +/// +public class QueryByIdSysLangInput : DeleteSysLangInput +{ +} + +/// +/// 多语言数据导入实体 +/// +[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)] +public class ImportSysLangInput : BaseImportInput +{ + /// + /// 语言名称 + /// + [ImporterHeader(Name = "*语言名称")] + [ExporterHeader("*语言名称", Format = "", Width = 25, IsBold = true)] + public string Name { get; set; } + + /// + /// 语言代码 + /// + [ImporterHeader(Name = "*语言代码")] + [ExporterHeader("*语言代码", Format = "", Width = 25, IsBold = true)] + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [ImporterHeader(Name = "*ISO 语言代码")] + [ExporterHeader("*ISO 语言代码", Format = "", Width = 25, IsBold = true)] + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [ImporterHeader(Name = "*URL 语言代码")] + [ExporterHeader("*URL 语言代码", Format = "", Width = 25, IsBold = true)] + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + [ImporterHeader(Name = "*书写方向")] + [ExporterHeader("*书写方向", Format = "", Width = 25, IsBold = true)] + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + [ImporterHeader(Name = "*日期格式")] + [ExporterHeader("*日期格式", Format = "", Width = 25, IsBold = true)] + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + [ImporterHeader(Name = "*时间格式")] + [ExporterHeader("*时间格式", Format = "", Width = 25, IsBold = true)] + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + [ImporterHeader(Name = "*每周起始日")] + [ExporterHeader("*每周起始日", Format = "", Width = 25, IsBold = true)] + public WeekEnum? WeekStart { get; set; } + + /// + /// 分组符号 + /// + [ImporterHeader(Name = "*分组符号")] + [ExporterHeader("*分组符号", Format = "", Width = 25, IsBold = true)] + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [ImporterHeader(Name = "*小数点符号")] + [ExporterHeader("*小数点符号", Format = "", Width = 25, IsBold = true)] + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + [ImporterHeader(Name = "千分位分隔符")] + [ExporterHeader("千分位分隔符", Format = "", Width = 25, IsBold = true)] + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [ImporterHeader(Name = "*是否启用")] + [ExporterHeader("*是否启用", Format = "", Width = 25, IsBold = true)] + public bool? Active { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Lang/Dto/SysLangOutput.cs b/Admin.NET.Core/Service/Lang/Dto/SysLangOutput.cs new file mode 100644 index 0000000..a4cf56b --- /dev/null +++ b/Admin.NET.Core/Service/Lang/Dto/SysLangOutput.cs @@ -0,0 +1,117 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! +namespace Admin.NET.Core; + +/// +/// 语言输出参数 +/// +public class SysLangOutput +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 语言名称 + /// + public string Name { get; set; } + + /// + /// 语言代码 + /// + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + public WeekEnum WeekStart { get; set; } + + /// + /// 分组符号 + /// + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + public bool Active { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } +} + +/// +/// 多语言数据导入模板实体 +/// +public class ExportSysLangOutput : ImportSysLangInput +{ + [ImporterHeader(IsIgnore = true)] + [ExporterHeader(IsIgnore = true)] + public override string Error { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Lang/SysLangService.cs b/Admin.NET.Core/Service/Lang/SysLangService.cs new file mode 100644 index 0000000..c3c5c09 --- /dev/null +++ b/Admin.NET.Core/Service/Lang/SysLangService.cs @@ -0,0 +1,113 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 语言服务 🧩 +/// +[ApiDescriptionSettings(Order = 100, Description = "语言服务")] +public partial class SysLangService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLangRep; + + public SysLangService(SqlSugarRepository sysLangRep) + { + _sysLangRep = sysLangRep; + } + + /// + /// 分页查询语言 🔖 + /// + /// + /// + [DisplayName("分页查询语言")] + [ApiDescriptionSettings(Name = "Page"), HttpPost] + public async Task> 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(); + return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取语言详情 ℹ️ + /// + /// + /// + [DisplayName("获取语言详情")] + [ApiDescriptionSettings(Name = "Detail"), HttpGet] + public async Task Detail([FromQuery] QueryByIdSysLangInput input) + { + return await _sysLangRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 增加语言 ➕ + /// + /// + /// + [DisplayName("增加语言")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(AddSysLangInput input) + { + var entity = input.Adapt(); + return await _sysLangRep.InsertAsync(entity) ? entity.Id : 0; + } + + /// + /// 更新语言 ✏️ + /// + /// + /// + [DisplayName("更新语言")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(UpdateSysLangInput input) + { + var entity = input.Adapt(); + await _sysLangRep.AsUpdateable(entity) + .ExecuteCommandAsync(); + } + + /// + /// 删除语言 ❌ + /// + /// + /// + [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); //真删除 + } + + /// + /// 获取下拉列表数据 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("获取下拉列表数据")] + [ApiDescriptionSettings(Name = "DropdownData"), HttpPost] + public async Task DropdownData() + { + var data = await _sysLangRep.Context.Queryable() + .Where(m => m.Active == true) + .Select(u => new + { + Code = u.Code, + Value = u.UrlCode, + Label = $"{u.Name}" + }).ToListAsync(); + return data; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/LangText/Dto/SysLangTextDto.cs b/Admin.NET.Core/Service/LangText/Dto/SysLangTextDto.cs new file mode 100644 index 0000000..58c4f6f --- /dev/null +++ b/Admin.NET.Core/Service/LangText/Dto/SysLangTextDto.cs @@ -0,0 +1,73 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 翻译表输出参数 +/// +public class SysLangTextDto +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 所属实体名 + /// + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + public long EntityId { get; set; } + + /// + /// 字段名 + /// + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + public string Content { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/LangText/Dto/SysLangTextInput.cs b/Admin.NET.Core/Service/LangText/Dto/SysLangTextInput.cs new file mode 100644 index 0000000..899ad52 --- /dev/null +++ b/Admin.NET.Core/Service/LangText/Dto/SysLangTextInput.cs @@ -0,0 +1,274 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 翻译表基础输入参数 +/// +public class SysLangTextBaseInput +{ + /// + /// 主键Id + /// + public virtual long? Id { get; set; } + + /// + /// 所属实体名 + /// + [Required(ErrorMessage = "所属实体名不能为空")] + public virtual string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [Required(ErrorMessage = "所属实体ID不能为空")] + public virtual long? EntityId { get; set; } + + /// + /// 字段名 + /// + [Required(ErrorMessage = "字段名不能为空")] + public virtual string FieldName { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + public virtual string LangCode { get; set; } + + /// + /// 翻译内容 + /// + [Required(ErrorMessage = "翻译内容不能为空")] + public virtual string Content { get; set; } +} + +/// +/// 翻译表分页查询输入参数 +/// +public class PageSysLangTextInput : BasePageInput +{ + /// + /// 所属实体名 + /// + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + public string Content { get; set; } + + /// + /// 选中主键列表 + /// + public List SelectKeyList { get; set; } +} + +/// +/// 翻译表增加输入参数 +/// +public class AddSysLangTextInput +{ + /// + /// 所属实体名 + /// + [Required(ErrorMessage = "所属实体名不能为空")] + [MaxLength(255, ErrorMessage = "所属实体名字符长度不能超过255")] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [Required(ErrorMessage = "所属实体ID不能为空")] + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + [Required(ErrorMessage = "字段名不能为空")] + [MaxLength(255, ErrorMessage = "字段名字符长度不能超过255")] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")] + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + [Required(ErrorMessage = "翻译内容不能为空")] + public string Content { get; set; } +} + +/// +/// 翻译表输入参数 +/// +public class ListSysLangTextInput +{ + /// + /// 所属实体名 + /// + [Required(ErrorMessage = "所属实体名不能为空")] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [Required(ErrorMessage = "所属实体ID不能为空")] + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + [Required(ErrorMessage = "字段名不能为空")] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; set; } +} + +/// +/// 翻译表删除输入参数 +/// +public class DeleteSysLangTextInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long? Id { get; set; } +} + +/// +/// 翻译表更新输入参数 +/// +public class UpdateSysLangTextInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long? Id { get; set; } + + /// + /// 所属实体名 + /// + [Required(ErrorMessage = "所属实体名不能为空")] + [MaxLength(255, ErrorMessage = "所属实体名字符长度不能超过255")] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [Required(ErrorMessage = "所属实体ID不能为空")] + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + [Required(ErrorMessage = "字段名不能为空")] + [MaxLength(255, ErrorMessage = "字段名字符长度不能超过255")] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")] + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + [Required(ErrorMessage = "翻译内容不能为空")] + public string Content { get; set; } +} + +/// +/// 翻译表主键查询输入参数 +/// +public class QueryByIdSysLangTextInput : DeleteSysLangTextInput +{ +} + +/// +/// 翻译表数据导入实体 +/// +[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)] +public class ImportSysLangTextInput : BaseImportInput +{ + /// + /// 所属实体名 + /// + [ImporterHeader(Name = "*所属实体名")] + [ExporterHeader("*所属实体名", Format = "", Width = 25, IsBold = true)] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [ImporterHeader(Name = "*所属实体ID")] + [ExporterHeader("*所属实体ID", Format = "", Width = 25, IsBold = true)] + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + [ImporterHeader(Name = "*字段名")] + [ExporterHeader("*字段名", Format = "", Width = 25, IsBold = true)] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + [ImporterHeader(Name = "*语言代码")] + [ExporterHeader("*语言代码", Format = "", Width = 25, IsBold = true)] + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + [ImporterHeader(Name = "*翻译内容")] + [ExporterHeader("*翻译内容", Format = "", Width = 25, IsBold = true)] + public string Content { get; set; } +} + +/// +/// +/// +public class AiTranslateTextInput +{ + /// + /// 原文 + /// + public string OriginalText { get; set; } + + /// + /// 目标语言 + /// + public string TargetLang { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/LangText/Dto/SysLangTextOutput.cs b/Admin.NET.Core/Service/LangText/Dto/SysLangTextOutput.cs new file mode 100644 index 0000000..0a43668 --- /dev/null +++ b/Admin.NET.Core/Service/LangText/Dto/SysLangTextOutput.cs @@ -0,0 +1,82 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! +namespace Admin.NET.Core; + +/// +/// 翻译表输出参数 +/// +public class SysLangTextOutput +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 所属实体名 + /// + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + public long EntityId { get; set; } + + /// + /// 字段名 + /// + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + public string Content { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } +} + +/// +/// 翻译表数据导入模板实体 +/// +public class ExportSysLangTextOutput : ImportSysLangTextInput +{ + [ImporterHeader(IsIgnore = true)] + [ExporterHeader(IsIgnore = true)] + public override string Error { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/LangText/SysLangTextCacheService.cs b/Admin.NET.Core/Service/LangText/SysLangTextCacheService.cs new file mode 100644 index 0000000..d6c39a6 --- /dev/null +++ b/Admin.NET.Core/Service/LangText/SysLangTextCacheService.cs @@ -0,0 +1,343 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class LangFieldMap +{ + /// 实体名,如 Product + public string EntityName { get; set; } + + /// 字段名,如 Name/Description + public string FieldName { get; set; } + + /// 如何取主键ID + public Func IdSelector { get; set; } + + /// 如何写回翻译值 + public Action SetTranslatedValue { get; set; } +} + +/// +/// 翻译缓存服务 🧩 +/// +[ApiDescriptionSettings(Order = 100, Description = "翻译缓存服务")] +public class SysLangTextCacheService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly SqlSugarRepository _sysLangTextRep; + private TimeSpan expireSeconds = TimeSpan.FromHours(1); + + public SysLangTextCacheService( + SysCacheService sysCacheService, + SqlSugarRepository sysLangTextRep) + { + _sysCacheService = sysCacheService; + _sysLangTextRep = sysLangTextRep; + } + + private string BuildKey(string entityName, string fieldName, long entityId, string langCode) + { + return $"LangCache_{entityName}_{fieldName}_{entityId}_{langCode}"; + } + + /// + /// 【单条翻译获取】 + /// 根据实体类型、字段、主键ID 和语言编码获取翻译内容。
+ /// 适用于:小表(如菜单、字典),可设置较长缓存时间。
+ ///
+ /// 【示例】
+ /// var content = await _sysLangTextCacheService.GetTranslation("Product", "Name", 123, "en-US"); + ///
+ /// 实体名称,如 "Product" + /// 字段名称,如 "Name" + /// 实体主键ID + /// 语言编码,如 "zh-CN" + /// 翻译后的内容(若无则返回 null 或空) + [NonAction] + public async Task GetTranslation(string entityName, string fieldName, long entityId, string langCode) + { + var key = BuildKey(entityName, fieldName, entityId, langCode); + var value = _sysCacheService.Get(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; + } + + /// + /// 根据实体类型、字段、主键ID 和语言编码获取翻译实体 + /// + /// 实体名称 + /// 字段名称 + /// 实体主键ID + /// 语言编码 + /// + [NonAction] + public async Task GetTranslationEntity(string entityName, string fieldName, long entityId, string langCode) + { + var key = BuildKey(entityName, fieldName, entityId, langCode) + "_entity"; + var value = _sysCacheService.Get(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; + } + + /// + /// 【批量翻译获取】
+ /// 根据实体、字段和一批主键ID获取对应翻译内容,自动从缓存或数据库获取。
+ /// 适用于:SKU、多商品、批量字典等需要高效批量获取的场景。
+ /// + /// 【示例】
+ /// var dict = await _sysLangTextCacheService.GetTranslations("SKU", "Name", skuIds, "en_US"); + ///
+ /// 实体名称 + /// 字段名称 + /// 主键ID集合 + /// 语言编码 + /// 主键ID到翻译内容的字典 + [NonAction] + public async Task> GetTranslations(string entityName, string fieldName, List entityIds, string langCode) + { + var result = new Dictionary(); + var missingIds = new HashSet(); // 用 HashSet 提高后面 Contains 的性能 + + foreach (var id in entityIds.Distinct()) // 先去重,防止重复缓存 Key + { + var key = BuildKey(entityName, fieldName, id, langCode); + var value = _sysCacheService.Get(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; + } + + /// + /// 【列表翻译】
+ /// 按配置把同一字段的翻译写回到实体列表中。内部会调用批量翻译接口。
+ ///
+ /// 【示例】
+ /// await _sysLangTextCacheService.TranslateList(products, "Product", "Name", p => p.Id, (p, val) => p.Name = val, "zh-CN"); + ///
+ /// 实体类型 + /// 待翻译的实体列表 + /// 实体名称 + /// 字段名称 + /// 用于取出主键ID的表达式 + /// 写回翻译值的委托 + /// 语言编码 + /// 翻译后的实体列表(引用传递) + [NonAction] + public async Task> TranslateList(List list, string entityName, string fieldName, Func idSelector, Action 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; + } + + /// + /// 【多字段批量翻译】 + /// 对列表中的实体对象,按配置的字段映射进行多字段翻译处理。
+ /// 常用于:菜单多语言、商品多语言、SKU多语言等需要多字段翻译的场景。

+ /// ✅ 特点:
+ /// 1️⃣ 可同时翻译同一实体的多个字段(如 Name、Description、Title 等)
+ /// 2️⃣ 内部先尝试从缓存读取,如缓存未命中则批量查询数据库,并自动写回缓存
+ /// 3️⃣ 引用传递,直接对原实体对象赋值,无需额外返回

+ /// 【使用示例】:
+ /// + /// var fields = new List<LangFieldMap<Product>> + /// { + /// new LangFieldMap<Product> { + /// EntityName = "Product", + /// FieldName = "Name", + /// IdSelector = p => p.Id, + /// SetTranslatedValue = (p, val) => p.Name = val + /// }, + /// new LangFieldMap<Product> { + /// EntityName = "Product", + /// FieldName = "Description", + /// IdSelector = p => p.Id, + /// SetTranslatedValue = (p, val) => p.Description = val + /// } + /// }; + /// await _sysLangTextCacheService.TranslateMultiFields(products, fields, "zh-CN"); + /// + ///
+ /// 要翻译的实体类型,如 Product/Menu/SKU 等 + /// 需要翻译的实体对象列表 + /// 需要翻译的字段映射集合,支持多个字段 + /// 语言编码,如 "zh-CN"、"en-US"、"it-IT" 等 + /// 翻译后的实体列表(引用传递,原对象已直接赋值) + [NonAction] + public async Task> TranslateMultiFields( + List list, + List> fields, + string langCode) + { + var keyToField = new Dictionary FieldMap)>(); + var missingKeys = new List(); + + // 先尝试从缓存读取 + 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(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(); + + // 分批查询,每组单独查询 + 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; + } + + /// + /// 删除缓存 + /// + /// + /// + /// + /// + public void DeleteCache(string entityName, string fieldName, long entityId, string langCode) + { + var key = BuildKey(entityName, fieldName, entityId, langCode); + _sysCacheService.Remove(key); + } + + /// + /// 更新缓存 + /// + /// + /// + /// + /// + /// + 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); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/LangText/SysLangTextService.cs b/Admin.NET.Core/Service/LangText/SysLangTextService.cs new file mode 100644 index 0000000..07910a1 --- /dev/null +++ b/Admin.NET.Core/Service/LangText/SysLangTextService.cs @@ -0,0 +1,436 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core.Service; + +/// +/// 翻译服务 🧩 +/// +[ApiDescriptionSettings(Order = 100, Description = "翻译服务")] +public partial class SysLangTextService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLangTextRep; + private readonly ISqlSugarClient _sqlSugarClient; + private readonly SysLangTextCacheService _sysLangTextCacheService; + + public SysLangTextService( + SqlSugarRepository sysLangTextRep, + SysLangTextCacheService sysLangTextCacheService, + ISqlSugarClient sqlSugarClient) + { + _sysLangTextRep = sysLangTextRep; + _sqlSugarClient = sqlSugarClient; + _sysLangTextCacheService = sysLangTextCacheService; + } + + /// + /// 分页查询翻译表 🔖 + /// + /// + /// + [DisplayName("分页查询翻译表")] + [ApiDescriptionSettings(Name = "Page"), HttpPost] + public async Task> 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(); + return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize); + } + + [DisplayName("获取翻译表")] + [ApiDescriptionSettings(Name = "List"), HttpPost] + public async Task> 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(); + return await query.ToListAsync(); + } + + /// + /// 获取翻译表详情 ℹ️ + /// + /// + /// + [DisplayName("获取翻译表详情")] + [ApiDescriptionSettings(Name = "Detail"), HttpGet] + public async Task Detail([FromQuery] QueryByIdSysLangTextInput input) + { + return await _sysLangTextRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 增加翻译表 ➕ + /// + /// + /// + [DisplayName("增加翻译表")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(AddSysLangTextInput input) + { + var entity = input.Adapt(); + return await _sysLangTextRep.InsertAsync(entity) ? entity.Id : 0; + } + + /// + /// 更新翻译表 ✏️ + /// + /// + /// + [DisplayName("更新翻译表")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(UpdateSysLangTextInput input) + { + var entity = input.Adapt(); + await _sysLangTextRep.AsUpdateable(entity) + .ExecuteCommandAsync(); + _sysLangTextCacheService.UpdateCache(entity.EntityName, entity.FieldName, entity.EntityId, entity.LangCode, entity.Content); + } + + /// + /// 删除翻译表 ❌ + /// + /// + /// + [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); + } + + /// + /// 批量删除翻译表 ❌ + /// + /// + /// + [DisplayName("批量删除翻译表")] + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + public async Task BatchDelete([Required(ErrorMessage = "主键列表不能为空")] List input) + { + var exp = Expressionable.Create(); + 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(); + + /// + /// 批量保存翻译表 ✏️ + /// + /// + /// + [DisplayName("批量保存翻译表")] + [ApiDescriptionSettings(Name = "BatchSave"), HttpPost] + public void BatchSave([Required(ErrorMessage = "列表不能为空")] List 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>(); + + 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())}"); + } + } + } + + /// + /// 导出翻译表记录 🔖 + /// + /// + /// + [DisplayName("导出翻译表记录")] + [ApiDescriptionSettings(Name = "Export"), HttpPost, NonUnify] + public async Task Export(PageSysLangTextInput input) + { + var list = (await Page(input)).Items?.Adapt>() ?? new(); + if (input.SelectKeyList?.Count > 0) list = list.Where(x => input.SelectKeyList.Contains(x.Id)).ToList(); + return ExcelHelper.ExportTemplate(list, "翻译表导出记录"); + } + + /// + /// 下载翻译表数据导入模板 ⬇️ + /// + /// + [DisplayName("下载翻译表数据导入模板")] + [ApiDescriptionSettings(Name = "Import"), HttpGet, NonUnify] + public IActionResult DownloadTemplate() + { + return ExcelHelper.ExportTemplate(new List(), "翻译表导入模板"); + } + + private static readonly object _sysLangTextImportLock = new object(); + + /// + /// 导入翻译表记录 💾 + /// + /// + [DisplayName("导入翻译表记录")] + [ApiDescriptionSettings(Name = "Import"), HttpPost, NonUnify, UnitOfWork] + public IActionResult ImportData([Required] IFormFile file) + { + lock (_sysLangTextImportLock) + { + var stream = ExcelHelper.ImportData(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>(); + + 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; + } + } + + /// + /// DEEPSEEK 翻译接口 + /// + /// + [DisplayName("DEEPSEEK 翻译接口")] + [ApiDescriptionSettings(Name = "AiTranslateText"), HttpPost] + public async Task AiTranslateText(AiTranslateTextInput input) + { + // 需要先把DeepSeek.example复制改名为DeepSeek.json文件,添加你的 API KEY + var deepSeekOptions = App.GetConfig("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(responseBody); + string errorMsg = errorResponse?.error?.message ?? $"HTTP {response.StatusCode}: {response.ReasonPhrase}"; + throw new HttpRequestException($"翻译API返回错误:{errorMsg}"); + } + + // 解析有效响应 + var result = JsonConvert.DeserializeObject(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; } + } + + /// + /// 生成提示词 + /// + /// + /// + /// + 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等)"; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Log/Dto/ExportLogDto.cs b/Admin.NET.Core/Service/Log/Dto/ExportLogDto.cs new file mode 100644 index 0000000..bd99c03 --- /dev/null +++ b/Admin.NET.Core/Service/Log/Dto/ExportLogDto.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 导出日志数据 +/// +[ExcelExporter(Name = "日志数据", TableStyle = OfficeOpenXml.Table.TableStyles.None, AutoFitAllColumn = true)] +public class ExportLogDto +{ + /// + /// 记录器类别名称 + /// + [ExporterHeader(DisplayName = "记录器类别名称", IsBold = true)] + public string LogName { get; set; } + + /// + /// 日志级别 + /// + [ExporterHeader(DisplayName = "日志级别", IsBold = true)] + public string LogLevel { get; set; } + + /// + /// 事件Id + /// + [ExporterHeader(DisplayName = "事件Id", IsBold = true)] + public string EventId { get; set; } + + /// + /// 日志消息 + /// + [ExporterHeader(DisplayName = "日志消息", IsBold = true)] + public string Message { get; set; } + + /// + /// 异常对象 + /// + [ExporterHeader(DisplayName = "异常对象", IsBold = true)] + public string Exception { get; set; } + + /// + /// 当前状态值 + /// + [ExporterHeader(DisplayName = "当前状态值", IsBold = true)] + public string State { get; set; } + + /// + /// 日志记录时间 + /// + [ExporterHeader(DisplayName = "日志记录时间", IsBold = true)] + public DateTime LogDateTime { get; set; } + + /// + /// 线程Id + /// + [ExporterHeader(DisplayName = "线程Id", IsBold = true)] + public int ThreadId { get; set; } + + /// + /// 请求跟踪Id + /// + [ExporterHeader(DisplayName = "请求跟踪Id", IsBold = true)] + public string TraceId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Log/Dto/LogInput.cs b/Admin.NET.Core/Service/Log/Dto/LogInput.cs new file mode 100644 index 0000000..490c97b --- /dev/null +++ b/Admin.NET.Core/Service/Log/Dto/LogInput.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageOpLogInput : PageVisLogInput +{ + /// + /// 模块名称 + /// + public string? ControllerName { get; set; } +} + +public class PageExLogInput : PageOpLogInput +{ +} + +public class PageVisLogInput : PageLogInput +{ + /// + /// 方法名称 + /// + public string? ActionName { get; set; } +} + +public class PageLogInput : BasePageTimeInput +{ + /// + /// 账号 + /// + public string? Account { get; set; } + + /// + /// 操作用时 + /// + public long? Elapsed { get; set; } + + /// + /// 状态 + /// + public string Status { get; set; } + + /// + /// IP地址 + /// + public string? RemoteIp { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class LogInput +{ + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Log/Dto/LogVisOutput.cs b/Admin.NET.Core/Service/Log/Dto/LogVisOutput.cs new file mode 100644 index 0000000..8e69aaf --- /dev/null +++ b/Admin.NET.Core/Service/Log/Dto/LogVisOutput.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class LogVisOutput +{ + /// + /// 登录地点 + /// + public string Location { get; set; } + + /// + /// 经度 + /// + public double? Longitude { get; set; } + + /// + /// 维度 + /// + public double? Latitude { get; set; } + + /// + /// 真实姓名 + /// + public string RealName { get; set; } + + /// + /// 日志时间 + /// + public DateTime? LogDateTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Log/SysLogDiffService.cs b/Admin.NET.Core/Service/Log/SysLogDiffService.cs new file mode 100644 index 0000000..268480e --- /dev/null +++ b/Admin.NET.Core/Service/Log/SysLogDiffService.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统差异日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 330)] +public class SysLogDiffService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogDiffRep; + private readonly UserManager _userManager; + + public SysLogDiffService(UserManager userManager, SqlSugarRepository sysLogDiffRep) + { + _sysLogDiffRep = sysLogDiffRep; + _userManager = userManager; + } + + /// + /// 获取差异日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取差异日志分页列表")] + public async Task> 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); + } + + /// + /// 获取差异日志详情 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取差异日志详情")] + public async Task GetDetail(long id) + { + return await _sysLogDiffRep.GetFirstAsync(u => u.Id == id); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Log/SysLogExService.cs b/Admin.NET.Core/Service/Log/SysLogExService.cs new file mode 100644 index 0000000..4a93b3e --- /dev/null +++ b/Admin.NET.Core/Service/Log/SysLogExService.cs @@ -0,0 +1,89 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统异常日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 350)] +public class SysLogExService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogExRep; + private readonly UserManager _userManager; + + public SysLogExService(UserManager userManager, SqlSugarRepository sysLogExRep) + { + _sysLogExRep = sysLogExRep; + _userManager = userManager; + } + + /// + /// 获取异常日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取异常日志分页列表")] + public async Task> 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); + } + + /// + /// 获取异常日志详情 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取异常日志详情")] + public async Task GetDetail(long id) + { + return await _sysLogExRep.GetFirstAsync(u => u.Id == id); + } + + /// + /// 清空异常日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Clear"), HttpPost] + [DisplayName("清空异常日志")] + public void Clear() + { + _sysLogExRep.AsSugarClient().DbMaintenance.TruncateTable(); + } + + /// + /// 导出异常日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Export"), NonUnify] + [DisplayName("导出异常日志")] + public async Task 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().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" }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Log/SysLogOpService.cs b/Admin.NET.Core/Service/Log/SysLogOpService.cs new file mode 100644 index 0000000..4324aa5 --- /dev/null +++ b/Admin.NET.Core/Service/Log/SysLogOpService.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统操作日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 360)] +public class SysLogOpService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogOpRep; + private readonly UserManager _userManager; + + public SysLogOpService(UserManager userManager, SqlSugarRepository sysLogOpRep) + { + _sysLogOpRep = sysLogOpRep; + _userManager = userManager; + } + + /// + /// 获取操作日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取操作日志分页列表")] + public async Task> 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); + } + + /// + /// 获取操作日志详情 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取操作日志详情")] + public async Task GetDetail(long id) + { + return await _sysLogOpRep.GetFirstAsync(u => u.Id == id); + } + + /// + /// 清空操作日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Clear"), HttpPost] + [DisplayName("清空操作日志")] + public void Clear() + { + _sysLogOpRep.AsSugarClient().DbMaintenance.TruncateTable(); + } + + /// + /// 导出操作日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Export"), NonUnify] + [DisplayName("导出操作日志")] + public async Task 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().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" }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Log/SysLogVisService.cs b/Admin.NET.Core/Service/Log/SysLogVisService.cs new file mode 100644 index 0000000..c25128a --- /dev/null +++ b/Admin.NET.Core/Service/Log/SysLogVisService.cs @@ -0,0 +1,75 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统访问日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 340)] +public class SysLogVisService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogVisRep; + private readonly UserManager _userManager; + + public SysLogVisService(UserManager userManager, SqlSugarRepository sysLogVisRep) + { + _sysLogVisRep = sysLogVisRep; + _userManager = userManager; + } + + /// + /// 获取访问日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取访问日志分页列表")] + public async Task> 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); + } + + /// + /// 清空访问日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Clear"), HttpPost] + [DisplayName("清空访问日志")] + public void Clear() + { + _sysLogVisRep.AsSugarClient().DbMaintenance.TruncateTable(); + } + + /// + /// 获取访问日志列表 🔖 + /// + /// + [DisplayName("获取访问日志列表")] + public async Task> 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(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Menu/Dto/MenuInput.cs b/Admin.NET.Core/Service/Menu/Dto/MenuInput.cs new file mode 100644 index 0000000..c7c266e --- /dev/null +++ b/Admin.NET.Core/Service/Menu/Dto/MenuInput.cs @@ -0,0 +1,51 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class MenuInput +{ + /// + /// 标题 + /// + public string Title { get; set; } + + /// + /// 菜单类型(1目录 2菜单 3按钮) + /// + public MenuTypeEnum? Type { get; set; } + + /// + /// 租户Id + /// + public virtual long TenantId { get; set; } +} + +public class AddMenuInput : SysMenu +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "菜单名称不能为空")] + public override string Title { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class UpdateMenuInput : AddMenuInput +{ +} + +public class DeleteMenuInput : BaseIdInput +{ +} + +public class MenuStatusInput : BaseStatusInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Menu/Dto/MenuOutput.cs b/Admin.NET.Core/Service/Menu/Dto/MenuOutput.cs new file mode 100644 index 0000000..ab7ad45 --- /dev/null +++ b/Admin.NET.Core/Service/Menu/Dto/MenuOutput.cs @@ -0,0 +1,157 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统菜单返回结果 +/// +public class MenuOutput +{ + /// + /// Id + /// + public long Id { get; set; } + + /// + /// 父Id + /// + public long Pid { get; set; } + + /// + /// 菜单类型(0目录 1菜单 2按钮) + /// + public MenuTypeEnum Type { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 路由地址 + /// + public string Path { get; set; } + + /// + /// 组件路径 + /// + public string Component { get; set; } + + /// + /// 权限标识 + /// + public string Permission { get; set; } + + /// + /// 重定向 + /// + public string Redirect { get; set; } + + /// + /// 排序 + /// + public int OrderNo { get; set; } + + /// + /// 状态 + /// + public StatusEnum Status { get; set; } + + /// + /// 备注 + /// + public string Remark { get; set; } + + /// + /// 创建时间 + /// + public virtual DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + public virtual DateTime UpdateTime { get; set; } + + /// + /// 创建者姓名 + /// + public virtual string CreateUserName { get; set; } + + /// + /// 修改者姓名 + /// + public virtual string UpdateUserName { get; set; } + + /// + /// 菜单Meta + /// + public SysMenuMeta Meta { get; set; } + + /// + /// 菜单子项 + /// + public List Children { get; set; } +} + +/// +/// 菜单Meta配置 +/// +public class SysMenuMeta +{ + /// + /// 标题 + /// + public string Title { get; set; } + + /// + /// 图标 + /// + public string Icon { get; set; } + + /// + /// 是否内嵌 + /// + public bool IsIframe { get; set; } + + /// + /// 外链链接 + /// + public string IsLink { get; set; } + + /// + /// 是否隐藏 + /// + public bool IsHide { get; set; } + + /// + /// 是否缓存 + /// + public bool IsKeepAlive { get; set; } + + /// + /// 是否固定 + /// + public bool IsAffix { get; set; } +} + +/// +/// 配置菜单对象映射 +/// +public class SysMenuMapper : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.ForType() + .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); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Menu/SysMenuService.cs b/Admin.NET.Core/Service/Menu/SysMenuService.cs new file mode 100644 index 0000000..17510f4 --- /dev/null +++ b/Admin.NET.Core/Service/Menu/SysMenuService.cs @@ -0,0 +1,462 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统菜单服务 🧩 +/// +[ApiDescriptionSettings(Order = 450)] +public class SysMenuService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysTenantMenuRep; + private readonly SqlSugarRepository _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 sysTenantMenuRep, + SqlSugarRepository 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; + } + + /// + /// 获取登录菜单树 🔖 + /// + /// + [DisplayName("获取登录菜单树")] + public async Task> GetLoginMenuTree() + { + var sysDefaultLang = App.GetOptions().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> + { + new LangFieldMap + { + 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>(); + } + + /// + /// 获取菜单列表 🔖 + /// + /// + [DisplayName("获取菜单列表")] + public async Task> GetList([FromQuery] MenuInput input) + { + var langCode = _userManager.LangCode; + var menuIdList = _userManager.SuperAdmin || _userManager.SysAdmin ? new List() : 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> + { + new LangFieldMap + { + 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> + { + new LangFieldMap + { + 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(); + } + + /// + /// 增加菜单 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加菜单")] + public async Task 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(); + CheckMenuParam(sysMenu); + + // 保存租户菜单权限 + await _sysMenuRep.InsertAsync(sysMenu); + await _sysTenantMenuRep.InsertAsync(new SysTenantMenu { TenantId = tenantId, MenuId = sysMenu.Id }); + + // 清除缓存 + DeleteMenuCache(); + + return sysMenu.Id; + } + + /// + /// 更新菜单 🔖 + /// + /// + /// + [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(); + 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(); + } + + /// + /// 删除菜单 🔖 + /// + /// + /// + [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(); + } + + /// + /// 设置菜单状态 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("设置菜单状态")] + public virtual async Task 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; + } + + /// + /// 增加和编辑时检查菜单数据 + /// + /// + 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; + } + } + + /// + /// 获取用户拥有按钮权限集合(缓存) 🔖 + /// + /// + [DisplayName("获取按钮权限集合")] + public async Task> GetOwnBtnPermList() + { + var userId = _userManager.UserId; + var permissions = _sysCacheService.Get>(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(), TimeSpan.FromDays(7)); + return new List(); + } + + permissions = await _sysMenuRep.AsQueryable() + .InnerJoinIF(!_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; + } + + /// + /// 获取系统所有按钮权限集合(缓存) + /// + /// + [NonAction] + public async Task> GetAllBtnPermList() + { + var permissions = _sysCacheService.Get>(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; + } + + /// + /// 根据租户id获取构建菜单联表查询实例 + /// + /// + /// + [NonAction] + public (ISugarQueryable query, long tenantId) GetSugarQueryableAndTenantId(long tenantId) + { + if (!_userManager.SuperAdmin) tenantId = _userManager.TenantId; + + // 超管用户菜单范围:种子菜单 + 租户id菜单 + ISugarQueryable query; + if (_userManager.SuperAdmin) + { + if (tenantId <= 0) + { + query = _sysMenuRep.AsQueryable().InnerJoinIF(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(false, (u, t) => true).Where(u => menuIds.Contains(u.Id)); + } + } + else + { + query = _sysMenuRep.AsQueryable().InnerJoinIF(tenantId > 0, (u, t) => t.TenantId == tenantId && u.Id == t.MenuId); + } + + return (query, tenantId); + } + + /// + /// 清除菜单和按钮缓存 + /// + [NonAction] + public void DeleteMenuCache() + { + // _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserMenu); + _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserButton); + } + + /// + /// 获取当前用户菜单Id集合 + /// + /// + [NonAction] + public async Task> GetMenuIdList() + { + var roleIdList = await _sysUserRoleService.GetUserRoleIdList(_userManager.UserId); + return await _sysRoleMenuService.GetRoleMenuIdList(roleIdList); + } + + /// + /// 排除前端存在全选的父级菜单 + /// + /// + [NonAction] + public async Task> ExcludeParentMenuOfFullySelected(List menuIds) + { + // 获取当前用户菜单 + var (query, _) = GetSugarQueryableAndTenantId(0); + var menuList = await query.ToListAsync(); + + // 排除列表,防止前端全选问题 + var exceptList = new List(); + 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(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Message/Dto/MessageInput.cs b/Admin.NET.Core/Service/Message/Dto/MessageInput.cs new file mode 100644 index 0000000..d8d43d6 --- /dev/null +++ b/Admin.NET.Core/Service/Message/Dto/MessageInput.cs @@ -0,0 +1,55 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class MessageInput +{ + /// + /// 接收者用户Id + /// + public long ReceiveUserId { get; set; } + + /// + /// 接收者名称 + /// + public string ReceiveUserName { get; set; } + + /// + /// 用户ID列表 + /// + public List UserIds { get; set; } + + /// + /// 消息标题 + /// + public string Title { get; set; } + + /// + /// 消息类型 + /// + public MessageTypeEnum MessageType { get; set; } + + /// + /// 消息内容 + /// + public string Message { get; set; } + + /// + /// 发送者Id + /// + public string SendUserId { get; set; } + + /// + /// 发送者名称 + /// + public string SendUserName { get; set; } + + /// + /// 发送时间 + /// + public DateTime SendTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Message/Dto/SmsInput.cs b/Admin.NET.Core/Service/Message/Dto/SmsInput.cs new file mode 100644 index 0000000..8a6020f --- /dev/null +++ b/Admin.NET.Core/Service/Message/Dto/SmsInput.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class SmsVerifyCodeInput +{ + /// + /// 手机号码 + /// + /// admin + [Required(ErrorMessage = "手机号码不能为空")] + [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")] + public string Phone { get; set; } + + /// + /// 验证码 + /// + /// 123456 + [Required(ErrorMessage = "验证码不能为空"), MinLength(4, ErrorMessage = "验证码不能少于4个字符")] + public string Code { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Message/SysEmailService.cs b/Admin.NET.Core/Service/Message/SysEmailService.cs new file mode 100644 index 0000000..8065394 --- /dev/null +++ b/Admin.NET.Core/Service/Message/SysEmailService.cs @@ -0,0 +1,62 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using MailKit.Net.Smtp; +using MimeKit; + +namespace Admin.NET.Core.Service; + +/// +/// 系统邮件发送服务 🧩 +/// +[ApiDescriptionSettings(Order = 370)] +public class SysEmailService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysTenantRep; + private readonly EmailOptions _emailOptions; + + public SysEmailService(IOptions emailOptions, SqlSugarRepository sysTenantRep) + { + _emailOptions = emailOptions.Value; + _sysTenantRep = sysTenantRep; + } + + /// + /// 发送邮件 📧 + /// + /// + /// + /// + /// + [DisplayName("发送邮件")] + public async Task SendEmail([Required] string content, string title = "", string toEmail = "") + { + long.TryParse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? SqlSugarConst.DefaultTenantId.ToString(), out var tenantId); + var webTitle = (await _sysTenantRep.GetFirstAsync(u => u.Id == tenantId))?.Title; + title = string.IsNullOrWhiteSpace(title) ? $"{webTitle} 系统邮件" : title; + var message = new MimeMessage(); + + message.From.Add(new MailboxAddress(_emailOptions.DefaultFromEmail, _emailOptions.DefaultFromEmail)); + + message.To.Add(string.IsNullOrWhiteSpace(toEmail) + ? new MailboxAddress(_emailOptions.DefaultToEmail, _emailOptions.DefaultToEmail) + : new MailboxAddress(toEmail, toEmail)); + + message.Subject = title; + message.Body = new TextPart("html") + { + Text = content + }; + + using var client = new SmtpClient(); + client.Connect(_emailOptions.Host, _emailOptions.Port, _emailOptions.EnableSsl); + client.Authenticate(_emailOptions.UserName, _emailOptions.Password); + client.Send(message); + client.Disconnect(true); + + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Message/SysMessageService.cs b/Admin.NET.Core/Service/Message/SysMessageService.cs new file mode 100644 index 0000000..aa0d40d --- /dev/null +++ b/Admin.NET.Core/Service/Message/SysMessageService.cs @@ -0,0 +1,76 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.SignalR; + +namespace Admin.NET.Core.Service; + +/// +/// 系统消息发送服务 🧩 +/// +[ApiDescriptionSettings(Order = 370)] +public class SysMessageService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly IHubContext _chatHubContext; + + public SysMessageService(SysCacheService sysCacheService, + IHubContext chatHubContext) + { + _sysCacheService = sysCacheService; + _chatHubContext = chatHubContext; + } + + /// + /// 发送消息给所有人 🔖 + /// + /// + /// + [DisplayName("发送消息给所有人")] + public async Task SendAllUser(MessageInput input) + { + await _chatHubContext.Clients.All.ReceiveMessage(input); + } + + /// + /// 发送消息给除了发送人的其他人 🔖 + /// + /// + /// + [DisplayName("发送消息给除了发送人的其他人")] + public async Task SendOtherUser(MessageInput input) + { + var hashKey = _sysCacheService.HashGetAll(CacheConst.KeyUserOnline); + var exceptReceiveUsers = hashKey.Where(u => u.Value.UserId == input.ReceiveUserId).Select(u => u.Value).ToList(); + await _chatHubContext.Clients.AllExcept(exceptReceiveUsers.Select(t => t.ConnectionId)).ReceiveMessage(input); + } + + /// + /// 发送消息给某个人 🔖 + /// + /// + /// + [DisplayName("发送消息给某个人")] + public async Task SendUser(MessageInput input) + { + var hashKey = _sysCacheService.HashGetAll(CacheConst.KeyUserOnline); + var receiveUsers = hashKey.Where(u => u.Value.UserId == input.ReceiveUserId).Select(u => u.Value).ToList(); + await receiveUsers.ForEachAsync(u => _chatHubContext.Clients.Client(u.ConnectionId ?? "").ReceiveMessage(input)); + } + + /// + /// 发送消息给某些人 🔖 + /// + /// + /// + [DisplayName("发送消息给某些人")] + public async Task SendUsers(MessageInput input) + { + var hashKey = _sysCacheService.HashGetAll(CacheConst.KeyUserOnline); + var receiveUsers = hashKey.Where(u => input.UserIds.Any(a => a == u.Value.UserId)).Select(u => u.Value).ToList(); + await receiveUsers.ForEachAsync(u => _chatHubContext.Clients.Client(u.ConnectionId ?? "").ReceiveMessage(input)); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Message/SysSmsService.cs b/Admin.NET.Core/Service/Message/SysSmsService.cs new file mode 100644 index 0000000..fea589b --- /dev/null +++ b/Admin.NET.Core/Service/Message/SysSmsService.cs @@ -0,0 +1,308 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using AlibabaCloud.SDK.Dysmsapi20170525.Models; +using TencentCloud.Common; +using TencentCloud.Common.Profile; +using TencentCloud.Sms.V20190711; + +namespace Admin.NET.Core.Service; + +/// +/// 系统短信服务 🧩 +/// +[AllowAnonymous] +[ApiDescriptionSettings(Order = 150)] +public class SysSmsService : IDynamicApiController, ITransient +{ + private readonly SMSOptions _smsOptions; + private readonly SysCacheService _sysCacheService; + + public SysSmsService(IOptions smsOptions, + SysCacheService sysCacheService) + { + _smsOptions = smsOptions.Value; + _sysCacheService = sysCacheService; + } + + /// + /// 发送短信 📨 + /// + /// + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("发送短信")] + public async Task SendSms([Required] string phoneNumber, string templateId = "0") + { + if (_smsOptions.Custom != null && _smsOptions.Custom.Enabled && !string.IsNullOrWhiteSpace(_smsOptions.Custom.ApiUrl)) + { + await CustomSendSms(phoneNumber, templateId); + } + else if (!string.IsNullOrWhiteSpace(_smsOptions.Aliyun.AccessKeyId) && !string.IsNullOrWhiteSpace(_smsOptions.Aliyun.AccessKeySecret)) + { + await AliyunSendSms(phoneNumber, templateId); + } + else + { + await TencentSendSms(phoneNumber, templateId); + } + } + + /// + /// 校验短信验证码 + /// + /// + /// + [AllowAnonymous] + [DisplayName("校验短信验证码")] + public bool VerifyCode(SmsVerifyCodeInput input) + { + var verifyCode = _sysCacheService.Get($"{CacheConst.KeyPhoneVerCode}{input.Phone}"); + + if (string.IsNullOrWhiteSpace(verifyCode)) throw Oops.Oh("验证码不存在或已失效,请重新获取!"); + + if (verifyCode != input.Code) throw Oops.Oh("验证码错误!"); + + return true; + } + + /// + /// 阿里云发送短信 📨 + /// + /// 手机号 + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("阿里云发送短信")] + public async Task AliyunSendSms([Required] string phoneNumber, string templateId = "0") + { + if (!phoneNumber.TryValidate(ValidationTypes.PhoneNumber).IsValid) throw Oops.Oh("请正确填写手机号码"); + + // 生成随机验证码 + var random = new Random(); + var verifyCode = random.Next(100000, 999999); + + var templateParam = new + { + code = verifyCode + }; + + var client = CreateAliyunClient(); + var template = _smsOptions.Aliyun.GetTemplate(templateId); + var sendSmsRequest = new SendSmsRequest + { + PhoneNumbers = phoneNumber, // 待发送手机号, 多个以逗号分隔 + SignName = template.SignName, // 短信签名 + TemplateCode = template.TemplateCode, // 短信模板 + TemplateParam = templateParam.ToJson(), // 模板中的变量替换JSON串 + OutId = YitIdHelper.NextId().ToString() + }; + var sendSmsResponse = await client.SendSmsAsync(sendSmsRequest); + if (sendSmsResponse.Body.Code == "OK" && sendSmsResponse.Body.Message == "OK") + { + // var bizId = sendSmsResponse.Body.BizId; + _sysCacheService.Set($"{CacheConst.KeyPhoneVerCode}{phoneNumber}", verifyCode, TimeSpan.FromSeconds(_smsOptions.VerifyCodeExpireSeconds)); + } + else + { + throw Oops.Oh($"短信发送失败:{sendSmsResponse.Body.Code}-{sendSmsResponse.Body.Message}"); + } + + await Task.CompletedTask; + } + + /// + /// 发送短信模板 + /// + /// 手机号 + /// 短信内容 + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("发送短信模板")] + public async Task AliyunSendSmsTemplate([Required] string phoneNumber, [Required] dynamic templateParam, string templateId) + { + if (!phoneNumber.TryValidate(ValidationTypes.PhoneNumber).IsValid) throw Oops.Oh("请正确填写手机号码"); + + if (string.IsNullOrWhiteSpace(templateParam.ToString())) throw Oops.Oh("短信内容不能为空"); + + var client = CreateAliyunClient(); + var template = _smsOptions.Aliyun.GetTemplate(templateId); + var sendSmsRequest = new SendSmsRequest + { + PhoneNumbers = phoneNumber, // 待发送手机号, 多个以逗号分隔 + SignName = template.SignName, // 短信签名 + TemplateCode = template.TemplateCode, // 短信模板 + TemplateParam = templateParam.ToString(), // 模板中的变量替换JSON串 + OutId = YitIdHelper.NextId().ToString() + }; + var sendSmsResponse = await client.SendSmsAsync(sendSmsRequest); + if (sendSmsResponse.Body.Code == "OK" && sendSmsResponse.Body.Message == "OK") + { + } + else + { + throw Oops.Oh($"短信发送失败:{sendSmsResponse.Body.Code}-{sendSmsResponse.Body.Message}"); + } + + await Task.CompletedTask; + } + + /// + /// 腾讯云发送短信 📨 + /// + /// + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("腾讯云发送短信")] + public async Task TencentSendSms([Required] string phoneNumber, string templateId = "0") + { + if (!phoneNumber.TryValidate(ValidationTypes.PhoneNumber).IsValid) throw Oops.Oh("请正确填写手机号码"); + + // 生成随机验证码 + var random = new Random(); + var verifyCode = random.Next(100000, 999999); + + // 实例化要请求产品的client对象,clientProfile是可选的 + var client = new SmsClient(CreateTencentClient(), "ap-guangzhou", new ClientProfile() { HttpProfile = new HttpProfile() { Endpoint = ("sms.tencentcloudapi.com") } }); + var template = _smsOptions.Tencentyun.GetTemplate(templateId); + // 实例化一个请求对象,每个接口都会对应一个request对象 + var req = new TencentCloud.Sms.V20190711.Models.SendSmsRequest + { + PhoneNumberSet = new string[] { "+86" + phoneNumber.Trim(',') }, + SmsSdkAppid = _smsOptions.Tencentyun.SdkAppId, + Sign = template.SignName, + TemplateID = template.TemplateCode, + TemplateParamSet = new string[] { verifyCode.ToString() } + }; + + // 返回的resp是一个SendSmsResponse的实例,与请求对象对应 + TencentCloud.Sms.V20190711.Models.SendSmsResponse resp = client.SendSmsSync(req); + + if (resp.SendStatusSet[0].Code == "Ok" && resp.SendStatusSet[0].Message == "send success") + { + // var bizId = sendSmsResponse.Body.BizId; + _sysCacheService.Set($"{CacheConst.KeyPhoneVerCode}{phoneNumber}", verifyCode, TimeSpan.FromSeconds(_smsOptions.VerifyCodeExpireSeconds)); + } + else + { + throw Oops.Oh($"短信发送失败:{resp.SendStatusSet[0].Code}-{resp.SendStatusSet[0].Message}"); + } + + await Task.CompletedTask; + } + + /// + /// 阿里云短信配置 + /// + /// + private AlibabaCloud.SDK.Dysmsapi20170525.Client CreateAliyunClient() + { + var config = new AlibabaCloud.OpenApiClient.Models.Config + { + AccessKeyId = _smsOptions.Aliyun.AccessKeyId, + AccessKeySecret = _smsOptions.Aliyun.AccessKeySecret, + Endpoint = "dysmsapi.aliyuncs.com" + }; + return new AlibabaCloud.SDK.Dysmsapi20170525.Client(config); + } + + /// + /// 腾讯云短信配置 + /// + /// + private Credential CreateTencentClient() + { + var cred = new Credential + { + SecretId = _smsOptions.Tencentyun.AccessKeyId, + SecretKey = _smsOptions.Tencentyun.AccessKeySecret + }; + return cred; + } + + /// + /// 自定义短信接口发送短信 📨 + /// + /// 手机号 + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("自定义短信接口发送短信")] + public async Task CustomSendSms([DataValidation(ValidationTypes.PhoneNumber)] string phoneNumber, string templateId = "0") + { + if (_smsOptions.Custom == null || !_smsOptions.Custom.Enabled) + throw Oops.Oh("自定义短信接口未启用"); + + if (string.IsNullOrWhiteSpace(_smsOptions.Custom.ApiUrl)) + throw Oops.Oh("自定义短信接口地址未配置"); + + // 生成随机验证码 + var verifyCode = Random.Shared.Next(100000, 999999); + + // 获取模板 + var template = _smsOptions.Custom.GetTemplate(templateId); + if (template == null) + throw Oops.Oh($"短信模板[{templateId}]不存在"); + + // 替换模板内容中的占位符 + var content = template.Content.Replace("{code}", verifyCode.ToString()); + + try + { + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(30); + + HttpResponseMessage response; + + //替换URL占位符 + var url = _smsOptions.Custom.ApiUrl + .Replace("{templateId}", templateId) + .Replace("{mobile}", phoneNumber) + .Replace("{content}", Uri.EscapeDataString(content)) + .Replace("{code}", verifyCode.ToString()); + + if (_smsOptions.Custom.Method.ToUpper() == "POST") + { + // 替换占位符 + var postData = _smsOptions.Custom.PostData? + .Replace("{templateId}", templateId) + .Replace("{mobile}", phoneNumber) + .Replace("{content}", content) + .Replace("{code}", verifyCode.ToString()); + HttpContent httpContent = new StringContent(postData ?? string.Empty, Encoding.UTF8, _smsOptions.Custom.ContentType ?? "application/x-www-form-urlencoded"); + response = await httpClient.PostAsync(url, httpContent); + } + else + { + // GET 请求 + response = await httpClient.GetAsync(url); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + + // 判断是否发送成功 + if (response.IsSuccessStatusCode && responseContent.Contains(_smsOptions.Custom.SuccessFlag)) + { + if (_smsOptions.Custom.ApiUrl.Contains("{code}") || template.Content.Contains("{code}") || (_smsOptions.Custom.PostData?.Contains("{code}") == true)) + { + // 如果模板含有验证码,则添加到缓存 + _sysCacheService.Set($"{CacheConst.KeyPhoneVerCode}{phoneNumber}", verifyCode, TimeSpan.FromSeconds(_smsOptions.VerifyCodeExpireSeconds)); + } + } + else + { + throw Oops.Oh($"短信发送失败:{responseContent}"); + } + } + catch (Exception ex) + { + throw Oops.Oh($"短信发送异常:{ex.Message}"); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Notice/Dto/NoticeInput.cs b/Admin.NET.Core/Service/Notice/Dto/NoticeInput.cs new file mode 100644 index 0000000..e433927 --- /dev/null +++ b/Admin.NET.Core/Service/Notice/Dto/NoticeInput.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageNoticeInput : BasePageInput +{ + /// + /// 标题 + /// + public virtual string Title { get; set; } + + /// + /// 类型(1通知 2公告) + /// + public virtual NoticeTypeEnum? Type { get; set; } +} + +public class AddNoticeInput : SysNotice +{ +} + +public class UpdateNoticeInput : AddNoticeInput +{ +} + +public class DeleteNoticeInput : BaseIdInput +{ +} + +public class NoticeInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Notice/SysNoticeService.cs b/Admin.NET.Core/Service/Notice/SysNoticeService.cs new file mode 100644 index 0000000..627b1e7 --- /dev/null +++ b/Admin.NET.Core/Service/Notice/SysNoticeService.cs @@ -0,0 +1,190 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统通知公告服务 🧩 +/// +[ApiDescriptionSettings(Order = 380, Description = "通知公告")] +public class SysNoticeService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysUserRep; + private readonly SqlSugarRepository _sysNoticeRep; + private readonly SqlSugarRepository _sysNoticeUserRep; + private readonly SysOnlineUserService _sysOnlineUserService; + + public SysNoticeService( + UserManager userManager, + SqlSugarRepository sysUserRep, + SqlSugarRepository sysNoticeRep, + SqlSugarRepository sysNoticeUserRep, + SysOnlineUserService sysOnlineUserService) + { + _userManager = userManager; + _sysUserRep = sysUserRep; + _sysNoticeRep = sysNoticeRep; + _sysNoticeUserRep = sysNoticeUserRep; + _sysOnlineUserService = sysOnlineUserService; + } + + /// + /// 获取通知公告分页列表 📢 + /// + /// + /// + [DisplayName("获取通知公告分页列表")] + public async Task> Page(PageNoticeInput input) + { + return await _sysNoticeRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Title), u => u.Title.Contains(input.Title.Trim())) + .WhereIF(input.Type > 0, u => u.Type == input.Type) + .WhereIF(!_userManager.SuperAdmin, u => u.CreateUserId == _userManager.UserId) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加通知公告 📢 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加通知公告")] + public async Task AddNotice(AddNoticeInput input) + { + var notice = input.Adapt(); + InitNoticeInfo(notice); + await _sysNoticeRep.InsertAsync(notice); + } + + /// + /// 更新通知公告 📢 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新通知公告")] + public async Task UpdateNotice(UpdateNoticeInput input) + { + if (input.CreateUserId != _userManager.UserId) + throw Oops.Oh(ErrorCodeEnum.D7003); + + var notice = input.Adapt(); + InitNoticeInfo(notice); + await _sysNoticeRep.UpdateAsync(notice); + } + + /// + /// 删除通知公告 📢 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除通知公告")] + public async Task DeleteNotice(DeleteNoticeInput input) + { + var sysNotice = await _sysNoticeRep.GetByIdAsync(input.Id); + + if (sysNotice.CreateUserId != _userManager.UserId) throw Oops.Oh(ErrorCodeEnum.D7003); + + if (sysNotice.Status == NoticeStatusEnum.PUBLIC) throw Oops.Oh(ErrorCodeEnum.D7001); + + await _sysNoticeRep.DeleteAsync(u => u.Id == input.Id); + + await _sysNoticeUserRep.DeleteAsync(u => u.NoticeId == input.Id); + } + + /// + /// 发布通知公告 📢 + /// + /// + /// + [DisplayName("发布通知公告")] + public async Task Public(NoticeInput input) + { + if (!(await _sysNoticeRep.IsAnyAsync(u => u.Id == input.Id && u.CreateUserId == _userManager.UserId))) + throw Oops.Oh(ErrorCodeEnum.D7003); + + // 更新发布状态和时间 + await _sysNoticeRep.UpdateAsync(u => new SysNotice() { Status = NoticeStatusEnum.PUBLIC, PublicTime = DateTime.Now }, u => u.Id == input.Id); + + var notice = await _sysNoticeRep.GetByIdAsync(input.Id); + + // 通知到的人(所有账号) + var userIdList = await _sysUserRep.AsQueryable().Select(u => u.Id).ToListAsync(); + + await _sysNoticeUserRep.DeleteAsync(u => u.NoticeId == notice.Id); + var noticeUserList = userIdList.Select(u => new SysNoticeUser + { + NoticeId = notice.Id, + UserId = u, + }).ToList(); + await _sysNoticeUserRep.InsertRangeAsync(noticeUserList); + + // 广播所有在线账号 + await _sysOnlineUserService.PublicNotice(notice, userIdList); + } + + /// + /// 设置通知公告已读状态 📢 + /// + /// + /// + [DisplayName("设置通知公告已读状态")] + public async Task SetRead(NoticeInput input) + { + await _sysNoticeUserRep.UpdateAsync(u => new SysNoticeUser + { + ReadStatus = NoticeUserStatusEnum.READ, + ReadTime = DateTime.Now + }, u => u.NoticeId == input.Id && u.UserId == _userManager.UserId); + } + + /// + /// 获取接收的通知公告 + /// + /// + /// + [DisplayName("获取接收的通知公告")] + public async Task> PageReceived(PageNoticeInput input) + { + return await _sysNoticeUserRep.AsQueryable().Includes(u => u.SysNotice) + .Where(u => u.UserId == _userManager.UserId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Title), u => u.SysNotice.Title.Contains(input.Title.Trim())) + .WhereIF(input.Type is > 0, u => u.SysNotice.Type == input.Type) + .OrderBy(u => u.SysNotice.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取未读的通知公告 📢 + /// + /// + [DisplayName("获取未读的通知公告")] + public async Task> GetUnReadList() + { + var noticeUserList = await _sysNoticeUserRep.AsQueryable().Includes(u => u.SysNotice) + .Where(u => u.UserId == _userManager.UserId && u.ReadStatus == NoticeUserStatusEnum.UNREAD) + .OrderBy(u => u.SysNotice.CreateTime, OrderByType.Desc).ToListAsync(); + return noticeUserList.Select(t => t.SysNotice).ToList(); + } + + /// + /// 初始化通知公告信息 + /// + /// + [NonAction] + private void InitNoticeInfo(SysNotice notice) + { + notice.PublicUserId = _userManager.UserId; + notice.PublicUserName = _userManager.RealName; + notice.PublicOrgId = _userManager.OrgId; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/OAuth/OAuthClaim.cs b/Admin.NET.Core/Service/OAuth/OAuthClaim.cs new file mode 100644 index 0000000..db89229 --- /dev/null +++ b/Admin.NET.Core/Service/OAuth/OAuthClaim.cs @@ -0,0 +1,12 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public static class OAuthClaim +{ + public const string GiteeAvatarUrl = "urn:gitee:avatar_url"; +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/OAuth/OAuthSetup.cs b/Admin.NET.Core/Service/OAuth/OAuthSetup.cs new file mode 100644 index 0000000..b58aea6 --- /dev/null +++ b/Admin.NET.Core/Service/OAuth/OAuthSetup.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; + +namespace Admin.NET.Core; + +public static class OAuthSetup +{ + /// + /// 三方授权登录OAuth注册 + /// + /// + public static void AddOAuth(this IServiceCollection services) + { + var authOpt = App.GetConfig("OAuth", true); + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddCookie(options => + { + options.Cookie.SameSite = SameSiteMode.Lax; + }) + .AddWeixin(options => + { + options.ClientId = authOpt.Weixin?.ClientId!; + options.ClientSecret = authOpt.Weixin?.ClientSecret!; + }) + .AddGitee(options => + { + options.ClientId = authOpt.Gitee?.ClientId!; + options.ClientSecret = authOpt.Gitee?.ClientSecret!; + + options.ClaimActions.MapJsonKey(OAuthClaim.GiteeAvatarUrl, "avatar_url"); + }); + } + + public static void UseOAuth(this IApplicationBuilder app) + { + app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax }); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/OAuth/SysOAuthService.cs b/Admin.NET.Core/Service/OAuth/SysOAuthService.cs new file mode 100644 index 0000000..99d9621 --- /dev/null +++ b/Admin.NET.Core/Service/OAuth/SysOAuthService.cs @@ -0,0 +1,120 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; +using System.Security.Claims; + +namespace Admin.NET.Core.Service; + +/// +/// 系统OAuth服务 🧩 +/// +[AllowAnonymous] +[ApiDescriptionSettings(Order = 498)] +public class SysOAuthService : IDynamicApiController, ITransient +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly SqlSugarRepository _sysWechatUserRep; + + public SysOAuthService(IHttpContextAccessor httpContextAccessor, + SqlSugarRepository sysWechatUserRep) + { + _httpContextAccessor = httpContextAccessor; + _sysWechatUserRep = sysWechatUserRep; + } + + /// + /// 第三方登录 🔖 + /// + /// + /// + /// + [ApiDescriptionSettings(Name = "SignIn"), HttpGet] + [DisplayName("第三方登录")] + public virtual async Task SignIn([FromQuery] string provider, [FromQuery] string redirectUrl) + { + if (string.IsNullOrWhiteSpace(provider) || !await _httpContextAccessor.HttpContext.IsProviderSupportedAsync(provider)) + throw Oops.Oh("不支持的OAuth类型"); + + var request = _httpContextAccessor.HttpContext!.Request; + var url = $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}Callback?provider={provider}&redirectUrl={redirectUrl}"; + var properties = new AuthenticationProperties + { + RedirectUri = url, + Items = { ["LoginProvider"] = provider } + }; + return await Task.FromResult(new ChallengeResult(provider, properties)); + } + + /// + /// 授权回调 🔖 + /// + /// + /// + /// + [ApiDescriptionSettings(Name = "SignInCallback"), HttpGet] + [DisplayName("授权回调")] + public virtual async Task SignInCallback([FromQuery] string provider = null, [FromQuery] string redirectUrl = "") + { + if (string.IsNullOrWhiteSpace(provider) || !await _httpContextAccessor.HttpContext.IsProviderSupportedAsync(provider)) + throw Oops.Oh("不支持的OAuth类型"); + + var authenticateResult = await _httpContextAccessor.HttpContext!.AuthenticateAsync(provider); + if (!authenticateResult.Succeeded) + throw Oops.Oh("授权失败"); + + var openIdClaim = authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier); + if (openIdClaim == null || string.IsNullOrWhiteSpace(openIdClaim.Value)) + throw Oops.Oh("授权失败"); + + var name = authenticateResult.Principal.FindFirst(ClaimTypes.Name)?.Value; + var email = authenticateResult.Principal.FindFirst(ClaimTypes.Email)?.Value; + var mobilePhone = authenticateResult.Principal.FindFirst(ClaimTypes.MobilePhone)?.Value; + var dateOfBirth = authenticateResult.Principal.FindFirst(ClaimTypes.DateOfBirth)?.Value; + var gender = authenticateResult.Principal.FindFirst(ClaimTypes.Gender)?.Value; + var avatarUrl = ""; + + var platformType = PlatformTypeEnum.微信公众号; + if (provider == "Gitee") + { + platformType = PlatformTypeEnum.Gitee; + avatarUrl = authenticateResult.Principal.FindFirst(OAuthClaim.GiteeAvatarUrl)?.Value; + } + + // 若账号不存在则新建 + var wechatUser = await _sysWechatUserRep.AsQueryable().Includes(u => u.SysUser).ClearFilter().FirstAsync(u => u.OpenId == openIdClaim.Value); + if (wechatUser == null) + { + var userId = await App.GetRequiredService().AddUser(new AddUserInput() + { + Account = name, + RealName = name, + NickName = name, + Email = email, + Avatar = avatarUrl, + Phone = mobilePhone, + OrgId = 1300000000101, // 根组织架构 + RoleIdList = new List { 1300000000104 } // 仅本人数据角色 + }); + + await _sysWechatUserRep.InsertAsync(new SysWechatUser() + { + UserId = userId, + OpenId = openIdClaim.Value, + Avatar = avatarUrl, + NickName = name, + PlatformType = platformType + }); + + wechatUser = await _sysWechatUserRep.AsQueryable().Includes(u => u.SysUser).ClearFilter().FirstAsync(u => u.OpenId == openIdClaim.Value); + } + + // 构建Token令牌 + var token = await App.GetRequiredService().CreateToken(wechatUser.SysUser); + + return new RedirectResult($"{redirectUrl}/#/login?token={token.AccessToken}"); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/OnlineUser/Dto/OnlineUserInput.cs b/Admin.NET.Core/Service/OnlineUser/Dto/OnlineUserInput.cs new file mode 100644 index 0000000..46c40d9 --- /dev/null +++ b/Admin.NET.Core/Service/OnlineUser/Dto/OnlineUserInput.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageOnlineUserInput : BasePageInput +{ + /// + /// 账号名称 + /// + public string UserName { get; set; } + + /// + /// 真实姓名 + /// + public string RealName { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs b/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs new file mode 100644 index 0000000..f54c99b --- /dev/null +++ b/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs @@ -0,0 +1,109 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.SignalR; + +namespace Admin.NET.Core.Service; + +/// +/// 系统在线用户服务 🧩 +/// +[ApiDescriptionSettings(Order = 300)] +public class SysOnlineUserService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SysConfigService _sysConfigService; + private readonly IHubContext _onlineUserHubContext; + private readonly SqlSugarRepository _sysOnlineUerRep; + + public SysOnlineUserService(SysConfigService sysConfigService, + IHubContext onlineUserHubContext, + SqlSugarRepository sysOnlineUerRep, + UserManager userManager) + { + _userManager = userManager; + _sysConfigService = sysConfigService; + _onlineUserHubContext = onlineUserHubContext; + _sysOnlineUerRep = sysOnlineUerRep; + } + + /// + /// 获取在线用户分页列表 🔖 + /// + /// + [DisplayName("获取在线用户分页列表")] + public async Task> Page(PageOnlineUserInput input) + { + return await _sysOnlineUerRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.UserName), u => u.UserName.Contains(input.UserName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.RealName), u => u.RealName.Contains(input.RealName)) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 强制下线 🔖 + /// + /// + /// + [NonValidation] + [DisplayName("强制下线")] + public async Task ForceOffline(SysOnlineUser user) + { + await _onlineUserHubContext.Clients.Client(user.ConnectionId ?? "").ForceOffline("强制下线"); + await _sysOnlineUerRep.DeleteAsync(user); + } + + /// + /// 发布站内消息 + /// + /// + /// + /// + [NonAction] + public async Task PublicNotice(SysNotice notice, List userIds) + { + var userList = await _sysOnlineUerRep.GetListAsync(u => userIds.Contains(u.UserId)); + if (userList.Count == 0) return; + + foreach (var item in userList) + { + await _onlineUserHubContext.Clients.Client(item.ConnectionId ?? "").PublicNotice(notice); + } + } + + /// + /// 单用户登录 + /// + /// + [NonAction] + public async Task SingleLogin(long userId) + { + if (await _sysConfigService.GetConfigValue(ConfigConst.SysSingleLogin)) + { + var users = await _sysOnlineUerRep.GetListAsync(u => u.UserId == userId); + foreach (var user in users) + { + await ForceOffline(user); + } + } + } + + /// + /// 通过用户ID踢掉在线用户 + /// + /// + /// + [NonAction] + public async Task ForceOffline(long userId) + { + var users = await _sysOnlineUerRep.GetListAsync(u => u.UserId == userId); + foreach (var user in users) + { + await ForceOffline(user); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessInput.cs b/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessInput.cs new file mode 100644 index 0000000..a3d5793 --- /dev/null +++ b/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessInput.cs @@ -0,0 +1,85 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 开放接口身份输入参数 +/// +public class OpenAccessInput : BasePageInput +{ + /// + /// 身份标识 + /// + public string AccessKey { get; set; } +} + +public class AddOpenAccessInput : SysOpenAccess +{ + /// + /// 身份标识 + /// + [Required(ErrorMessage = "身份标识不能为空")] + public override string AccessKey { get; set; } + + /// + /// 密钥 + /// + [Required(ErrorMessage = "密钥不能为空")] + public override string AccessSecret { get; set; } + + /// + /// 绑定用户Id + /// + [Required(ErrorMessage = "绑定用户不能为空")] + public override long BindUserId { get; set; } +} + +public class UpdateOpenAccessInput : AddOpenAccessInput +{ +} + +public class DeleteOpenAccessInput : BaseIdInput +{ +} + +public class GenerateSignatureInput +{ + /// + /// 身份标识 + /// + [Required(ErrorMessage = "身份标识不能为空")] + public string AccessKey { get; set; } + + /// + /// 密钥 + /// + [Required(ErrorMessage = "密钥不能为空")] + public string AccessSecret { get; set; } + + /// + /// 请求方法 + /// + public HttpMethodEnum Method { get; set; } + + /// + /// 请求接口地址 + /// + [Required(ErrorMessage = "请求接口地址不能为空")] + public string Url { get; set; } + + /// + /// 时间戳 + /// + [Required(ErrorMessage = "时间戳不能为空")] + public long Timestamp { get; set; } + + /// + /// 随机数 + /// + [Required(ErrorMessage = "随机数不能为空")] + public string Nonce { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessOutput.cs b/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessOutput.cs new file mode 100644 index 0000000..47516fd --- /dev/null +++ b/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessOutput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class OpenAccessOutput : SysOpenAccess +{ + /// + /// 绑定用户账号 + /// + public string BindUserAccount { get; set; } + + /// + /// 绑定租户名称 + /// + public string BindTenantName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs b/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs new file mode 100644 index 0000000..b8fdf2c --- /dev/null +++ b/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs @@ -0,0 +1,195 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Security.Claims; +using System.Security.Cryptography; + +namespace Admin.NET.Core.Service; + +/// +/// 开放接口身份服务 🧩 +/// +[ApiDescriptionSettings(Order = 244)] +public class SysOpenAccessService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysOpenAccessRep; + private readonly SysCacheService _sysCacheService; + + /// + /// 开放接口身份服务构造函数 + /// + public SysOpenAccessService(SqlSugarRepository sysOpenAccessRep, + SysCacheService sysCacheService) + { + _sysOpenAccessRep = sysOpenAccessRep; + _sysCacheService = sysCacheService; + } + + /// + /// 生成签名 + /// + /// + /// + [DisplayName("生成签名")] + public string GenerateSignature(GenerateSignatureInput input) + { + // 密钥 + var appSecretByte = Encoding.UTF8.GetBytes(input.AccessSecret); + + // 拼接参数 + var parameter = $"{input.Method.ToString().ToUpper()}&{input.Url}&{input.AccessKey}&{input.Timestamp}&{input.Nonce}"; + // 使用 HMAC-SHA256 协议创建基于哈希的消息身份验证代码 (HMAC),以appSecretByte 作为密钥,对上面拼接的参数进行计算签名,所得签名进行 Base-64 编码 + using HMAC hmac = new HMACSHA256(); + hmac.Key = appSecretByte; + var sign = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(parameter))); + return sign; + } + + /// + /// 获取开放接口身份分页列表 🔖 + /// + /// + /// + [DisplayName("获取开放接口身份分页列表")] + public async Task> Page(OpenAccessInput input) + { + return await _sysOpenAccessRep.AsQueryable() + .LeftJoin((u, a) => u.BindUserId == a.Id) + .LeftJoin((u, a, b) => u.BindTenantId == b.Id) + .LeftJoin((u, a, b, c) => b.OrgId == c.Id) + .WhereIF(!string.IsNullOrWhiteSpace(input.AccessKey?.Trim()), (u, a, b, c) => u.AccessKey.Contains(input.AccessKey)) + .Select((u, a, b, c) => new OpenAccessOutput + { + BindUserAccount = a.Account, + BindTenantName = c.Name, + }, true) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加开放接口身份 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加开放接口身份")] + public async Task AddOpenAccess(AddOpenAccessInput input) + { + if (await _sysOpenAccessRep.AsQueryable().AnyAsync(u => u.AccessKey == input.AccessKey && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.O1000); + + var openAccess = input.Adapt(); + await _sysOpenAccessRep.InsertAsync(openAccess); + } + + /// + /// 更新开放接口身份 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新开放接口身份")] + public async Task UpdateOpenAccess(UpdateOpenAccessInput input) + { + if (await _sysOpenAccessRep.AsQueryable().AnyAsync(u => u.AccessKey == input.AccessKey && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.O1000); + + var openAccess = input.Adapt(); + _sysCacheService.Remove(CacheConst.KeyOpenAccess + openAccess.AccessKey); + + await _sysOpenAccessRep.UpdateAsync(openAccess); + } + + /// + /// 删除开放接口身份 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除开放接口身份")] + public async Task DeleteOpenAccess(DeleteOpenAccessInput input) + { + var openAccess = await _sysOpenAccessRep.GetFirstAsync(u => u.Id == input.Id); + if (openAccess != null) + _sysCacheService.Remove(CacheConst.KeyOpenAccess + openAccess.AccessKey); + + await _sysOpenAccessRep.DeleteAsync(u => u.Id == input.Id); + } + + /// + /// 创建密钥 🔖 + /// + /// + [DisplayName("创建密钥")] + public async Task CreateSecret() + { + return await Task.FromResult(Convert.ToBase64String(Guid.NewGuid().ToByteArray())[..^2]); + } + + /// + /// 根据 Key 获取对象 + /// + /// + /// + [NonAction] + public async Task GetByKey(string accessKey) + { + return await Task.FromResult( + _sysCacheService.GetOrAdd(CacheConst.KeyOpenAccess + accessKey, _ => + { + return _sysOpenAccessRep.AsQueryable() + .Includes(u => u.BindUser) + .Includes(u => u.BindUser, p => p.SysOrg) + .First(u => u.AccessKey == accessKey); + }) + ); + } + + /// + /// Signature 身份验证事件默认实现 + /// + [NonAction] + public static SignatureAuthenticationEvent GetSignatureAuthenticationEventImpl() + { + return new SignatureAuthenticationEvent + { + OnGetAccessSecret = context => + { + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + try + { + var openAccessService = context.HttpContext.RequestServices.GetRequiredService(); + var openAccess = openAccessService.GetByKey(context.AccessKey).GetAwaiter().GetResult(); + return Task.FromResult(openAccess == null ? "" : openAccess.AccessSecret); + } + catch (Exception ex) + { + logger.LogError(ex, "开放接口身份验证"); + return Task.FromResult(""); + } + }, + OnValidated = context => + { + var openAccessService = context.HttpContext.RequestServices.GetRequiredService(); + var openAccess = openAccessService.GetByKey(context.AccessKey).GetAwaiter().GetResult(); + var identity = ((ClaimsIdentity)context.Principal!.Identity!); + + identity.AddClaims(new[] + { + new Claim(ClaimConst.UserId, openAccess.BindUserId + ""), + new Claim(ClaimConst.TenantId, openAccess.BindTenantId + ""), + new Claim(ClaimConst.Account, openAccess.BindUser.Account + ""), + new Claim(ClaimConst.RealName, openAccess.BindUser.RealName), + new Claim(ClaimConst.AccountType, ((int)openAccess.BindUser.AccountType).ToString()), + new Claim(ClaimConst.OrgId, openAccess.BindUser.OrgId + ""), + new Claim(ClaimConst.OrgName, openAccess.BindUser.SysOrg?.Name + ""), + new Claim(ClaimConst.OrgType, openAccess.BindUser.SysOrg?.Type + ""), + }); + return Task.CompletedTask; + } + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Org/Dto/OrgInput.cs b/Admin.NET.Core/Service/Org/Dto/OrgInput.cs new file mode 100644 index 0000000..8c88745 --- /dev/null +++ b/Admin.NET.Core/Service/Org/Dto/OrgInput.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class OrgInput : BaseIdInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 机构类型 + /// + public string Type { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddOrgInput : SysOrg +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "机构名称不能为空")] + public override string Name { get; set; } + + /// + /// 机构类型 + /// + [Dict("org_type", ErrorMessage = "机构类型不能合法", AllowNullValue = true, AllowEmptyStrings = true)] + public override string? Type { get; set; } +} + +public class UpdateOrgInput : AddOrgInput +{ +} + +public class DeleteOrgInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Org/Dto/OrgTreeOutput.cs b/Admin.NET.Core/Service/Org/Dto/OrgTreeOutput.cs new file mode 100644 index 0000000..85ed203 --- /dev/null +++ b/Admin.NET.Core/Service/Org/Dto/OrgTreeOutput.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 机构树形输出 +/// +public class OrgTreeOutput +{ + /// + /// 主键Id + /// + [SugarColumn(IsTreeKey = true)] + public long Id { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } + + /// + /// 父Id + /// + public long Pid { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 机构子项 + /// + public List Children { get; set; } + + /// + /// 是否禁止选中 + /// + public bool Disabled { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Org/SysOrgService.cs b/Admin.NET.Core/Service/Org/SysOrgService.cs new file mode 100644 index 0000000..0c64e60 --- /dev/null +++ b/Admin.NET.Core/Service/Org/SysOrgService.cs @@ -0,0 +1,490 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统机构服务 🧩 +/// +[ApiDescriptionSettings(Order = 470)] +public class SysOrgService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SysCacheService _sysCacheService; + private readonly SysUserExtOrgService _sysUserExtOrgService; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysRoleOrgService _sysRoleOrgService; + private readonly SqlSugarRepository _sysOrgRep; + + public SysOrgService(UserManager userManager, + SysCacheService sysCacheService, + SysUserExtOrgService sysUserExtOrgService, + SysUserRoleService sysUserRoleService, + SysRoleOrgService sysRoleOrgService, + SqlSugarRepository sysOrgRep) + { + _userManager = userManager; + _sysCacheService = sysCacheService; + _sysUserExtOrgService = sysUserExtOrgService; + _sysUserRoleService = sysUserRoleService; + _sysRoleOrgService = sysRoleOrgService; + _sysOrgRep = sysOrgRep; + } + + /// + /// 获取机构列表 🔖 + /// + /// + [DisplayName("获取机构列表")] + public async Task> GetList([FromQuery] OrgInput input) + { + // 获取拥有的机构Id集合 + var userOrgIdList = await GetUserOrgIdList(); + + var queryable = _sysOrgRep.AsQueryable().WhereIF(input.TenantId > 0, u => u.TenantId == input.TenantId).OrderBy(u => new { u.OrderNo, u.Id }); + // 带条件筛选时返回列表数据 + if (!string.IsNullOrWhiteSpace(input.Name) || !string.IsNullOrWhiteSpace(input.Code) || !string.IsNullOrWhiteSpace(input.Type)) + { + return await queryable.WhereIF(userOrgIdList.Count > 0, u => userOrgIdList.Contains(u.Id)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code == input.Code) + .WhereIF(!string.IsNullOrWhiteSpace(input.Type), u => u.Type == input.Type) + .ToListAsync(); + } + + List orgTree; + if (_userManager.SuperAdmin) + { + orgTree = await queryable.ToTreeAsync(u => u.Children, u => u.Pid, input.Id); + } + else + { + orgTree = await queryable.ToTreeAsync(u => u.Children, u => u.Pid, input.Id, userOrgIdList.Select(d => (object)d).ToArray()); + // 递归禁用没权限的机构(防止用户修改或创建无权的机构和用户) + HandlerOrgTree(orgTree, userOrgIdList); + } + + var sysOrg = await _sysOrgRep.GetSingleAsync(u => u.Id == input.Id); + if (sysOrg == null) return orgTree; + + sysOrg.Children = orgTree; + orgTree = new List { sysOrg }; + return orgTree; + } + + /// + /// 递归禁用没权限的机构 + /// + /// + /// + private static void HandlerOrgTree(List orgTree, List userOrgIdList) + { + foreach (var org in orgTree) + { + org.Disabled = !userOrgIdList.Contains(org.Id); // 设置禁用/不可选择 + if (org.Children != null) + HandlerOrgTree(org.Children, userOrgIdList); + } + } + + /// + /// 获取机构树 🔖 + /// + /// + /// + [DisplayName("获取机构树")] + public async Task> GetTree([FromQuery] OrgInput input) + { + // 获取拥有的机构Id集合 + var userOrgIdList = await GetUserOrgIdList(); + + var queryable = _sysOrgRep.AsQueryable().WhereIF(input.TenantId > 0, u => u.TenantId == input.TenantId).OrderBy(u => new { u.OrderNo, u.Id }); + List orgTree; + if (_userManager.SuperAdmin) + { + orgTree = await queryable.Select().ToTreeAsync(u => u.Children, u => u.Pid, input.Id); + } + else + { + orgTree = await queryable.Select().ToTreeAsync(u => u.Children, u => u.Pid, input.Id, userOrgIdList.Select(d => (object)d).ToArray()); + // 递归禁用没权限的机构(防止用户修改或创建无权的机构和用户) + HandlerOrgTree(orgTree, userOrgIdList); + } + + var sysOrg = await _sysOrgRep.AsQueryable().Select().FirstAsync(u => u.Id == input.Id); + if (sysOrg == null) return orgTree; + + sysOrg.Children = orgTree; + orgTree = new List { sysOrg }; + return orgTree; + } + + /// + /// 递归禁用没权限的机构 + /// + /// + /// + private static void HandlerOrgTree(List orgTree, List userOrgIdList) + { + foreach (var org in orgTree) + { + org.Disabled = !userOrgIdList.Contains(org.Id); // 设置禁用/不可选择 + if (org.Children != null) + HandlerOrgTree(org.Children, userOrgIdList); + } + } + + /// + /// 增加机构 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加机构")] + public async Task AddOrg(AddOrgInput input) + { + if (!_userManager.SuperAdmin && input.Pid == 0) + throw Oops.Oh(ErrorCodeEnum.D2009); + + if (await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code)) + throw Oops.Oh(ErrorCodeEnum.D2002); + + if (!_userManager.SuperAdmin && input.Pid != 0) + { + // 新增机构父Id不是0,则进行权限校验 + var orgIdList = await GetUserOrgIdList(); + // 新增机构的父机构不在自己的数据范围内 + if (orgIdList.Count < 1 || !orgIdList.Contains(input.Pid)) + throw Oops.Oh(ErrorCodeEnum.D2003); + } + + // 删除与此父机构有关的用户机构缓存 + if (input.Pid == 0) + { + DeleteAllUserOrgCache(0, 0); + } + else + { + var pOrg = await _sysOrgRep.GetFirstAsync(u => u.Id == input.Pid); + if (pOrg != null) + DeleteAllUserOrgCache(pOrg.Id, pOrg.Pid); + } + + var newOrg = await _sysOrgRep.AsInsertable(input.Adapt()).ExecuteReturnEntityAsync(); + return newOrg.Id; + } + + /// + /// 批量增加机构 + /// + /// + /// + [NonAction] + public async Task BatchAddOrgs(List orgs) + { + DeleteAllUserOrgCache(0, 0); + await _sysOrgRep.AsDeleteable().ExecuteCommandAsync(); + await _sysOrgRep.AsInsertable(orgs).ExecuteCommandAsync(); + } + + /// + /// 更新机构 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新机构")] + public async Task UpdateOrg(UpdateOrgInput input) + { + if (!_userManager.SuperAdmin && input.Pid == 0) + throw Oops.Oh(ErrorCodeEnum.D2012); + + if (input.Pid != 0) + { + //var pOrg = await _sysOrgRep.GetFirstAsync(u => u.Id == input.Pid); + //_ = pOrg ?? throw Oops.Oh(ErrorCodeEnum.D2000); + + // 若父机构发生变化则清空用户机构缓存 + var sysOrg = await _sysOrgRep.GetFirstAsync(u => u.Id == input.Id); + if (sysOrg != null && sysOrg.Pid != input.Pid) + { + // 删除与此机构、新父机构有关的用户机构缓存 + DeleteAllUserOrgCache(sysOrg.Id, input.Pid); + } + } + if (input.Id == input.Pid) + throw Oops.Oh(ErrorCodeEnum.D2001); + + if (await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D2002); + + // 父Id不能为自己的子节点 + var childIdList = await GetChildIdListWithSelfById(input.Id); + if (childIdList.Contains(input.Pid)) + throw Oops.Oh(ErrorCodeEnum.D2001); + + // 是否有权限操作此机构 + if (!_userManager.SuperAdmin) + { + var orgIdList = await GetUserOrgIdList(); + if (orgIdList.Count < 1 || !orgIdList.Contains(input.Id)) + throw Oops.Oh(ErrorCodeEnum.D2003); + } + + await _sysOrgRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除机构 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除机构")] + public async Task DeleteOrg(DeleteOrgInput input) + { + var sysOrg = await _sysOrgRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + + // 是否有权限操作此机构 + if (!_userManager.SuperAdmin) + { + var orgIdList = await GetUserOrgIdList(); + if (orgIdList.Count < 1 || !orgIdList.Contains(sysOrg.Id)) + throw Oops.Oh(ErrorCodeEnum.D2003); + } + + // 若机构为租户默认机构禁止删除 + var isTenantOrg = await _sysOrgRep.ChangeRepository>() + .IsAnyAsync(u => u.OrgId == input.Id); + if (isTenantOrg) + throw Oops.Oh(ErrorCodeEnum.D2008); + + // 若机构有用户则禁止删除 + var orgHasEmp = await _sysOrgRep.ChangeRepository>() + .IsAnyAsync(u => u.OrgId == input.Id); + if (orgHasEmp) + throw Oops.Oh(ErrorCodeEnum.D2004); + + // 若扩展机构有用户则禁止删除 + var hasExtOrgEmp = await _sysUserExtOrgService.HasUserOrg(sysOrg.Id); + if (hasExtOrgEmp) + throw Oops.Oh(ErrorCodeEnum.D2005); + + // 若子机构有用户则禁止删除 + var childOrgTreeList = await _sysOrgRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + var childOrgIdList = childOrgTreeList.Select(u => u.Id).ToList(); + + // 若子机构有用户则禁止删除 + var cOrgHasEmp = await _sysOrgRep.ChangeRepository>() + .IsAnyAsync(u => childOrgIdList.Contains(u.OrgId)); + if (cOrgHasEmp) throw Oops.Oh(ErrorCodeEnum.D2007); + + // 若有绑定注册方案则禁止删除 + var hasUserRegWay = await _sysOrgRep.Context.Queryable().AnyAsync(u => u.OrgId == input.Id); + if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D2010); + + // 删除与此机构、父机构有关的用户机构缓存 + DeleteAllUserOrgCache(sysOrg.Id, sysOrg.Pid); + + // 级联删除机构子节点 + await _sysOrgRep.DeleteAsync(u => childOrgIdList.Contains(u.Id)); + + // 级联删除角色机构数据 + await _sysRoleOrgService.DeleteRoleOrgByOrgIdList(childOrgIdList); + + // 级联删除用户机构数据 + await _sysUserExtOrgService.DeleteUserExtOrgByOrgIdList(childOrgIdList); + } + + /// + /// 删除与此机构、父机构有关的用户机构缓存 + /// + /// + /// + private void DeleteAllUserOrgCache(long orgId, long orgPid) + { + var userOrgKeyList = _sysCacheService.GetKeysByPrefixKey(CacheConst.KeyUserOrg); + if (userOrgKeyList is not { Count: > 0 }) return; + + foreach (var userOrgKey in userOrgKeyList) + { + var userOrgList = _sysCacheService.Get>(userOrgKey); + var userId = long.Parse(userOrgKey.Substring(CacheConst.KeyUserOrg)); + if (userOrgList != null && (userOrgList.Contains(orgId) || userOrgList.Contains(orgPid))) + SqlSugarFilter.DeleteUserOrgCache(userId, _sysOrgRep.Context.CurrentConnectionConfig.ConfigId.ToString()); + + if (orgPid != 0) continue; + + var dataScope = _sysCacheService.Get($"{CacheConst.KeyRoleMaxDataScope}{userId}"); + if (dataScope == (int)DataScopeEnum.All) + SqlSugarFilter.DeleteUserOrgCache(userId, _sysOrgRep.Context.CurrentConnectionConfig.ConfigId.ToString()); + } + } + + /// + /// 获取当前用户机构Id集合 + /// + /// + [NonAction] + public async Task> GetUserOrgIdList() + { + if (_userManager.SuperAdmin) return new(); + return await GetUserOrgIdList(_userManager.UserId, _userManager.OrgId); + } + + /// + /// 根据指定用户Id获取机构Id集合 + /// + /// + [NonAction] + public async Task> GetUserOrgIdList(long userId, long userOrgId) + { + var orgIdList = _sysCacheService.Get>($"{CacheConst.KeyUserOrg}{userId}"); // 取缓存 + if (orgIdList is { Count: >= 1 }) return orgIdList; + + // 本人创建机构集合 + var orgList0 = await _sysOrgRep.AsQueryable().Where(u => u.CreateUserId == userId).Select(u => u.Id).ToListAsync(); + + // 扩展机构集合 + var orgList1 = await _sysUserExtOrgService.GetUserExtOrgList(userId); + + // 角色机构集合 + var orgList2 = await GetUserRoleOrgIdList(userId, userOrgId); + + // 机构并集 + orgIdList = orgList1.Select(u => u.OrgId).Union(orgList2).Union(orgList0).ToList(); + + // 当前所属机构 + if (!orgIdList.Contains(userOrgId)) orgIdList.Add(userOrgId); + + _sysCacheService.Set($"{CacheConst.KeyUserOrg}{userId}", orgIdList, TimeSpan.FromDays(7)); // 存缓存 + return orgIdList; + } + + /// + /// 获取用户角色机构Id集合 + /// + /// + /// 用户的机构Id + /// + private async Task> GetUserRoleOrgIdList(long userId, long userOrgId) + { + var roleList = await _sysUserRoleService.GetUserRoleList(userId); + + if (roleList.Count < 1) return new(); // 空机构Id集合 + + return await GetUserOrgIdList(roleList, userId, userOrgId); + } + + /// + /// 判定用户是否有某角色权限 + /// + /// + /// 角色代码 + /// + [NonAction] + public async Task GetUserHasRole(long userId, SysRole role) + { + if (_userManager.SuperAdmin) + return true; + var userOrgId = _userManager.OrgId; + var roleList = await _sysUserRoleService.GetUserRoleList(userId); + if (roleList != null && roleList.Exists(r => r.Code == role.Code) == true) + return true; + roleList = new List { role }; + var orgIds = await GetUserOrgIdList(roleList, userId, userOrgId); + return orgIds.Contains(userOrgId); + } + + /// + /// 根据角色Id集合获取机构Id集合 + /// + /// + /// + /// 用户的机构Id + /// + private async Task> GetUserOrgIdList(List roleList, long userId, long userOrgId) + { + // 按最大范围策略设定(若同时拥有ALL和SELF权限,则结果ALL) + int strongerDataScopeType = (int)DataScopeEnum.Self; + + // 自定义数据范围的角色集合 + var customDataScopeRoleIdList = new List(); + + // 数据范围的机构集合 + var dataScopeOrgIdList = new List(); + + if (roleList is { Count: > 0 }) + { + roleList.ForEach(u => + { + if (u.DataScope == DataScopeEnum.Define) + { + customDataScopeRoleIdList.Add(u.Id); + strongerDataScopeType = (int)u.DataScope; // 自定义数据权限时也要更新最大范围 + } + else if ((int)u.DataScope <= strongerDataScopeType) + { + strongerDataScopeType = (int)u.DataScope; + // 根据数据范围获取机构集合 + var orgIds = GetOrgIdListByDataScope(userOrgId, strongerDataScopeType).GetAwaiter().GetResult(); + dataScopeOrgIdList = dataScopeOrgIdList.Union(orgIds).ToList(); + } + }); + } + + // 缓存当前用户最大角色数据范围 + _sysCacheService.Set(CacheConst.KeyRoleMaxDataScope + userId, strongerDataScopeType, TimeSpan.FromDays(7)); + + // 根据角色集合获取机构集合 + var roleOrgIdList = await _sysRoleOrgService.GetRoleOrgIdList(customDataScopeRoleIdList); + + // 并集机构集合 + return roleOrgIdList.Union(dataScopeOrgIdList).ToList(); + } + + /// + /// 根据数据范围获取机构Id集合 + /// + /// 用户的机构Id + /// + /// + private async Task> GetOrgIdListByDataScope(long userOrgId, int dataScope) + { + var orgId = userOrgId;//var orgId = _userManager.OrgId; + var orgIdList = new List(); + switch (dataScope) + { + // 若数据范围是全部,则获取所有机构Id集合 + case (int)DataScopeEnum.All: + orgIdList = await _sysOrgRep.AsQueryable().Select(u => u.Id).ToListAsync(); + break; + // 若数据范围是本部门及以下,则获取本节点和子节点集合 + case (int)DataScopeEnum.DeptChild: + orgIdList = await GetChildIdListWithSelfById(orgId); + break; + // 若数据范围是本部门不含子节点,则直接返回本部门 + case (int)DataScopeEnum.Dept: + orgIdList.Add(orgId); + break; + } + return orgIdList; + } + + /// + /// 根据节点Id获取子节点Id集合(包含自己) + /// + /// + /// + [NonAction] + public async Task> GetChildIdListWithSelfById(long pid) + { + var orgTreeList = await _sysOrgRep.AsQueryable().ToChildListAsync(u => u.Pid, pid, true); + return orgTreeList.Select(u => u.Id).ToList(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Plugin/Dto/PluginInput.cs b/Admin.NET.Core/Service/Plugin/Dto/PluginInput.cs new file mode 100644 index 0000000..f4815ee --- /dev/null +++ b/Admin.NET.Core/Service/Plugin/Dto/PluginInput.cs @@ -0,0 +1,42 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PagePluginInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddPluginInput : SysPlugin +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "功能名称不能为空")] + public override string Name { get; set; } +} + +public class UpdatePluginInput : AddPluginInput +{ +} + +public class DeletePluginInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Plugin/SysPluginService.cs b/Admin.NET.Core/Service/Plugin/SysPluginService.cs new file mode 100644 index 0000000..06694f1 --- /dev/null +++ b/Admin.NET.Core/Service/Plugin/SysPluginService.cs @@ -0,0 +1,126 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统动态插件服务 🧩 +/// +[ApiDescriptionSettings(Order = 245)] +public class SysPluginService : IDynamicApiController, ITransient +{ + private readonly IDynamicApiRuntimeChangeProvider _provider; + private readonly SqlSugarRepository _sysPluginRep; + private readonly UserManager _userManager; + + public SysPluginService(IDynamicApiRuntimeChangeProvider provider, + SqlSugarRepository sysPluginRep, + UserManager userManager) + { + _provider = provider; + _userManager = userManager; + _sysPluginRep = sysPluginRep; + } + + /// + /// 获取动态插件列表 🧩 + /// + /// + /// + [DisplayName("获取动态插件列表")] + public async Task> Page(PagePluginInput input) + { + return await _sysPluginRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加动态插件 🧩 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加动态插件")] + public async Task AddPlugin(AddPluginInput input) + { + var isExist = await _sysPluginRep.IsAnyAsync(u => u.Name == input.Name || u.AssemblyName == input.AssemblyName); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1900); + + // 添加动态程序集/接口 + input.AssemblyName = CompileAssembly(input.CsharpCode, input.AssemblyName); + + await _sysPluginRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新动态插件 🧩 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新动态插件")] + public async Task UpdatePlugin(UpdatePluginInput input) + { + var isExist = await _sysPluginRep.IsAnyAsync(u => (u.Name == input.Name || u.AssemblyName == input.AssemblyName) && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1900); + + // 先移除再添加动态程序集/接口 + RemoveAssembly(input.AssemblyName); + input.AssemblyName = CompileAssembly(input.CsharpCode); + + await _sysPluginRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除动态插件 🧩 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除动态插件")] + public async Task DeletePlugin(DeletePluginInput input) + { + var plugin = await _sysPluginRep.GetByIdAsync(input.Id); + if (plugin == null) return; + + // 移除动态程序集/接口 + RemoveAssembly(plugin.AssemblyName); + + await _sysPluginRep.DeleteAsync(u => u.Id == input.Id); + } + + /// + /// 添加动态程序集/接口 🧩 + /// + /// + /// 程序集名称 + /// + [DisplayName("添加动态程序集/接口")] + public string CompileAssembly([FromBody] string csharpCode, [FromQuery] string assemblyName = default) + { + // 编译 C# 代码并返回动态程序集 + var dynamicAssembly = App.CompileCSharpClassCode(csharpCode, assemblyName); + + // 将程序集添加进动态 WebAPI 应用部件 + _provider.AddAssembliesWithNotifyChanges(dynamicAssembly); + + // 返回动态程序集名称 + return dynamicAssembly.GetName().Name; + } + + /// + /// 移除动态程序集/接口 🧩 + /// + [ApiDescriptionSettings(Name = "RemoveAssembly"), HttpPost] + [DisplayName("移除动态程序集/接口")] + public void RemoveAssembly(string assemblyName) + { + _provider.RemoveAssembliesWithNotifyChanges(assemblyName); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Pos/Dto/PosInput.cs b/Admin.NET.Core/Service/Pos/Dto/PosInput.cs new file mode 100644 index 0000000..615d347 --- /dev/null +++ b/Admin.NET.Core/Service/Pos/Dto/PosInput.cs @@ -0,0 +1,42 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PosInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddPosInput : SysPos +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "职位名称不能为空")] + public override string Name { get; set; } +} + +public class UpdatePosInput : AddPosInput +{ +} + +public class DeletePosInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Pos/SysPosService.cs b/Admin.NET.Core/Service/Pos/SysPosService.cs new file mode 100644 index 0000000..19170f9 --- /dev/null +++ b/Admin.NET.Core/Service/Pos/SysPosService.cs @@ -0,0 +1,110 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统职位服务 🧩 +/// +[ApiDescriptionSettings(Order = 460)] +public class SysPosService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysPosRep; + private readonly SysUserExtOrgService _sysUserExtOrgService; + + public SysPosService(UserManager userManager, + SqlSugarRepository sysPosRep, + SysUserExtOrgService sysUserExtOrgService) + { + _userManager = userManager; + _sysPosRep = sysPosRep; + _sysUserExtOrgService = sysUserExtOrgService; + } + + /// + /// 获取职位列表 🔖 + /// + /// + /// + [DisplayName("获取职位列表")] + public async Task> GetList([FromQuery] PosInput input) + { + return await _sysPosRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .Mapper(u => + { + u.UserList = _sysPosRep.Context.Queryable() + .Where(a => a.PosId == u.Id || SqlFunc.Subqueryable() + .Where(t => a.Id == t.UserId && t.PosId == u.Id).Any()) + .ToList(); + }) + .ToListAsync(); + } + + /// + /// 增加职位 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加职位")] + public async Task AddPos(AddPosInput input) + { + if (await _sysPosRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code)) throw Oops.Oh(ErrorCodeEnum.D6000); + + await _sysPosRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新职位 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新职位")] + public async Task UpdatePos(UpdatePosInput input) + { + if (await _sysPosRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D6000); + + var sysPos = await _sysPosRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D6003); + if (!_userManager.SuperAdmin && sysPos.CreateUserId != _userManager.UserId) throw Oops.Oh(ErrorCodeEnum.D6002); + + await _sysPosRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除职位 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除职位")] + public async Task DeletePos(DeletePosInput input) + { + var sysPos = await _sysPosRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D6003); + if (!_userManager.SuperAdmin && sysPos.CreateUserId != _userManager.UserId) throw Oops.Oh(ErrorCodeEnum.D6002); + + // 若职位有用户则禁止删除 + var hasPosEmp = await _sysPosRep.ChangeRepository>() + .IsAnyAsync(u => u.PosId == input.Id); + if (hasPosEmp) throw Oops.Oh(ErrorCodeEnum.D6001); + + // 若附属职位有用户则禁止删除 + var hasExtPosEmp = await _sysUserExtOrgService.HasUserPos(input.Id); + if (hasExtPosEmp) throw Oops.Oh(ErrorCodeEnum.D6001); + + // 若有绑定注册方案则禁止删除 + var hasUserRegWay = await _sysPosRep.Context.Queryable().AnyAsync(u => u.PosId == input.Id); + if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D6004); + + await _sysPosRep.DeleteAsync(u => u.Id == input.Id); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Print/Dto/PrintInput.cs b/Admin.NET.Core/Service/Print/Dto/PrintInput.cs new file mode 100644 index 0000000..3cf7b32 --- /dev/null +++ b/Admin.NET.Core/Service/Print/Dto/PrintInput.cs @@ -0,0 +1,42 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PagePrintInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddPrintInput : SysPrint +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "模板名称不能为空")] + public override string Name { get; set; } +} + +public class UpdatePrintInput : AddPrintInput +{ +} + +public class DeletePrintInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Print/SysPrintService.cs b/Admin.NET.Core/Service/Print/SysPrintService.cs new file mode 100644 index 0000000..8f70332 --- /dev/null +++ b/Admin.NET.Core/Service/Print/SysPrintService.cs @@ -0,0 +1,91 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统打印模板服务 🧩 +/// +[ApiDescriptionSettings(Order = 305)] +public class SysPrintService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysPrintRep; + private readonly UserManager _userManager; + + public SysPrintService(SqlSugarRepository sysPrintRep, UserManager userManager) + { + _sysPrintRep = sysPrintRep; + _userManager = userManager; + } + + /// + /// 获取打印模板列表 🖨️ + /// + /// + /// + [DisplayName("获取打印模板列表")] + public async Task> Page(PagePrintInput input) + { + return await _sysPrintRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取打印模板 🖨️ + /// + /// + /// + [DisplayName("获取打印模板")] + public async Task GetPrint(string name) + { + return await _sysPrintRep.GetFirstAsync(u => u.Name == name); + } + + /// + /// 增加打印模板 🖨️ + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加打印模板")] + public async Task AddPrint(AddPrintInput input) + { + var isExist = await _sysPrintRep.IsAnyAsync(u => u.Name == input.Name); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1800); + + await _sysPrintRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新打印模板 🖨️ + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新打印模板")] + public async Task UpdatePrint(UpdatePrintInput input) + { + var isExist = await _sysPrintRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1800); + + await _sysPrintRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除打印模板 🖨️ + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除打印模板")] + public async Task DeletePrint(DeletePrintInput input) + { + await _sysPrintRep.DeleteAsync(u => u.Id == input.Id); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Region/Dto/RegionInput.cs b/Admin.NET.Core/Service/Region/Dto/RegionInput.cs new file mode 100644 index 0000000..bb46561 --- /dev/null +++ b/Admin.NET.Core/Service/Region/Dto/RegionInput.cs @@ -0,0 +1,46 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageRegionInput : BasePageInput +{ + /// + /// 父节点Id + /// + public long Pid { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } +} + +public class RegionInput : BaseIdInput +{ +} + +public class AddRegionInput : SysRegion +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "名称不能为空")] + public override string Name { get; set; } +} + +public class UpdateRegionInput : AddRegionInput +{ +} + +public class DeleteRegionInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Region/SysRegionService.cs b/Admin.NET.Core/Service/Region/SysRegionService.cs new file mode 100644 index 0000000..fd257b3 --- /dev/null +++ b/Admin.NET.Core/Service/Region/SysRegionService.cs @@ -0,0 +1,385 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using NewLife.Http; +using NewLife.Serialization; + +namespace Admin.NET.Core.Service; + +/// +/// 系统行政区域服务 🧩 +/// +[ApiDescriptionSettings(Order = 310)] +public class SysRegionService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysRegionRep; + private readonly SysConfigService _sysConfigService; + + public SysRegionService(SqlSugarRepository sysRegionRep, SysConfigService sysConfigService) + { + _sysRegionRep = sysRegionRep; + _sysConfigService = sysConfigService; + } + + /// + /// 获取行政区域分页列表 🔖 + /// + /// + /// + [DisplayName("获取行政区域分页列表")] + public async Task> Page(PageRegionInput input) + { + return await _sysRegionRep.AsQueryable() + .WhereIF(input.Pid > 0, u => u.Pid == input.Pid || u.Id == input.Pid) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取行政区域列表 🔖 + /// + /// + /// + [DisplayName("获取行政区域列表")] + public async Task> GetList([FromQuery] RegionInput input) + { + return await _sysRegionRep.GetListAsync(u => u.Pid == input.Id); + } + + /// + /// 获取行政区域树 🔖 + /// + /// + [DisplayName("获取行政区域树")] + public async Task> GetTree() + { + return await _sysRegionRep.AsQueryable().ToTreeAsync(u => u.Children, u => u.Pid, null); + } + + /// + /// 增加行政区域 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加行政区域")] + public async Task AddRegion(AddRegionInput input) + { + input.Code = input.Code?.Trim() ?? ""; + if (input.Code.Length != 12 && input.Code.Length != 9 && input.Code.Length != 6) throw Oops.Oh(ErrorCodeEnum.R2003); + + if (input.Pid != 0) + { + var pRegion = await _sysRegionRep.GetFirstAsync(u => u.Id == input.Pid); + pRegion ??= await _sysRegionRep.GetFirstAsync(u => u.Code == input.Pid.ToString()); + if (pRegion == null) throw Oops.Oh(ErrorCodeEnum.D2000); + input.Pid = pRegion.Id; + } + + var isExist = await _sysRegionRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.R2002); + + var sysRegion = input.Adapt(); + var newRegion = await _sysRegionRep.AsInsertable(sysRegion).ExecuteReturnEntityAsync(); + return newRegion.Id; + } + + /// + /// 更新行政区域 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新行政区域")] + public async Task UpdateRegion(UpdateRegionInput input) + { + input.Code = input.Code?.Trim() ?? ""; + if (input.Code.Length != 12 && input.Code.Length != 9 && input.Code.Length != 6) throw Oops.Oh(ErrorCodeEnum.R2003); + + var sysRegion = await _sysRegionRep.GetFirstAsync(u => u.Id == input.Id); + if (sysRegion == null) throw Oops.Oh(ErrorCodeEnum.D1002); + + if (sysRegion.Pid != input.Pid && input.Pid != 0) + { + var pRegion = await _sysRegionRep.GetFirstAsync(u => u.Id == input.Pid); + pRegion ??= await _sysRegionRep.GetFirstAsync(u => u.Code == input.Pid.ToString()); + if (pRegion == null) throw Oops.Oh(ErrorCodeEnum.D2000); + + input.Pid = pRegion.Id; + var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + var childIdList = regionTreeList.Select(u => u.Id).ToList(); + if (childIdList.Contains(input.Pid)) throw Oops.Oh(ErrorCodeEnum.R2004); + } + + if (input.Id == input.Pid) throw Oops.Oh(ErrorCodeEnum.R2001); + + var isExist = await _sysRegionRep.IsAnyAsync(u => (u.Name == input.Name && u.Code == input.Code) && u.Id != sysRegion.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.R2002); + + //// 父Id不能为自己的子节点 + //var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + //var childIdList = regionTreeList.Select(u => u.Id).ToList(); + //if (childIdList.Contains(input.Pid)) + // throw Oops.Oh(ErrorCodeEnum.R2001); + + await _sysRegionRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除行政区域 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除行政区域")] + public async Task DeleteRegion(DeleteRegionInput input) + { + var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + var regionIdList = regionTreeList.Select(u => u.Id).ToList(); + await _sysRegionRep.DeleteAsync(u => regionIdList.Contains(u.Id)); + } + + /// + /// 同步行政区域 🔖 + /// + /// + [DisplayName("同步行政区域")] + public async Task Sync() + { + var syncLevel = await _sysConfigService.GetConfigValue(ConfigConst.SysRegionSyncLevel); + if (syncLevel is < 1 or > 5) syncLevel = 3;//默认区县级 + + await _sysRegionRep.AsTenant().UseTranAsync(async () => + { + await _sysRegionRep.DeleteAsync(u => u.Id > 0); + await SyncByMap(syncLevel); + }, err => + { + throw Oops.Oh(ErrorCodeEnum.R2005, err.Message); + }); + + // var context = BrowsingContext.New(AngleSharp.Configuration.Default.WithDefaultLoader()); + // var dom = await context.OpenAsync(_url); + // + // // 省级列表 + // var itemList = dom.QuerySelectorAll("table.provincetable tr.provincetr td a"); + // if (itemList.Length == 0) throw Oops.Oh(ErrorCodeEnum.R2005); + // + // await _sysRegionRep.DeleteAsync(u => u.Id > 0); + // + // foreach (var element in itemList) + // { + // var item = (IHtmlAnchorElement)element; + // var list = new List(); + // + // var region = new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = 0, + // Name = item.TextContent, + // Remark = item.Href, + // Level = 1, + // }; + // list.Add(region); + // + // // 市级 + // if (!string.IsNullOrEmpty(item.Href)) + // { + // var dom1 = await context.OpenAsync(item.Href); + // var itemList1 = dom1.QuerySelectorAll("table.citytable tr.citytr td a"); + // for (var i1 = 0; i1 < itemList1.Length; i1 += 2) + // { + // var item1 = (IHtmlAnchorElement)itemList1[i1 + 1]; + // var region1 = new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = region.Id, + // Name = item1.TextContent, + // Code = itemList1[i1].TextContent, + // Remark = item1.Href, + // Level = 2, + // }; + // + // // 若URL中查询的一级行政区域缺少Code则通过二级区域填充 + // if (list.Count == 1 && !string.IsNullOrEmpty(region1.Code)) + // region.Code = region1.Code.Substring(0, 2).PadRight(region1.Code.Length, '0'); + // + // // 同步层级为“1-省级”退出 + // if (syncLevel < 2) break; + // + // list.Add(region1); + // + // // 区县级 + // if (string.IsNullOrEmpty(item1.Href) || syncLevel <= 2) continue; + // + // var dom2 = await context.OpenAsync(item1.Href); + // var itemList2 = dom2.QuerySelectorAll("table.countytable tr.countytr td a"); + // for (var i2 = 0; i2 < itemList2.Length; i2 += 2) + // { + // var item2 = (IHtmlAnchorElement)itemList2[i2 + 1]; + // var region2 = new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = region1.Id, + // Name = item2.TextContent, + // Code = itemList2[i2].TextContent, + // Remark = item2.Href, + // Level = 3, + // }; + // list.Add(region2); + // + // // 街道级 + // if (string.IsNullOrEmpty(item2.Href) || syncLevel <= 3) continue; + // + // var dom3 = await context.OpenAsync(item2.Href); + // var itemList3 = dom3.QuerySelectorAll("table.towntable tr.towntr td a"); + // for (var i3 = 0; i3 < itemList3.Length; i3 += 2) + // { + // var item3 = (IHtmlAnchorElement)itemList3[i3 + 1]; + // var region3 = new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = region2.Id, + // Name = item3.TextContent, + // Code = itemList3[i3].TextContent, + // Remark = item3.Href, + // Level = 4, + // }; + // list.Add(region3); + // + // // 村级 + // if (string.IsNullOrEmpty(item3.Href) || syncLevel <= 4) continue; + // + // var dom4 = await context.OpenAsync(item3.Href); + // var itemList4 = dom4.QuerySelectorAll("table.villagetable tr.villagetr td"); + // for (var i4 = 0; i4 < itemList4.Length; i4 += 3) + // { + // list.Add(new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = region3.Id, + // Name = itemList4[i4 + 2].TextContent, + // Code = itemList4[i4].TextContent, + // CityCode = itemList4[i4 + 1].TextContent, + // Level = 5, + // }); + // } + // } + // } + // } + // } + // + // //按省份同步快速写入提升同步效率,全部一次性写入容易出现从统计局获取数据失败 + // await _sysRegionRep.Context.Fastest().BulkCopyAsync(list); + // } + } + + /// + /// 从统计局地图页面同步 + /// + /// + private async Task SyncByMap(int syncLevel) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Referer", "http://xzqh.mca.gov.cn/map"); + var html = await client.GetStringAsync("http://xzqh.mca.gov.cn/map"); + + var municipalityList = new List { "北京", "天津", "上海", "重庆" }; + var provList = Regex.Match(html, @"(?<=var json = )(\[\{.*?\}\])(?=;)").Value.ToJsonEntity>>(); + foreach (var dict1 in provList) + { + var list = new List(); + var provName = dict1.GetValueOrDefault("shengji"); + var province = new SysRegion + { + Id = YitIdHelper.NextId(), + Name = Regex.Replace(provName, "[((].*?[))]", ""), + Code = dict1.GetValueOrDefault("quHuaDaiMa"), + CityCode = dict1.GetValueOrDefault("quhao"), + Level = 1, + Pid = 0, + }; + list.Add(province); + + if (syncLevel <= 1) continue; + + var prefList = await GetSelectList(provName); + foreach (var dict2 in prefList) + { + var prefName = dict2.GetValueOrDefault("diji"); + var city = new SysRegion + { + Id = YitIdHelper.NextId(), + Code = dict2.GetValueOrDefault("quHuaDaiMa"), + CityCode = dict2.GetValueOrDefault("quhao"), + Pid = province.Id, + Name = prefName, + Level = 2 + }; + if (municipalityList.Any(m => city.Name.StartsWith(m))) + { + city.Name = "市辖区"; + if (province.Code == city.Code) city.Code = province.Code.Substring(0, 2) + "0100"; + } + list.Add(city); + + if (syncLevel <= 2) continue; + + var countyList = await GetSelectList(provName, prefName); + foreach (var dict3 in countyList) + { + var countyName = dict3.GetValueOrDefault("xianji"); + var county = new SysRegion + { + Id = YitIdHelper.NextId(), + Code = dict3.GetValueOrDefault("quHuaDaiMa"), + CityCode = dict3.GetValueOrDefault("quhao"), + Name = countyName, + Pid = city.Id, + Level = 3 + }; + if (city.Code.IsNullOrEmpty()) + { + // 省直辖县级行政单位 节点无Code编码处理 + city.Code = county.Code.Substring(0, 3).PadRight(6, '0'); + } + list.Add(county); + } + } + + // 按省份同步快速写入提升同步效率,全部一次性写入容易出现从统计局获取数据失败 + // 仅当数据量大于1000或非Oracle数据库时采用大数据量写入方式(SqlSugar官方已说明,数据量小于1000时,其性能不如普通插入, oracle此方法不支持事务) + if (list.Count > 1000 && _sysRegionRep.Context.CurrentConnectionConfig.DbType != SqlSugar.DbType.Oracle) + { + // 执行大数据量写入 + try + { + await _sysRegionRep.Context.Fastest().BulkCopyAsync(list); + } + catch (SqlSugarException) + { + // 若写入失败则尝试普通插入方式 + await _sysRegionRep.InsertRangeAsync(list); + } + } + else + { + await _sysRegionRep.InsertRangeAsync(list); + } + } + + // 获取选择数据 + async Task>> GetSelectList(string prov, string prefecture = null) + { + var data = ""; + if (!string.IsNullOrWhiteSpace(prov)) data += $"shengji={prov}"; + if (!string.IsNullOrWhiteSpace(prefecture)) data += $"&diji={prefecture}"; + var json = await client.PostFormAsync("http://xzqh.mca.gov.cn/selectJson", data); + return json.ToJsonEntity>>(); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Role/Dto/RoleInput.cs b/Admin.NET.Core/Service/Role/Dto/RoleInput.cs new file mode 100644 index 0000000..8f73a5e --- /dev/null +++ b/Admin.NET.Core/Service/Role/Dto/RoleInput.cs @@ -0,0 +1,55 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class RoleInput : BaseIdInput +{ + /// + /// 状态 + /// + public virtual StatusEnum Status { get; set; } +} + +public class PageRoleInput : BasePageInput +{ + /// + /// 租户Id + /// + public long TenantId { get; set; } + + /// + /// 名称 + /// + public virtual string Name { get; set; } + + /// + /// 编码 + /// + public virtual string Code { get; set; } +} + +public class AddRoleInput : SysRole +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "角色名称不能为空")] + public override string Name { get; set; } + + /// + /// 菜单Id集合 + /// + public List MenuIdList { get; set; } +} + +public class UpdateRoleInput : AddRoleInput +{ +} + +public class DeleteRoleInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Role/Dto/RoleMenuInput.cs b/Admin.NET.Core/Service/Role/Dto/RoleMenuInput.cs new file mode 100644 index 0000000..cb3ef0e --- /dev/null +++ b/Admin.NET.Core/Service/Role/Dto/RoleMenuInput.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统角色菜单 +/// +public class RoleMenuInput : BaseIdInput +{ + /// + /// 菜单Id集合 + /// + public List MenuIdList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Role/Dto/RoleMenuOutput .cs b/Admin.NET.Core/Service/Role/Dto/RoleMenuOutput .cs new file mode 100644 index 0000000..e2a4511 --- /dev/null +++ b/Admin.NET.Core/Service/Role/Dto/RoleMenuOutput .cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 角色菜单输出参数 +/// +public class RoleMenuOutput +{ + /// + /// Id + /// + public long Id { get; set; } + + /// + /// 名称 + /// + public string Title { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Role/Dto/RoleOrgInput.cs b/Admin.NET.Core/Service/Role/Dto/RoleOrgInput.cs new file mode 100644 index 0000000..51e8010 --- /dev/null +++ b/Admin.NET.Core/Service/Role/Dto/RoleOrgInput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 授权角色机构 +/// +public class RoleOrgInput : BaseIdInput +{ + /// + /// 数据范围 + /// + public int DataScope { get; set; } + + /// + /// 机构Id集合 + /// + public List OrgIdList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Role/Dto/RoleOutput.cs b/Admin.NET.Core/Service/Role/Dto/RoleOutput.cs new file mode 100644 index 0000000..9acd598 --- /dev/null +++ b/Admin.NET.Core/Service/Role/Dto/RoleOutput.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 角色列表输出参数 +/// +public class RoleOutput +{ + /// + /// Id + /// + public long Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 租户Id + /// + public long tenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Role/SysRoleMenuService.cs b/Admin.NET.Core/Service/Role/SysRoleMenuService.cs new file mode 100644 index 0000000..b80b5ed --- /dev/null +++ b/Admin.NET.Core/Service/Role/SysRoleMenuService.cs @@ -0,0 +1,82 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统角色菜单服务 +/// +public class SysRoleMenuService : ITransient +{ + private readonly SqlSugarRepository _sysRoleMenuRep; + private readonly SysCacheService _sysCacheService; + + public SysRoleMenuService(SqlSugarRepository sysRoleMenuRep, + SysCacheService sysCacheService) + { + _sysRoleMenuRep = sysRoleMenuRep; + _sysCacheService = sysCacheService; + } + + /// + /// 根据角色Id集合获取菜单Id集合 + /// + /// + /// + public async Task> GetRoleMenuIdList(List roleIdList) + { + return await _sysRoleMenuRep.AsQueryable() + .Where(u => roleIdList.Contains(u.RoleId)) + .Select(u => u.MenuId).ToListAsync(); + } + + /// + /// 授权角色菜单 + /// + /// + /// + public async Task GrantRoleMenu(RoleMenuInput input) + { + await _sysRoleMenuRep.DeleteAsync(u => u.RoleId == input.Id); + + // 追加父级菜单 + var allIdList = await _sysRoleMenuRep.Context.Queryable().Select(u => new { u.Id, u.Pid }).ToListAsync(); + var pIdList = allIdList.ToChildList(u => u.Pid, u => u.Id, u => input.MenuIdList.Contains(u.Id)).Select(u => u.Pid).Distinct().ToList(); + input.MenuIdList = input.MenuIdList.Concat(pIdList).Distinct().Where(u => u != 0).ToList(); + + // 保存授权数据 + var menus = input.MenuIdList.Select(u => new SysRoleMenu + { + RoleId = input.Id, + MenuId = u + }).ToList(); + await _sysRoleMenuRep.InsertRangeAsync(menus); + + // 清除缓存 + // _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserMenu); + _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserButton); + } + + /// + /// 根据菜单Id集合删除角色菜单 + /// + /// + /// + public async Task DeleteRoleMenuByMenuIdList(List menuIdList) + { + await _sysRoleMenuRep.DeleteAsync(u => menuIdList.Contains(u.MenuId)); + } + + /// + /// 根据角色Id删除角色菜单 + /// + /// + /// + public async Task DeleteRoleMenuByRoleId(long roleId) + { + await _sysRoleMenuRep.DeleteAsync(u => u.RoleId == roleId); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Role/SysRoleOrgService.cs b/Admin.NET.Core/Service/Role/SysRoleOrgService.cs new file mode 100644 index 0000000..fe07744 --- /dev/null +++ b/Admin.NET.Core/Service/Role/SysRoleOrgService.cs @@ -0,0 +1,75 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统角色机构服务 +/// +public class SysRoleOrgService : ITransient +{ + private readonly SqlSugarRepository _sysRoleOrgRep; + + public SysRoleOrgService(SqlSugarRepository sysRoleOrgRep) + { + _sysRoleOrgRep = sysRoleOrgRep; + } + + /// + /// 授权角色机构 + /// + /// + /// + public async Task GrantRoleOrg(RoleOrgInput input) + { + await _sysRoleOrgRep.DeleteAsync(u => u.RoleId == input.Id); + if (input.DataScope == (int)DataScopeEnum.Define) + { + var roleOrgList = input.OrgIdList.Select(u => new SysRoleOrg + { + RoleId = input.Id, + OrgId = u + }).ToList(); + await _sysRoleOrgRep.InsertRangeAsync(roleOrgList); + } + } + + /// + /// 根据角色Id集合获取角色机构Id集合 + /// + /// + /// + public async Task> GetRoleOrgIdList(List roleIdList) + { + if (roleIdList?.Count > 0) + { + return await _sysRoleOrgRep.AsQueryable() + .Where(u => roleIdList.Contains(u.RoleId)) + .Select(u => u.OrgId).ToListAsync(); + } + else return new List(); + } + + /// + /// 根据机构Id集合删除角色机构 + /// + /// + /// + public async Task DeleteRoleOrgByOrgIdList(List orgIdList) + { + await _sysRoleOrgRep.DeleteAsync(u => orgIdList.Contains(u.OrgId)); + } + + /// + /// 根据角色Id删除角色机构 + /// + /// + /// + public async Task DeleteRoleOrgByRoleId(long roleId) + { + await _sysRoleOrgRep.DeleteAsync(u => u.RoleId == roleId); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Role/SysRoleService.cs b/Admin.NET.Core/Service/Role/SysRoleService.cs new file mode 100644 index 0000000..0dd4d21 --- /dev/null +++ b/Admin.NET.Core/Service/Role/SysRoleService.cs @@ -0,0 +1,276 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统角色服务 🧩 +/// +[ApiDescriptionSettings(Order = 480)] +public class SysRoleService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysRoleRep; + private readonly SysRoleMenuService _sysRoleMenuService; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysRoleOrgService _sysRoleOrgService; + private readonly SysMenuService _sysMenuService; + private readonly SysOrgService _sysOrgService; + private readonly SysCacheService _sysCacheService; + + public SysRoleService(UserManager userManager, + SysOrgService sysOrgService, + SysMenuService sysMenuService, + SysRoleOrgService sysRoleOrgService, + SqlSugarRepository sysRoleRep, + SysRoleMenuService sysRoleMenuService, + SysUserRoleService sysUserRoleService, + SysCacheService sysCacheService) + { + _userManager = userManager; + _sysRoleRep = sysRoleRep; + _sysOrgService = sysOrgService; + _sysMenuService = sysMenuService; + _sysRoleOrgService = sysRoleOrgService; + _sysRoleMenuService = sysRoleMenuService; + _sysUserRoleService = sysUserRoleService; + _sysCacheService = sysCacheService; + } + + /// + /// 获取角色分页列表 🔖 + /// + /// + /// + [DisplayName("获取角色分页列表")] + public async Task> Page(PageRoleInput input) + { + // 当前用户已拥有的角色集合 + var roleIdList = _userManager.SuperAdmin ? new List() : await _sysUserRoleService.GetUserRoleIdList(_userManager.UserId); + return await _sysRoleRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!_userManager.SuperAdmin, u => u.TenantId == _userManager.TenantId) // 若非超管,则只能操作本租户的角色 + .WhereIF(!_userManager.SuperAdmin && !_userManager.SysAdmin, u => u.CreateUserId == _userManager.UserId || roleIdList.Contains(u.Id)) // 若非超管且非系统管理员,则只能操作自己创建的角色|自己拥有的角色 + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取角色列表 🔖 + /// + /// + [DisplayName("获取角色列表")] + public async Task> GetList() + { + // 当前用户已拥有的角色集合 + var roleIdList = _userManager.SuperAdmin ? new List() : await _sysUserRoleService.GetUserRoleIdList(_userManager.UserId); + + return await _sysRoleRep.AsQueryable() + .WhereIF(!_userManager.SuperAdmin, u => u.TenantId == _userManager.TenantId) // 若非超管,则只能操作本租户的角色 + .WhereIF(!_userManager.SuperAdmin && !_userManager.SysAdmin, u => u.CreateUserId == _userManager.UserId || roleIdList.Contains(u.Id)) // 若非超管且非系统管理员,则只显示自己创建和已拥有的角色 + .Where(u => u.Status != StatusEnum.Disable) // 非禁用的 + .OrderBy(u => new { u.OrderNo, u.Id }).Select().ToListAsync(); + } + + /// + /// 增加角色 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加角色")] + public async Task AddRole(AddRoleInput input) + { + if (await _sysRoleRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code)) + throw Oops.Oh(ErrorCodeEnum.D1006); + + var newRole = await _sysRoleRep.AsInsertable(input.Adapt()).ExecuteReturnEntityAsync(); + input.Id = newRole.Id; + await UpdateRoleMenu(input); + } + + /// + /// 更新角色菜单权限 + /// + /// + /// + private async Task UpdateRoleMenu(AddRoleInput input) + { + if (input.MenuIdList == null || input.MenuIdList.Count < 1) return; + await GrantMenu(new RoleMenuInput() + { + Id = input.Id, + MenuIdList = input.MenuIdList.ToList() + }); + } + + /// + /// 更新角色 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新角色")] + public async Task UpdateRole(UpdateRoleInput input) + { + if (await _sysRoleRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D1006); + + await _sysRoleRep.AsUpdateable(input.Adapt()).IgnoreColumns(true) + .IgnoreColumns(u => new { u.DataScope }).ExecuteCommandAsync(); + + await UpdateRoleMenu(input); + } + + /// + /// 删除角色 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除角色")] + public async Task DeleteRole(DeleteRoleInput input) + { + // 若角色有用户则禁止删除 + var userIds = await _sysUserRoleService.GetUserIdList(input.Id); + if (userIds != null && userIds.Count > 0) throw Oops.Oh(ErrorCodeEnum.D1025); + + // 若有绑定注册方案则禁止删除 + var hasUserRegWay = await _sysRoleRep.Context.Queryable().AnyAsync(u => u.RoleId == input.Id); + if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D1033); + + var sysRole = await _sysRoleRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + await _sysRoleRep.DeleteAsync(sysRole); + + // 级联删除角色机构数据 + await _sysRoleOrgService.DeleteRoleOrgByRoleId(sysRole.Id); + + // 级联删除用户角色数据 + await _sysUserRoleService.DeleteUserRoleByRoleId(sysRole.Id); + + // 级联删除角色菜单数据 + await _sysRoleMenuService.DeleteRoleMenuByRoleId(sysRole.Id); + } + + /// + /// 授权角色菜单 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权角色菜单")] + public async Task GrantMenu(RoleMenuInput input) + { + if (input.MenuIdList == null || input.MenuIdList.Count < 1) return; + + await ClearUserApiCache(input.Id); + + await _sysRoleMenuService.GrantRoleMenu(input); + } + + /// + /// 授权角色数据范围 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权角色数据范围")] + public async Task GrantDataScope(RoleOrgInput input) + { + // 删除与该角色相关的用户机构缓存 + var userIdList = await _sysUserRoleService.GetUserIdList(input.Id); + foreach (var userId in userIdList) + { + SqlSugarFilter.DeleteUserOrgCache(userId, _sysRoleRep.Context.CurrentConnectionConfig.ConfigId.ToString()); + } + + var role = await _sysRoleRep.GetFirstAsync(u => u.Id == input.Id); + var dataScope = input.DataScope; + if (!_userManager.SuperAdmin) + { + switch (dataScope) + { + // 非超级管理员没有全部数据范围权限 + case (int)DataScopeEnum.All: throw Oops.Oh(ErrorCodeEnum.D1016); + // 若数据范围自定义,则判断授权数据范围是否有权限 + case (int)DataScopeEnum.Define: + { + var grantOrgIdList = input.OrgIdList; + if (grantOrgIdList.Count > 0) + { + var orgIdList = await _sysOrgService.GetUserOrgIdList(); + if (orgIdList.Count < 1) + throw Oops.Oh(ErrorCodeEnum.D1016); + if (!grantOrgIdList.All(u => orgIdList.Any(c => c == u))) + throw Oops.Oh(ErrorCodeEnum.D1016); + } + + break; + } + } + } + role.DataScope = (DataScopeEnum)dataScope; + await _sysRoleRep.AsUpdateable(role).UpdateColumns(u => new { u.DataScope }).ExecuteCommandAsync(); + await _sysRoleOrgService.GrantRoleOrg(input); + } + + /// + /// 根据角色Id获取菜单Id集合 🔖 + /// + /// + /// + [DisplayName("根据角色Id获取菜单Id集合")] + public async Task> GetOwnMenuList([FromQuery] RoleInput input) + { + var menuIds = await _sysRoleMenuService.GetRoleMenuIdList(new List { input.Id }); + return await _sysMenuService.ExcludeParentMenuOfFullySelected(menuIds); + } + + /// + /// 根据角色Id获取机构Id集合 🔖 + /// + /// + /// + [DisplayName("根据角色Id获取机构Id集合")] + public async Task> GetOwnOrgList([FromQuery] RoleInput input) + { + return await _sysRoleOrgService.GetRoleOrgIdList(new List { input.Id }); + } + + /// + /// 设置角色状态 🔖 + /// + /// + /// + [DisplayName("设置角色状态")] + public async Task SetStatus(RoleInput input) + { + if (!Enum.IsDefined(typeof(StatusEnum), input.Status)) throw Oops.Oh(ErrorCodeEnum.D3005); + + return await _sysRoleRep.AsUpdateable() + .SetColumns(u => u.Status == input.Status) + .Where(u => u.Id == input.Id) + .ExecuteCommandAsync(); + } + + /// + /// 删除与该角色相关的用户接口缓存 + /// + /// + /// + [NonAction] + public async Task ClearUserApiCache(long roleId) + { + var userIdList = await _sysUserRoleService.GetUserIdList(roleId); + foreach (var userId in userIdList) + { + _sysCacheService.Remove($"{CacheConst.KeyUserButton}{userId}"); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Schedule/Dto/ScheduleInput.cs b/Admin.NET.Core/Service/Schedule/Dto/ScheduleInput.cs new file mode 100644 index 0000000..73887c2 --- /dev/null +++ b/Admin.NET.Core/Service/Schedule/Dto/ScheduleInput.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class ScheduleInput : BaseIdInput +{ + /// + /// 状态 + /// + public virtual FinishStatusEnum Status { get; set; } +} + +public class ListScheduleInput +{ + public DateTime? StartTime { get; set; } + + public DateTime? EndTime { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddScheduleInput : SysSchedule +{ + /// + /// 日程内容 + /// + [Required(ErrorMessage = "日程内容不能为空")] + public override string Content { get; set; } +} + +public class UpdateScheduleInput : AddScheduleInput +{ +} + +public class DeleteScheduleInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Schedule/SysScheduleService.cs b/Admin.NET.Core/Service/Schedule/SysScheduleService.cs new file mode 100644 index 0000000..c7f56e2 --- /dev/null +++ b/Admin.NET.Core/Service/Schedule/SysScheduleService.cs @@ -0,0 +1,104 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统日程服务 +/// +[ApiDescriptionSettings(Order = 295)] +public class SysScheduleService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysSchedule; + + public SysScheduleService(UserManager userManager, + SqlSugarRepository sysSchedule) + { + _userManager = userManager; + _sysSchedule = sysSchedule; + } + + /// + /// 获取日程列表 + /// + /// + [DisplayName("获取日程列表")] + public async Task> Page(ListScheduleInput input) + { + return await _sysSchedule.AsQueryable() + .Where(u => u.UserId == _userManager.UserId) + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.ScheduleTime >= input.StartTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.ScheduleTime <= input.EndTime) + .OrderBy(u => u.StartTime, OrderByType.Asc) + .ToListAsync(); + } + + /// + /// 获取日程详情 + /// + /// + /// + [DisplayName("获取日程详情")] + public async Task GetDetail(long id) + { + return await _sysSchedule.GetFirstAsync(u => u.Id == id); + } + + /// + /// 增加日程 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加日程")] + public async Task AddUserSchedule(AddScheduleInput input) + { + input.UserId = _userManager.UserId; + await _sysSchedule.InsertAsync(input.Adapt()); + } + + /// + /// 更新日程 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新日程")] + public async Task UpdateUserSchedule(UpdateScheduleInput input) + { + await _sysSchedule.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除日程 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除日程")] + public async Task DeleteUserSchedule(DeleteScheduleInput input) + { + await _sysSchedule.DeleteAsync(u => u.Id == input.Id); + } + + /// + /// 设置日程状态 + /// + /// + /// + [DisplayName("设置日程状态")] + public async Task SetStatus(ScheduleInput input) + { + if (!Enum.IsDefined(typeof(FinishStatusEnum), input.Status)) throw Oops.Oh(ErrorCodeEnum.D3005); + + return await _sysSchedule.AsUpdateable() + .SetColumns(u => u.Status == input.Status) + .Where(u => u.Id == input.Id) + .ExecuteCommandAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Server/SysServerService.cs b/Admin.NET.Core/Service/Server/SysServerService.cs new file mode 100644 index 0000000..574cd41 --- /dev/null +++ b/Admin.NET.Core/Service/Server/SysServerService.cs @@ -0,0 +1,191 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +#if NET10_0_OR_GREATER + +using XiHan.Framework.Utils.Core; +using XiHan.Framework.Utils.Reflections; +using ReflectionHelper = XiHan.Framework.Utils.Reflections.ReflectionHelper; + +#endif // NET10_0_OR_GREATER + +namespace Admin.NET.Core.Service; + +/// +/// 系统服务器监控服务 🧩 +/// +[ApiDescriptionSettings(Order = 290, Description = "服务器监控")] +public class SysServerService : IDynamicApiController, ITransient +{ + public SysServerService() + { + } + +#if NET10_0_OR_GREATER + + /// + /// 获取服务器硬件信息 + /// + /// + [DisplayName("获取服务器硬件信息")] + public SystemInfo HardwareInfo() + { + var hardwareInfo = SystemInfoManager.GetSystemInfo(); + return hardwareInfo; + } + + /// + /// 获取服务器运行时信息 + /// + /// + [DisplayName("获取服务器运行时信息")] + public XiHan.Framework.Utils.Runtime.RuntimeInfo RuntimeInfo() + { + var systemRuntimeInfo = new XiHan.Framework.Utils.Runtime.RuntimeInfo(); + return systemRuntimeInfo; + } + + /// + /// 获取框架主要程序集 + /// + /// + [DisplayName("获取框架主要程序集")] + public List NuGetPackagesInfo() + { + var nuGetPackages = ReflectionHelper.GetNuGetPackages("Admin.NET"); + return nuGetPackages; + } + +#endif // NET10_0_OR_GREATER + + /// + /// 获取服务器配置信息 🔖 + /// + /// + [DisplayName("获取服务器配置信息")] + public dynamic GetServerBase() + { + return new + { + HostName = Environment.MachineName, // 主机名称 + SystemOs = ComputerUtil.GetOSInfo(),//RuntimeInformation.OSDescription, // 操作系统 + OsArchitecture = Environment.OSVersion.Platform.ToString() + " " + RuntimeInformation.OSArchitecture.ToString(), // 系统架构 + ProcessorCount = Environment.ProcessorCount + " 核", // CPU核心数 + SysRunTime = ComputerUtil.GetRunTime(), // 系统运行时间 + RemoteIp = ComputerUtil.GetIpFromOnline(), // 外网地址 + LocalIp = App.HttpContext?.Connection?.LocalIpAddress!.MapToIPv4().ToString(), // 本地地址 + FrameworkDescription = RuntimeInformation.FrameworkDescription + " / " + App.GetOptions().ConnectionConfigs[0].DbType.ToString(), // NET框架 + 数据库类型 + Environment = App.HostEnvironment.IsDevelopment() ? "Development" : "Production", + Wwwroot = App.WebHostEnvironment.WebRootPath, // 网站根目录 + Stage = App.HostEnvironment.IsStaging() ? "Stage环境" : "非Stage环境", // 是否Stage环境 + }; + } + + /// + /// 获取服务器使用信息 🔖 + /// + /// + [DisplayName("获取服务器使用信息")] + public dynamic GetServerUsed() + { + var programStartTime = Process.GetCurrentProcess().StartTime; + var totalMilliseconds = (DateTime.Now - programStartTime).TotalMilliseconds.ToString(); + var ts = totalMilliseconds.Contains('.') ? totalMilliseconds.Split('.')[0] : totalMilliseconds; + var programRunTime = DateTimeUtil.FormatTime(ts.ParseToLong()); + + var memoryMetrics = ComputerUtil.GetComputerInfo(); + return new + { + memoryMetrics.FreeRam, // 空闲内存 + memoryMetrics.UsedRam, // 已用内存 + memoryMetrics.TotalRam, // 总内存 + memoryMetrics.RamRate, // 内存使用率 + memoryMetrics.CpuRates, // Cpu使用率多CPU未完成 + memoryMetrics.CpuRate, // Cpu 1使用率 + StartTime = programStartTime.ToString("yyyy-MM-dd HH:mm:ss"), // 服务启动时间 + RunTime = programRunTime, // 服务运行时间 + }; + } + + /// + /// 获取服务器磁盘信息 🔖 + /// + /// + [DisplayName("获取服务器磁盘信息")] + public dynamic GetServerDisk() + { + return ComputerUtil.GetDiskInfos(); + } + + /// + /// 获取框架主要程序集 🔖 + /// + /// + [DisplayName("获取框架主要程序集")] + public dynamic GetAssemblyList() + { + var furionAssembly = typeof(App).Assembly.GetName(); + var sqlSugarAssembly = typeof(ISqlSugarClient).Assembly.GetName(); + var yitIdAssembly = typeof(YitIdHelper).Assembly.GetName(); + var redisAssembly = typeof(Redis).Assembly.GetName(); + var jsonAssembly = typeof(NewtonsoftJsonMvcCoreBuilderExtensions).Assembly.GetName(); + var excelAssembly = typeof(IExcelImporter).Assembly.GetName(); + var pdfAssembly = typeof(Magicodes.ExporterAndImporter.Pdf.IPdfExporter).Assembly.GetName(); + var wordAssembly = typeof(Magicodes.ExporterAndImporter.Word.IWordExporter).Assembly.GetName(); + var captchaAssembly = typeof(Lazy.Captcha.Core.ICaptcha).Assembly.GetName(); + var wechatApiAssembly = typeof(WechatApiClient).Assembly.GetName(); + var wechatTenpayAssembly = typeof(WechatTenpayClient).Assembly.GetName(); + var ossAssembly = typeof(OnceMi.AspNetCore.OSS.IOSSServiceFactory).Assembly.GetName(); + var parserAssembly = typeof(Parser).Assembly.GetName(); + var elasticsearchClientAssembly = typeof(Elastic.Clients.Elasticsearch.ElasticsearchClient).Assembly.GetName(); + var limitAssembly = typeof(AspNetCoreRateLimit.IpRateLimitMiddleware).Assembly.GetName(); + var htmlParserAssembly = typeof(AngleSharp.Html.Parser.HtmlParser).Assembly.GetName(); + var fluentEmailAssembly = typeof(MailKit.Net.Smtp.SmtpClient).Assembly.GetName(); + var qRCodeGeneratorAssembly = typeof(QRCoder.QRCodeGenerator).Assembly.GetName(); + var alibabaSendSmsRequestAssembly = typeof(AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest).Assembly.GetName(); + var tencentSendSmsRequestAssembly = typeof(TencentCloud.Sms.V20190711.Models.SendSmsRequest).Assembly.GetName(); + var rabbitMQAssembly = typeof(RabbitMQEventSourceStore).Assembly.GetName(); + var ldapConnectionAssembly = typeof(Novell.Directory.Ldap.LdapConnection).Assembly.GetName(); + var ipToolAssembly = typeof(IPTools.Core.IpTool).Assembly.GetName(); + var weixinAuthenticationOptionsAssembly = typeof(AspNet.Security.OAuth.Weixin.WeixinAuthenticationOptions).Assembly.GetName(); + var giteeAuthenticationOptionsAssembly = typeof(AspNet.Security.OAuth.Gitee.GiteeAuthenticationOptions).Assembly.GetName(); + var hashidsAssembly = typeof(HashidsNet.Hashids).Assembly.GetName(); + var sftpClientAssembly = typeof(Renci.SshNet.SftpClient).Assembly.GetName(); + var hardwareInfoAssembly = typeof(Hardware.Info.HardwareInfo).Assembly.GetName(); + + return new[] + { + new { furionAssembly.Name, furionAssembly.Version }, + new { sqlSugarAssembly.Name, sqlSugarAssembly.Version }, + new { yitIdAssembly.Name, yitIdAssembly.Version }, + new { redisAssembly.Name, redisAssembly.Version }, + new { jsonAssembly.Name, jsonAssembly.Version }, + new { excelAssembly.Name, excelAssembly.Version }, + new { pdfAssembly.Name, pdfAssembly.Version }, + new { wordAssembly.Name, wordAssembly.Version }, + new { captchaAssembly.Name, captchaAssembly.Version }, + new { wechatApiAssembly.Name, wechatApiAssembly.Version }, + new { wechatTenpayAssembly.Name, wechatTenpayAssembly.Version }, + new { ossAssembly.Name, ossAssembly.Version }, + new { parserAssembly.Name, parserAssembly.Version }, + new { elasticsearchClientAssembly.Name, elasticsearchClientAssembly.Version }, + new { limitAssembly.Name, limitAssembly.Version }, + new { htmlParserAssembly.Name, htmlParserAssembly.Version }, + new { fluentEmailAssembly.Name, fluentEmailAssembly.Version }, + new { qRCodeGeneratorAssembly.Name, qRCodeGeneratorAssembly.Version }, + new { alibabaSendSmsRequestAssembly.Name, alibabaSendSmsRequestAssembly.Version }, + new { tencentSendSmsRequestAssembly.Name, tencentSendSmsRequestAssembly.Version }, + new { rabbitMQAssembly.Name, rabbitMQAssembly.Version }, + new { ldapConnectionAssembly.Name, ldapConnectionAssembly.Version }, + new { ipToolAssembly.Name, ipToolAssembly.Version }, + new { weixinAuthenticationOptionsAssembly.Name, weixinAuthenticationOptionsAssembly.Version }, + new { giteeAuthenticationOptionsAssembly.Name, giteeAuthenticationOptionsAssembly.Version }, + new { hashidsAssembly.Name, hashidsAssembly.Version }, + new { sftpClientAssembly.Name, sftpClientAssembly.Version }, + new { hardwareInfoAssembly.Name, hardwareInfoAssembly.Version }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Template/Dto/SysTemplateInput.cs b/Admin.NET.Core/Service/Template/Dto/SysTemplateInput.cs new file mode 100644 index 0000000..30cdbae --- /dev/null +++ b/Admin.NET.Core/Service/Template/Dto/SysTemplateInput.cs @@ -0,0 +1,114 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageTemplateInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 分组名称 + /// + public string GroupName { get; set; } + + /// + /// 模板类型 + /// + public TemplateTypeEnum? Type { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +/// +/// 新增模板输入参数 +/// +public class AddTemplateInput : SysTemplate +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "名称不能为空")] + public override string Name { get; set; } + + /// + /// 模板类型 + /// + [Enum] + public override TemplateTypeEnum Type { get; set; } + + /// + /// 编码 + /// + [Required(ErrorMessage = "编码不能为空")] + public override string Code { get; set; } + + /// + /// 分组名称 + /// + [Required(ErrorMessage = "分组名称不能为空")] + public override string GroupName { get; set; } + + /// + /// 模板内容 + /// + [Required(ErrorMessage = "内容名称不能为空")] + public override string Content { get; set; } +} + +/// +/// 更新模板输入参数 +/// +public class UpdateTemplateInput : AddTemplateInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "Id不能为空")] + [DataValidation(ValidationTypes.Numeric)] + public override long Id { get; set; } +} + +/// +/// 预览模板输入参数 +/// +public class ProViewTemplateInput : BaseIdInput +{ + /// + /// 渲染参数 + /// + [Required(ErrorMessage = "渲染参数不能为空")] + public object Data { get; set; } +} + +/// +/// 模板渲染输入参数 +/// +public class RenderTemplateInput +{ + /// + /// 模板内容 + /// + [Required(ErrorMessage = "内容名称不能为空")] + public string Content { get; set; } + + /// + /// 渲染参数 + /// + [Required(ErrorMessage = "渲染参数不能为空")] + public object Data { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Template/Dto/SysTemplateOutput.cs b/Admin.NET.Core/Service/Template/Dto/SysTemplateOutput.cs new file mode 100644 index 0000000..a3a2108 --- /dev/null +++ b/Admin.NET.Core/Service/Template/Dto/SysTemplateOutput.cs @@ -0,0 +1,11 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class SysTemplateOutput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Template/SysTemplateService.cs b/Admin.NET.Core/Service/Template/SysTemplateService.cs new file mode 100644 index 0000000..aec7269 --- /dev/null +++ b/Admin.NET.Core/Service/Template/SysTemplateService.cs @@ -0,0 +1,179 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统消息模板服务 🧩 +/// +[ApiDescriptionSettings(Order = 305)] +public class SysTemplateService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysTemplateRep; + private readonly UserManager _userManager; + private readonly IViewEngine _viewEngine; + + public SysTemplateService( + SqlSugarRepository sysTemplateRep, + IViewEngine viewEngine, + UserManager userManager) + { + _sysTemplateRep = sysTemplateRep; + _userManager = userManager; + _viewEngine = viewEngine; + } + + /// + /// 获取模板列表 📑 + /// + /// + /// + [ApiDescriptionSettings] + [DisplayName("获取模板列表")] + public async Task> Page(PageTemplateInput input) + { + return await _sysTemplateRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), u => u.GroupName.Contains(input.GroupName)) + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .WhereIF(input.Type.HasValue, u => u.Type == input.Type) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取模板 📑 + /// + /// + /// + [DisplayName("获取模板")] + [ApiDescriptionSettings] + public async Task GetTemplate(string code) + { + return await _sysTemplateRep.GetFirstAsync(u => u.Name == code); + } + + /// + /// 预览模板内容 📑 + /// + /// + /// + [DisplayName("预览模板内容")] + [ApiDescriptionSettings] + public async Task ProView(ProViewTemplateInput input) + { + var template = await _sysTemplateRep.GetFirstAsync(u => u.Id == input.Id); + return await RenderAsync(template.Content, input.Data); + } + + /// + /// 增加模板 📑 + /// + /// + /// + [DisplayName("增加模板")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task AddTemplate(AddTemplateInput input) + { + var isExist = await _sysTemplateRep.IsAnyAsync(u => u.Name == input.Name); + if (isExist) throw Oops.Oh(ErrorCodeEnum.T1000); + + isExist = await _sysTemplateRep.IsAnyAsync(u => u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.T1001); + + await _sysTemplateRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新模板 📑 + /// + /// + /// + [DisplayName("更新模板")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task UpdateTemplate(UpdateTemplateInput input) + { + var isExist = await _sysTemplateRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.T1000); + + isExist = await _sysTemplateRep.IsAnyAsync(u => u.Code == input.Code && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.T1001); + + await _sysTemplateRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除模板 📑 + /// + /// + /// + [DisplayName("删除模板")] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task DeleteTemplate(BaseIdInput input) + { + await _sysTemplateRep.DeleteAsync(u => u.Id == input.Id); + } + + /// + /// 获取分组列表 🔖 + /// + /// + [ApiDescriptionSettings] + [DisplayName("获取分组列表")] + public async Task> GetGroupList() + { + return await _sysTemplateRep.AsQueryable() + .GroupBy(u => u.GroupName) + .Select(u => u.GroupName).ToListAsync(); + } + + /// + /// 渲染模板内容 📑 + /// + /// + /// + [DisplayName("渲染模板内容")] + [ApiDescriptionSettings, HttpPost] + public async Task Render(RenderTemplateInput input) + { + return await RenderAsync(input.Content, input.Data); + } + + /// + /// 渲染模板内容 📑 + /// + /// + /// + /// + [NonAction] + public async Task RenderAsync(string content, object data) + { + return await _viewEngine.RunCompileFromCachedAsync(Regex.Replace(content, "@\\((.*?)\\)", "@(Model.$1)"), 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"); + }); + } + + /// + /// 根据编码渲染模板内容 + /// + /// + /// + /// + [NonAction] + public async Task RenderByCode(string code, Dictionary data) + { + var template = await _sysTemplateRep.GetFirstAsync(u => u.Code == code); + return await RenderAsync(template.Content, data); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs b/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs new file mode 100644 index 0000000..b6c3e27 --- /dev/null +++ b/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs @@ -0,0 +1,124 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class TenantInput : BaseIdInput +{ + /// + /// 状态 + /// + public StatusEnum Status { get; set; } +} + +public class PageTenantInput : BasePageInput +{ + /// + /// 名称 + /// + public virtual string Name { get; set; } + + /// + /// 电话 + /// + public virtual string Phone { get; set; } +} + +public class AddTenantInput : TenantOutput +{ + /// + /// 租户名称 + /// + [Required(ErrorMessage = "租户名称不能为空"), MinLength(2, ErrorMessage = "租户名称不能少于2个字符")] + public override string Name { get; set; } + + /// + /// 租管账号 + /// + [Required(ErrorMessage = "租管账号不能为空"), MinLength(3, ErrorMessage = "租管账号不能少于3个字符")] + public override string AdminAccount { get; set; } + + /// + /// 系统主标题 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Title)", "系统主标题不能为空")] + public override string Title { get; set; } + + /// + /// 系统副标题 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(ViceTitle)", "系统副标题不能为空")] + public override string ViceTitle { get; set; } + + /// + /// 系统描述 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(ViceDesc)", "系统描述不能为空")] + public override string ViceDesc { get; set; } + + /// + /// 版权说明 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Copyright)", "版权说明不能为空")] + public override string Copyright { get; set; } + + /// + /// ICP备案号 + /// + public override string Icp { get; set; } + + /// + /// ICP地址 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && !string.IsNullOrWhiteSpace(Icp) && string.IsNullOrWhiteSpace(IcpUrl)", "ICP地址不能为空")] + public override string IcpUrl { get; set; } + + /// + /// Logo图片Base64码 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Logo) && string.IsNullOrWhiteSpace(LogoBase64)", "图标不能为空")] + public virtual string LogoBase64 { get; set; } + + /// + /// Logo文件名 + /// + public virtual string LogoFileName { get; set; } +} + +public class UpdateTenantInput : AddTenantInput +{ +} + +public class DeleteTenantInput : BaseIdInput +{ +} + +/// +/// 租户菜单 +/// +public class TenantMenuInput : BaseIdInput +{ + /// + /// 菜单Id集合 + /// + public List MenuIdList { get; set; } +} + +public class TenantUserInput +{ + /// + /// 用户Id + /// + public long UserId { get; set; } +} + +public class TenantIdInput +{ + /// + /// 租户Id + /// + public long TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Tenant/Dto/TenantOutput.cs b/Admin.NET.Core/Service/Tenant/Dto/TenantOutput.cs new file mode 100644 index 0000000..6837efb --- /dev/null +++ b/Admin.NET.Core/Service/Tenant/Dto/TenantOutput.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class TenantOutput : SysTenant +{ + /// + /// 租户名称 + /// + public virtual string Name { get; set; } + + /// + /// 管理员账号 + /// + public virtual string AdminAccount { get; set; } + + /// + /// 电子邮箱 + /// + public virtual string Email { get; set; } + + /// + /// 电话 + /// + public virtual string Phone { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Tenant/SysTenantService.cs b/Admin.NET.Core/Service/Tenant/SysTenantService.cs new file mode 100644 index 0000000..a9af1cd --- /dev/null +++ b/Admin.NET.Core/Service/Tenant/SysTenantService.cs @@ -0,0 +1,767 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统租户管理服务 🧩 +/// +[ApiDescriptionSettings(Order = 390)] +public class SysTenantService : IDynamicApiController, ITransient +{ + private static readonly SysMenuService SysMenuService = App.GetService(); + private readonly SqlSugarRepository _sysUserExtOrgRep; + private readonly SqlSugarRepository _sysTenantMenuRep; + private readonly SqlSugarRepository _sysRoleMenuRep; + private readonly SqlSugarRepository _userRoleRep; + private readonly SqlSugarRepository _sysTenantRep; + private readonly SqlSugarRepository _sysRoleRep; + private readonly SqlSugarRepository _sysUserRep; + private readonly SqlSugarRepository _sysOrgRep; + private readonly SqlSugarRepository _sysPosRep; + private readonly SysRoleMenuService _sysRoleMenuService; + private readonly SysConfigService _sysConfigService; + private readonly SysCacheService _sysCacheService; + private readonly UploadOptions _uploadOptions; + + public SysTenantService( + SqlSugarRepository sysUserExtOrgRep, + SqlSugarRepository sysTenantMenuRep, + SqlSugarRepository sysRoleMenuRep, + SqlSugarRepository userRoleRep, + SqlSugarRepository sysTenantRep, + SqlSugarRepository sysUserRep, + SqlSugarRepository sysRoleRep, + SqlSugarRepository sysOrgRep, + SqlSugarRepository sysPosRep, + IOptions uploadOptions, + SysRoleMenuService sysRoleMenuService, + SysConfigService sysConfigService, + SysCacheService sysCacheService) + { + _sysTenantRep = sysTenantRep; + _sysOrgRep = sysOrgRep; + _sysRoleRep = sysRoleRep; + _sysPosRep = sysPosRep; + _sysUserRep = sysUserRep; + _userRoleRep = userRoleRep; + _sysRoleMenuRep = sysRoleMenuRep; + _sysCacheService = sysCacheService; + _uploadOptions = uploadOptions.Value; + _sysConfigService = sysConfigService; + _sysTenantMenuRep = sysTenantMenuRep; + _sysUserExtOrgRep = sysUserExtOrgRep; + _sysRoleMenuService = sysRoleMenuService; + } + + /// + /// 获取租户分页列表 🔖 + /// + /// + /// + [DisplayName("获取租户分页列表")] + public async Task> Page(PageTenantInput input) + { + return await _sysTenantRep.AsQueryable() + .LeftJoin((u, a) => u.UserId == a.Id).ClearFilter() + .LeftJoin((u, a, b) => u.OrgId == b.Id).ClearFilter() + .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), (u, a) => a.Phone.Contains(input.Phone.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), (u, a, b) => b.Name.Contains(input.Name.Trim())) + .OrderBy(u => new { u.OrderNo, u.Id }) + .Select((u, a, b) => new TenantOutput + { + Id = u.Id, + OrgId = b.Id, + Name = b.Name, + UserId = a.Id, + AdminAccount = a.Account, + Phone = a.Phone, + Host = u.Host, + Email = a.Email, + TenantType = u.TenantType, + DbType = u.DbType, + Connection = u.Connection, + ConfigId = u.ConfigId, + OrderNo = u.OrderNo, + Remark = u.Remark, + Status = u.Status, + CreateTime = u.CreateTime, + CreateUserName = u.CreateUserName, + UpdateTime = u.UpdateTime, + UpdateUserName = u.UpdateUserName, + }, true) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取租户列表 + /// + /// + [AllowAnonymous] + [DisplayName("获取租户列表"), HttpGet] + public async Task GetList() + { + return await _sysTenantRep.AsQueryable() + .LeftJoin((u, a) => u.OrgId == a.Id).ClearFilter() + .Where(u => u.Status == StatusEnum.Enable) + .Select((u, a) => new + { + Label = SqlFunc.HasValue(u.Title) ? $"{u.Title}-{a.Name}" : a.Name, + Host = u.Host.ToLower(), + Value = u.Id, + }).ToListAsync(); + } + + /// + /// 获取当前租户系统信息 + /// + /// + [NonAction] + public async Task GetCurrentTenantSysInfo() + { + var tenantId = long.Parse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? "0"); + var host = App.HttpContext.Request.Host.Host.ToLower(); + var tenant = await _sysTenantRep.AsQueryable() + .WhereIF(tenantId > 0, u => u.Id == tenantId && SqlFunc.ToLower(u.Host).Contains(host)) + .WhereIF(!(tenantId > 0), u => SqlFunc.ToLower(u.Host).Contains(host)) + .FirstAsync(); + tenant ??= await _sysTenantRep.GetFirstAsync(u => u.Id == SqlSugarConst.DefaultTenantId); + _ = tenant ?? throw Oops.Oh(ErrorCodeEnum.D1002); + return tenant; + } + + /// + /// 获取库隔离的租户列表 + /// + /// + [NonAction] + public async Task> GetTenantDbList() + { + return await _sysTenantRep.GetListAsync(u => u.TenantType == TenantTypeEnum.Db && u.Status == StatusEnum.Enable); + } + + /// + /// 增加租户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加租户")] + public async Task AddTenant(AddTenantInput input) + { + var isExist = await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1300); + + input.Host = input.Host?.ToLower(); + isExist = await _sysTenantRep.IsAnyAsync(u => !string.IsNullOrWhiteSpace(u.Host) && u.Host == input.Host); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1303); + + isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.AdminAccount); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1301); + + // 从库配置判断 + if (!string.IsNullOrWhiteSpace(input.SlaveConnections) && !JSON.IsValid(input.SlaveConnections)) throw Oops.Oh(ErrorCodeEnum.D1302); + + switch (input.TenantType) + { + // Id隔离时设置与主库一致 + case TenantTypeEnum.Id: + var config = _sysTenantRep.AsSugarClient().CurrentConnectionConfig; + input.DbType = config.DbType; + input.Connection = config.ConnectionString; + break; + + case TenantTypeEnum.Db: + if (string.IsNullOrWhiteSpace(input.Connection)) + throw Oops.Oh(ErrorCodeEnum.Z1004); + break; + + default: + throw Oops.Oh(ErrorCodeEnum.D3004); + } + if (input.EnableReg == YesNoEnum.N) input.RegWayId = null; + var tenant = input.Adapt(); + + // 设置logo + SetLogoUrl(tenant, input.LogoBase64, input.LogoFileName); + + tenant.Id = _sysTenantRep.InsertReturnEntity(tenant).Id; + await InitNewTenant(tenant); + + await CacheTenant(); + } + + /// + /// 设置logo + /// + /// + /// + /// + [NonAction] + public void SetLogoUrl(SysTenant tenant, string logoBase64, string logoFileName) + { + if (string.IsNullOrEmpty(tenant?.Logo) && string.IsNullOrEmpty(tenant?.Logo)) return; + + // 旧图标文件相对路径 + var oldSysLogoRelativeFilePath = tenant.Logo ?? ""; + var oldSysLogoAbsoluteFilePath = Path.Combine(App.WebHostEnvironment.WebRootPath, oldSysLogoRelativeFilePath.TrimStart('/')); + + var groups = Regex.Match(logoBase64, @"data:image/(?.+?);base64,(?.+)").Groups; + + //var type = groups["type"].Value; + var base64Data = groups["data"].Value; + var binData = Convert.FromBase64String(base64Data); + + // 根据文件名取扩展名 + var ext = string.IsNullOrWhiteSpace(logoFileName) ? ".png" : Path.GetExtension(logoFileName); + + // 本地图标保存路径 + var fileName = $"{tenant.ViceTitle}-logo{ext}".ToLower(); + var path = _uploadOptions.Path.Replace("/{yyyy}/{MM}/{dd}", ""); + path = path.StartsWith("/") || Regex.IsMatch(path, "^[A-Z|a-z]:") ? path : Path.Combine(App.WebHostEnvironment.WebRootPath, path); + var absoluteFilePath = Path.Combine(path, fileName); + + // 删除已存在文件 + if (File.Exists(oldSysLogoAbsoluteFilePath)) File.Delete(oldSysLogoAbsoluteFilePath); + + // 创建文件夹 + var absoluteFileDir = Path.GetDirectoryName(absoluteFilePath); + if (!Directory.Exists(absoluteFileDir)) Directory.CreateDirectory(absoluteFileDir); + + // 保存图标文件 + File.WriteAllBytesAsync(absoluteFilePath, binData); + + // 保存图标配置 + tenant.Logo = $"/upload/{fileName}"; + } + + /// + /// 设置租户状态 🔖 + /// + /// + /// + [DisplayName("设置租户状态")] + public async Task SetStatus(TenantInput input) + { + var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.Id); + if (tenant == null || tenant.ConfigId == SqlSugarConst.MainConfigId) throw Oops.Oh(ErrorCodeEnum.Z1001); + + if (!Enum.IsDefined(typeof(StatusEnum), input.Status)) throw Oops.Oh(ErrorCodeEnum.D3005); + + tenant.Status = input.Status; + return await _sysTenantRep.AsUpdateable(tenant).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync(); + } + + /// + /// 新增租户初始化 + /// + /// + private async Task InitNewTenant(TenantOutput tenant) + { + var tenantId = tenant.Id; + var tenantName = tenant.Name; + + // 初始化机构 + var newOrg = new SysOrg { TenantId = tenantId, Pid = 0, Name = tenantName, Code = tenantName, Remark = tenantName, }; + await _sysOrgRep.InsertAsync(newOrg); + + // 初始化默认角色 + var newRole = new SysRole { TenantId = tenantId, Name = CommonConst.DefaultBaseRoleName, Code = CommonConst.DefaultBaseRoleCode, DataScope = DataScopeEnum.Self, Remark = "此角色为系统自动创建角色" }; + var baseRole = await _sysRoleRep.InsertReturnEntityAsync(newRole); + var baseRoleMenuIdList = GetBaseRoleMenuIdList().ToList(); + await _sysRoleMenuService.GrantRoleMenu(new RoleMenuInput { Id = baseRole.Id, MenuIdList = baseRoleMenuIdList.Select(u => u.MenuId).ToList() }); + + // 初始化职位 + var newPos = new SysPos { TenantId = tenantId, Name = "管理员-" + tenantName, Code = tenantName, Remark = tenantName }; + await _sysPosRep.InsertAsync(newPos); + + // 初始化租户管理员账号 + var password = await _sysConfigService.GetConfigValue(ConfigConst.SysPassword); + var newUser = new SysUser + { + TenantId = tenantId, + Account = tenant.AdminAccount, + Password = CryptogramUtil.Encrypt(password), + NickName = "系统管理员", + Email = tenant.Email, + Phone = tenant.Phone, + AccountType = AccountTypeEnum.SysAdmin, + OrgId = newOrg.Id, + PosId = newPos.Id, + Birthday = DateTime.Parse("2000-01-01"), + RealName = "系统管理员", + Remark = "系统管理员" + tenantName, + }; + await _sysUserRep.InsertAsync(newUser); + + // 关联租户组织机构和管理员用户 + await _sysTenantRep.UpdateAsync(u => new SysTenant { UserId = newUser.Id, OrgId = newOrg.Id }, u => u.Id == tenantId); + + // 默认租户管理员角色菜单集合 + var menuList = GetTenantDefaultMenuList().ToList(); + await GrantMenu(new TenantMenuInput { Id = tenantId, MenuIdList = menuList.Select(u => u.MenuId).ToList() }); + } + + /// + /// 获取租户默认菜单 + /// + /// 如果某租户需要定制主页,可以忽略 + /// + [NonAction] + public IEnumerable GetTenantDefaultMenuList(bool ignoreHome = false) + { + var menuList = new List(); + + // 默认数据库配置 + var defaultConfig = App.GetOptions().ConnectionConfigs.FirstOrDefault(); + //从程序集中获取种子菜单数据,种子菜单存在于其他类库中,需要动态加载 + var menuSeedDataTypeList = GetSeedDataTypes(defaultConfig, nameof(SysMenuSeedData)); + var allMenuList = new List(); + foreach (var menu in menuSeedDataTypeList) + { + var menuSeedDataList = ((IEnumerable)menu.GetMethod("HasData")?.Invoke(Activator.CreateInstance(menu), null))?.Cast(); + if (menuSeedDataList != null) + { + allMenuList.AddRange(menuSeedDataList); + } + } + + //实现三个层级的菜单 + var topMenuList = allMenuList.Where(u => u.Pid == 0 && u.Type == MenuTypeEnum.Dir).ToList(); + menuList.AddRange(topMenuList); + + var childMenuList = allMenuList.ToChildList(u => u.Id, u => u.Pid, u => topMenuList.Select(p => p.Id).Contains(u.Pid)); + menuList.AddRange(childMenuList); + + var endMenuList = allMenuList.ToChildList(u => u.Id, u => u.Pid, u => childMenuList.Select(p => p.Id).Contains(u.Pid)); + if (endMenuList != null) + { + menuList.AddRange(endMenuList); + } + //是否需要排除首页菜单 + if (ignoreHome) menuList = menuList.Where(u => !(u.Type == MenuTypeEnum.Menu && u.Name == "home")).ToList(); + + menuList = menuList.Distinct().ToList(); + + return menuList.Select(u => new SysTenantMenu + { + Id = CommonUtil.GetFixedHashCode("" + SqlSugarConst.DefaultTenantId + u.Id, 1300000000000), + TenantId = SqlSugarConst.DefaultTenantId, + MenuId = u.Id + }); + } + + /// + /// 获取种子数据类型 + /// + /// 数据库连接配置 + /// + /// 种子数据类型列表 + [NonAction] + private List GetSeedDataTypes(DbConnectionConfig config, string typeName) + { + return App.EffectiveTypes + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.Name == typeName && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>)))) + .WhereIF(config.SeedSettings.EnableIncreSeed, u => u.IsDefined(typeof(IncreSeedAttribute), false)) + .OrderBy(u => u.GetCustomAttributes(typeof(SeedDataAttribute), false).Length > 0 ? ((SeedDataAttribute)u.GetCustomAttributes(typeof(SeedDataAttribute), false)[0]).Order : 0) + .ToList(); + } + + /// + /// 获取租户默认菜单 + /// + /// + [NonAction] + public IEnumerable GetBaseRoleMenuIdList() + { + var menuList = new List(); + var allMenuList = new SysMenuSeedData().HasData().ToList(); + + var dashboardMenu = allMenuList.First(u => u.Type == MenuTypeEnum.Dir && u.Title == "工作台"); + menuList.AddRange(allMenuList.ToChildList(u => u.Id, u => u.Pid, dashboardMenu.Id)); + + var systemMenu = allMenuList.First(u => u.Type == MenuTypeEnum.Dir && u.Title == "系统管理"); + menuList.Add(systemMenu); + menuList.AddRange(allMenuList.ToChildList(u => u.Id, u => u.Pid, u => u.Pid == systemMenu.Id && new[] { "机构管理", "个人中心" }.Contains(u.Title))); + menuList = menuList.Where(u => !new[] { "增加", "编辑", "删除" }.Contains(u.Title)).ToList(); + + return menuList.Select(u => new SysTenantMenu + { + Id = CommonUtil.GetFixedHashCode("" + SqlSugarConst.DefaultTenantId + u.Id, 1300000000000), + TenantId = SqlSugarConst.DefaultTenantId, + MenuId = u.Id + }); + } + + /// + /// 删除租户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除租户")] + public async Task DeleteTenant(DeleteTenantInput input) + { + // 禁止删除默认租户 + if (input.Id.ToString() == SqlSugarConst.MainConfigId) throw Oops.Oh(ErrorCodeEnum.D1023); + + // 若账号为开放接口绑定租户则禁止删除 + var isOpenAccessTenant = await _sysTenantRep.ChangeRepository>().IsAnyAsync(u => u.BindTenantId == input.Id); + if (isOpenAccessTenant) throw Oops.Oh(ErrorCodeEnum.D1031); + + await _sysTenantRep.DeleteAsync(u => u.Id == input.Id); + + await CacheTenant(input.Id); + + // 删除与租户相关的表数据 + await _sysTenantMenuRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + await _sysTenantRep.Context.Deleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + + var users = await _sysUserRep.AsQueryable().ClearFilter().Where(u => u.TenantId == input.Id).ToListAsync(); + var userIds = users.Select(u => u.Id).ToList(); + await _sysUserRep.AsDeleteable().Where(u => userIds.Contains(u.Id)).ExecuteCommandAsync(); + await _userRoleRep.AsDeleteable().Where(u => userIds.Contains(u.UserId)).ExecuteCommandAsync(); + await _sysUserExtOrgRep.AsDeleteable().Where(u => userIds.Contains(u.UserId)).ExecuteCommandAsync(); + await _sysTenantRep.Context.Deleteable().Where(u => userIds.Contains(u.UserId)).ExecuteCommandAsync(); + await _sysTenantRep.Context.Deleteable().Where(u => userIds.Contains(u.UserId)).ExecuteCommandAsync(); + + var roleIds = await _sysRoleRep.AsQueryable().ClearFilter().Where(u => u.TenantId == input.Id).Select(u => u.Id).ToListAsync(); + await _sysRoleRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + await _sysRoleMenuRep.AsDeleteable().Where(u => roleIds.Contains(u.RoleId)).ExecuteCommandAsync(); + await _sysTenantRep.Context.Deleteable().Where(u => roleIds.Contains(u.RoleId)).ExecuteCommandAsync(); + + await _sysOrgRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + + await _sysPosRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + } + + /// + /// 更新租户 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新租户")] + public async Task UpdateTenant(UpdateTenantInput input) + { + var isExist = await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.OrgId); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1300); + + input.Host = input.Host?.ToLower(); + isExist = await _sysTenantRep.IsAnyAsync(u => !string.IsNullOrWhiteSpace(u.Host) && u.Host == input.Host && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1303); + + isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.AdminAccount && u.Id != input.UserId); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1301); + + // Id隔离时设置与主库一致 + switch (input.TenantType) + { + case TenantTypeEnum.Id: + var config = _sysTenantRep.AsSugarClient().CurrentConnectionConfig; + input.DbType = config.DbType; + input.Connection = config.ConnectionString; + break; + + case TenantTypeEnum.Db: + if (string.IsNullOrWhiteSpace(input.Connection)) + throw Oops.Oh(ErrorCodeEnum.Z1004); + break; + + default: + throw Oops.Oh(ErrorCodeEnum.D3004); + } + // 从库配置判断 + if (!string.IsNullOrWhiteSpace(input.SlaveConnections) && !JSON.IsValid(input.SlaveConnections)) + throw Oops.Oh(ErrorCodeEnum.D1302); + + // 设置logo + var tenant = input.Adapt(); + if (!string.IsNullOrWhiteSpace(input.LogoBase64)) SetLogoUrl(tenant, input.LogoBase64, input.LogoFileName); + + // 更新租户信息 + await _sysTenantRep.AsUpdateable(tenant).IgnoreColumns(true).ExecuteCommandAsync(); + + // 更新系统机构 + await _sysOrgRep.UpdateAsync(u => new SysOrg() { Name = input.Name }, u => u.Id == input.OrgId); + + // 更新系统用户 + await _sysUserRep.UpdateAsync(u => new SysUser() { Account = input.AdminAccount, Phone = input.Phone, Email = input.Email }, u => u.Id == input.UserId); + + await CacheTenant(input.Id); + } + + /// + /// 授权租户菜单 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权租户菜单")] + public async Task GrantMenu(TenantMenuInput input) + { + // 获取需要授权的菜单列表 + var menuList = await _sysTenantRep.Context.Queryable() + .Where(u => input.MenuIdList.Contains(u.Id)) + .InnerJoin((u, t) => t.TenantId == input.Id && u.Id == t.MenuId) + .ToListAsync(); + + // 检查是否存在重复菜单 + if (menuList.Where(u => u.Type != MenuTypeEnum.Btn).GroupBy(u => new { u.Pid, u.Title }).Any(u => u.Count() > 1) || + menuList.Where(u => u.Type == MenuTypeEnum.Btn).GroupBy(u => u.Permission).Any(u => u.Count() > 1)) + throw Oops.Oh(ErrorCodeEnum.D1304); + + // 检查路由是否重复 + if (menuList.Where(u => !string.IsNullOrWhiteSpace(u.Name)).GroupBy(u => u.Name).Any(u => u.Count() > 1)) + throw Oops.Oh(ErrorCodeEnum.D4009); + + //获取旧记录数据 原ID不能改变 种子初始化后 数据重复 + var tenantMenuList = await _sysTenantMenuRep.AsQueryable().Where(u => u.TenantId == input.Id).ToListAsync(); + + // 删除旧记录 + await _sysTenantMenuRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + + // 追加父级菜单 + var allIdList = await _sysTenantRep.Context.Queryable().Select(u => new { u.Id, u.Pid }).ToListAsync(); + var pIdList = allIdList.ToChildList(u => u.Pid, u => u.Id, u => input.MenuIdList.Contains(u.Id)).Select(u => u.Pid).Distinct().ToList(); + input.MenuIdList = input.MenuIdList.Concat(pIdList).Distinct().Where(u => u != 0).ToList(); + + // 保存租户菜单 + var sysTenantMenuList = input.MenuIdList.Select(menuId => new SysTenantMenu { TenantId = input.Id, MenuId = menuId }).ToList(); + + //原ID不变 + foreach (var item in sysTenantMenuList) + { + var tenantMenu = tenantMenuList.FirstOrDefault(u => u.TenantId == item.TenantId && u.MenuId == item.MenuId); + if (tenantMenu != null) item.Id = tenantMenu.Id; + } + + await _sysTenantMenuRep.InsertRangeAsync(sysTenantMenuList); + + // 清除菜单权限缓存 + SysMenuService.DeleteMenuCache(); + } + + /// + /// 获取租户菜单Id集合 🔖 + /// + /// + /// + [DisplayName("获取租户菜单Id集合")] + public async Task> GetTenantMenuList([FromQuery] BaseIdInput input) + { + var menuIds = await _sysTenantMenuRep.AsQueryable().Where(u => u.TenantId == input.Id).Select(u => u.MenuId).ToListAsync(); + return await SysMenuService.ExcludeParentMenuOfFullySelected(menuIds); + } + + /// + /// 重置租户管理员密码 🔖 + /// + /// + /// + [DisplayName("重置租户管理员密码")] + public async Task ResetPwd(TenantUserInput input) + { + var password = await _sysConfigService.GetConfigValue(ConfigConst.SysPassword); + var encryptPassword = CryptogramUtil.Encrypt(password); + await _sysUserRep.UpdateAsync(u => new SysUser { Password = encryptPassword }, u => u.Id == input.UserId); + return password; + } + + /// + /// 切换租户 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("切换租户")] + public async Task ChangeTenant(BaseIdInput input) + { + var userId = (App.HttpContext?.User.FindFirst(ClaimConst.UserId)?.Value)?.ToLong(); + _ = await _sysTenantRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + var user = await _sysUserRep.GetFirstAsync(u => u.Id == userId) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + user.TenantId = input.Id; + + return await GetAccessTokenInNotSingleLogin(user); + } + + /// + /// 进入租管端 🔖 + /// + /// + /// + [DisplayName("进入租管端")] + public async Task GoTenant(BaseIdInput input) + { + var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + var user = await _sysUserRep.GetFirstAsync(u => u.Id == tenant.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + return await GetAccessTokenInNotSingleLogin(user); + } + + /// + /// 同步授权菜单(用于版本更新后,同步授权数据) 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("同步授权菜单")] + public async Task SyncGrantMenu(BaseIdInput input) + { + var menuIdList = input.Id == SqlSugarConst.DefaultTenantId + ? new SysMenuSeedData().HasData().Select(u => u.Id).ToList() + : await _sysRoleRep.AsQueryable().ClearFilter() + .InnerJoin((u, t) => t.Id == input.Id && u.TenantId == t.Id) + .InnerJoin((u, t, rm) => u.Id == rm.RoleId) + .Select((u, t, rm) => rm.MenuId) + .Distinct() + .ToListAsync() ?? throw Oops.Oh(ErrorCodeEnum.D1019); + var adminRole = await _sysRoleRep.AsQueryable().ClearFilter().FirstAsync(u => u.TenantId == input.Id && u.Code == "sys_admin"); + if (adminRole != null) + { + await _sysRoleRep.Context.Deleteable().Where(u => u.RoleId == adminRole.Id).ExecuteCommandAsync(); + await App.GetService().DeleteRole(new DeleteRoleInput { Id = adminRole.Id }); + } + await GrantMenu(new TenantMenuInput { Id = input.Id, MenuIdList = menuIdList }); + } + + /// + /// 在非单用户登录模式下获取登录令牌 + /// + /// + /// + [NonAction] + public async Task GetAccessTokenInNotSingleLogin(SysUser user) + { + // 使用非单用户模式登录 + var singleLogin = _sysCacheService.Get($"{CacheConst.KeyConfig}{ConfigConst.SysSingleLogin}"); + try + { + _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysSingleLogin}", false); + return await App.GetService().CreateToken(user); + } + finally + { + // 恢复单用户登录参数 + if (singleLogin) _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysSingleLogin}", true); + } + } + + /// + /// 缓存所有租户 + /// + /// + /// + [NonAction] + public async Task CacheTenant(long tenantId = 0) + { + // 移除 ISqlSugarClient 中的库连接并排除默认主库 + if (tenantId > 0 && tenantId.ToString() != SqlSugarConst.MainConfigId) + _sysTenantRep.AsTenant().RemoveConnection(tenantId); + + var tenantList = await _sysTenantRep.GetListAsync(); + + // 对租户库连接进行SM2加密 + foreach (var tenant in tenantList.Where(tenant => !string.IsNullOrWhiteSpace(tenant.Connection))) + tenant.Connection = CryptogramUtil.SM2Encrypt(tenant.Connection); + + _sysCacheService.Set(CacheConst.KeyTenant, tenantList); + } + + /// + /// 创建租户数据库 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "CreateDb"), HttpPost] + [DisplayName("创建租户数据库")] + public async Task CreateDb(TenantInput input) + { + var tenant = await _sysTenantRep.GetSingleAsync(u => u.Id == input.Id); + if (tenant == null) return; + + if (tenant.DbType == SqlSugar.DbType.Oracle) + throw Oops.Oh(ErrorCodeEnum.Z1002); + + if (string.IsNullOrWhiteSpace(tenant.Connection) || tenant.Connection.Length < 10) + throw Oops.Oh(ErrorCodeEnum.Z1004); + + // 默认数据库配置 + var defaultConfig = App.GetOptions().ConnectionConfigs.FirstOrDefault(); + + var config = new DbConnectionConfig + { + ConfigId = tenant.Id.ToString(), + DbType = tenant.DbType, + ConnectionString = tenant.Connection, + DbSettings = new DbSettings() + { + EnableInitDb = true, + EnableDiffLog = false, + EnableUnderLine = defaultConfig!.DbSettings.EnableUnderLine, + } + }; + SqlSugarSetup.InitTenantDatabase(App.GetRequiredService().AsTenant(), config); + } + + /// + /// 获取租户下的用户列表 🔖 + /// + /// + /// + [DisplayName("获取租户下的用户列表")] + public async Task> UserList(TenantIdInput input) + { + return await _sysUserRep.AsQueryable().ClearFilter().Where(u => u.TenantId == input.TenantId).ToListAsync(); + } + + /// + /// 获取租户数据库连接 + /// + /// + [NonAction] + public SqlSugarScopeProvider GetTenantDbConnectionScope(long tenantId) + { + var iTenant = _sysTenantRep.AsTenant(); + + // 若已存在租户库连接,则直接返回 + if (iTenant.IsAnyConnection(tenantId.ToString())) return iTenant.GetConnectionScope(tenantId.ToString()); + + lock (iTenant) + { + // 从缓存里面获取租户信息 + var tenant = _sysCacheService.Get>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == tenantId); + if (tenant == null || tenant.TenantType == TenantTypeEnum.Id) return null; + + // 获取默认库连接配置 + var dbOptions = App.GetOptions(); + var mainConnConfig = dbOptions.ConnectionConfigs.First(u => u.ConfigId.ToString() == SqlSugarConst.MainConfigId); + + // 设置租户库连接配置 + var tenantConnConfig = new DbConnectionConfig + { + ConfigId = tenant.Id.ToString(), + DbType = tenant.DbType, + TenantType = tenant.TenantType, + IsAutoCloseConnection = true, + ConnectionString = CryptogramUtil.SM2Decrypt(tenant.Connection), // 对租户库连接进行SM2解密 + DbSettings = new DbSettings() + { + EnableUnderLine = mainConnConfig.DbSettings.EnableUnderLine, + }, + SlaveConnectionConfigs = JSON.IsValid(tenant.SlaveConnections) ? JSON.Deserialize>(tenant.SlaveConnections) : null // 从库连接配置 + }; + iTenant.AddConnection(tenantConnConfig); + + var sqlSugarScopeProvider = iTenant.GetConnectionScope(tenantId.ToString()); + SqlSugarSetup.SetDbConfig(tenantConnConfig); + SqlSugarSetup.SetDbAop(sqlSugarScopeProvider, dbOptions.EnableConsoleSql, dbOptions.SuperAdminIgnoreIDeletedFilter); + + return sqlSugarScopeProvider; + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Update/Dto/UpdateInput.cs b/Admin.NET.Core/Service/Update/Dto/UpdateInput.cs new file mode 100644 index 0000000..5c3e870 --- /dev/null +++ b/Admin.NET.Core/Service/Update/Dto/UpdateInput.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 还原输入参数 +/// +public class RestoreInput +{ + /// + /// 文件名 + /// + [Required(ErrorMessage = "文件名不能为空")] + public string FileName { get; set; } +} + +/// +/// WebHook输入参数 +/// +public class WebHookInput +{ + /// + /// 密钥 + /// + [Required(ErrorMessage = "密钥不能为空")] + public string Key { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Update/Dto/UpdateOutput.cs b/Admin.NET.Core/Service/Update/Dto/UpdateOutput.cs new file mode 100644 index 0000000..1a9d236 --- /dev/null +++ b/Admin.NET.Core/Service/Update/Dto/UpdateOutput.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class BackupOutput +{ + /// + /// 文件路径 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string FilePath { get; set; } + + /// + /// 文件名 + /// + public string FileName { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreateTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Update/SysUpdateService.cs b/Admin.NET.Core/Service/Update/SysUpdateService.cs new file mode 100644 index 0000000..ad7a595 --- /dev/null +++ b/Admin.NET.Core/Service/Update/SysUpdateService.cs @@ -0,0 +1,418 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.IO.Compression; +using System.Net; +using System.Security.Cryptography; + +namespace Admin.NET.Core.Service; + +/// +/// 系统更新管理服务 🧩 +/// +[ApiDescriptionSettings(Order = 390)] +public class SysUpdateService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly CDConfigOptions _cdConfigOptions; + + public SysUpdateService(IOptions giteeOptions, SysCacheService sysCacheService) + { + _cdConfigOptions = giteeOptions.Value; + _sysCacheService = sysCacheService; + } + + /// + /// 备份列表 + /// + /// + [DisplayName("备份列表")] + [ApiDescriptionSettings(Name = "List"), HttpPost] + public Task> List() + { + const string backendDir = "Admin.NET"; + var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, "..")); + return Task.FromResult(Directory.GetFiles(rootPath, backendDir + "*.zip", SearchOption.TopDirectoryOnly) + .Select(filePath => + { + var file = new FileInfo(filePath); + return new BackupOutput + { + CreateTime = file.CreationTime, + FilePath = filePath, + FileName = file.Name + }; + }) + .OrderByDescending(u => u.CreateTime) + .ToList()); + } + + /// + /// 还原 + /// + /// + [DisplayName("还原")] + [ApiDescriptionSettings(Name = "Restore"), HttpPost] + public async Task Restore(RestoreInput input) + { + // 检查参数 + CheckConfig(); + try + { + var file = (await List()).FirstOrDefault(u => u.FileName.EqualIgnoreCase(input.FileName)); + if (file == null) + { + PrintfLog("文件不存在..."); + return; + } + + PrintfLog("正在还原..."); + using ZipArchive archive = new(File.OpenRead(file.FilePath), ZipArchiveMode.Read, leaveOpen: false); + archive.ExtractToDirectory(_cdConfigOptions.BackendOutput, true); + PrintfLog("还原成功..."); + } + catch (Exception ex) + { + PrintfLog("发生异常:" + ex.Message); + throw; + } + } + + /// + /// 从远端更新系统 + /// + /// + [DisplayName("系统更新")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update() + { + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"【{DateTime.Now}】从远端仓库部署项目"); + try + { + PrintfLog("----------------------------从远端仓库部署项目-开始----------------------------"); + + // 检查参数 + CheckConfig(); + + // 检查操作间隔 + if (_cdConfigOptions.UpdateInterval > 0) + { + if (_sysCacheService.Get(CacheConst.KeySysUpdateInterval)) throw Oops.Oh("请勿频繁操作"); + _sysCacheService.Set(CacheConst.KeySysUpdateInterval, true, TimeSpan.FromMinutes(_cdConfigOptions.UpdateInterval)); + } + + PrintfLog($"客户端host:{App.HttpContext.Request.Host}"); + PrintfLog($"客户端IP:{App.HttpContext.GetRemoteIpAddressToIPv4(true)}"); + PrintfLog($"仓库地址:https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git"); + PrintfLog($"仓库分支:{_cdConfigOptions.Branch}"); + + // 获取解压后的根目录 + var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, "..")); + var tempDir = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}-{_cdConfigOptions.Branch}"); + + PrintfLog("清理旧文件..."); + FileHelper.TryDelete(tempDir); + + PrintfLog("拉取远端代码..."); + var stream = await GiteeHelper.DownloadRepoZip(_cdConfigOptions.Owner, _cdConfigOptions.Repo, + _cdConfigOptions.AccessToken, _cdConfigOptions.Branch); + + PrintfLog("文件包解压..."); + using ZipArchive archive = new(stream, ZipArchiveMode.Read, leaveOpen: false); + archive.ExtractToDirectory(rootPath); + + // 项目目录 + var backendDir = "Admin.NET"; // 后端根目录 + var entryProjectName = "Evergreen.Health.Api"; // 启动项目目录 + var tempOutput = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_temp"); + + PrintfLog("编译项目..."); + PrintfLog($"发布版本:{_cdConfigOptions.Publish.Configuration}"); + PrintfLog($"目标框架:{_cdConfigOptions.Publish.TargetFramework}"); + PrintfLog($"运行环境:{_cdConfigOptions.Publish.RuntimeIdentifier}"); + var option = _cdConfigOptions.Publish; + var adminNetDir = Path.Combine(tempDir, backendDir); + var args = $"publish \"{entryProjectName}\" -c {option.Configuration} -f {option.TargetFramework} -r {option.RuntimeIdentifier} --output \"{tempOutput}\""; + await RunCommandAsync("dotnet", args, adminNetDir); + + PrintfLog("复制 wwwroot 目录..."); + var wwwrootDir = Path.Combine(adminNetDir, entryProjectName, "wwwroot"); + FileHelper.CopyDirectory(wwwrootDir, Path.Combine(tempOutput, "wwwroot"), true); + + // 删除排除文件 + foreach (var filePath in (_cdConfigOptions.ExcludeFiles ?? new()).SelectMany(file => Directory.GetFiles(tempOutput, file, SearchOption.TopDirectoryOnly))) + { + PrintfLog($"排除文件:{filePath}"); + FileHelper.TryDelete(filePath); + } + + PrintfLog("备份原项目文件..."); + string backupPath = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_{DateTime.Now:yyyy_MM_dd}.zip"); + if (File.Exists(backupPath)) File.Delete(backupPath); + ZipFile.CreateFromDirectory(_cdConfigOptions.BackendOutput, backupPath); + + // 将临时文件移动到正式目录 + FileHelper.CopyDirectory(tempOutput, _cdConfigOptions.BackendOutput, true); + + PrintfLog("清理文件..."); + FileHelper.TryDelete(tempOutput); + FileHelper.TryDelete(tempDir); + + if (_cdConfigOptions.BackupCount > 0) + { + var fileList = await List(); + if (fileList.Count > _cdConfigOptions.BackupCount) + PrintfLog("清除多余的备份文件..."); + while (fileList.Count > _cdConfigOptions.BackupCount) + { + var last = fileList.Last(); + FileHelper.TryDelete(last.FilePath); + fileList.Remove(last); + } + } + + PrintfLog("重启项目后生效..."); + } + catch (Exception ex) + { + PrintfLog("发生异常:" + ex.Message); + throw; + } + finally + { + PrintfLog("----------------------------从远端仓库部署项目-结束----------------------------"); + Console.ForegroundColor = originColor; + } + } + + /// + /// 仓库WebHook接口 + /// + /// + [AllowAnonymous] + [DisplayName("仓库WebHook接口")] + [ApiDescriptionSettings(Name = "WebHook"), HttpPost] + public async Task WebHook(Dictionary input) + { + if (!_cdConfigOptions.Enabled) throw Oops.Oh("未启用持续部署功能"); + PrintfLog("----------------------------收到WebHook请求-开始----------------------------"); + + try + { + // 获取请求头信息 + var even = App.HttpContext.Request.Headers.FirstOrDefault(u => u.Key == "X-Gitee-Event").Value + .FirstOrDefault(); + var ua = App.HttpContext.Request.Headers.FirstOrDefault(u => u.Key == "User-Agent").Value.FirstOrDefault(); + + var timestamp = input.GetValueOrDefault("timestamp")?.ToString(); + var token = input.GetValueOrDefault("sign")?.ToString(); + PrintfLog("User-Agent:" + ua); + PrintfLog("Gitee-Event:" + even); + PrintfLog("Gitee-Token:" + token); + PrintfLog("Gitee-Timestamp:" + timestamp); + + PrintfLog("开始验签..."); + var secret = GetWebHookKey(); + var stringToSign = $"{timestamp}\n{secret}"; + using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + var signData = mac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); + var encodedSignData = Convert.ToBase64String(signData); + var calculatedSignature = WebUtility.UrlEncode(encodedSignData); + + if (calculatedSignature != token) throw Oops.Oh("非法签名"); + PrintfLog("验签成功..."); + + var hookName = input.GetValueOrDefault("hook_name") as string; + PrintfLog("Hook-Name:" + hookName); + + switch (hookName) + { + // 提交修改 + case "push_hooks": + { + var commitList = input.GetValueOrDefault("commits")?.Adapt>>() ?? new(); + foreach (var commit in commitList) + { + var author = commit.GetValueOrDefault("author")?.Adapt>(); + PrintfLog("Commit-Message:" + commit.GetValueOrDefault("message")); + PrintfLog("Commit-Time:" + commit.GetValueOrDefault("timestamp")); + PrintfLog("Commit-Author:" + author?.GetValueOrDefault("username")); + PrintfLog("Modified-List:" + author?.GetValueOrDefault("modified")?.Adapt>().Join()); + PrintfLog("----------------------------------------------------------"); + } + + break; + } + // 合并 Pull Request + case "merge_request_hooks": + { + var pull = input.GetValueOrDefault("pull_request")?.Adapt>(); + var user = pull?.GetValueOrDefault("user")?.Adapt>(); + PrintfLog("Pull-Request-Title:" + pull?.GetValueOrDefault("message")); + PrintfLog("Pull-Request-Time:" + pull?.GetValueOrDefault("created_at")); + PrintfLog("Pull-Request-Author:" + user?.GetValueOrDefault("username")); + PrintfLog("Pull-Request-Body:" + pull?.GetValueOrDefault("body")); + break; + } + // 新的issue + case "issue_hooks": + { + var issue = input.GetValueOrDefault("issue")?.Adapt>(); + var user = issue?.GetValueOrDefault("user")?.Adapt>(); + var labelList = issue?.GetValueOrDefault("labels")?.Adapt>>(); + PrintfLog("Issue-UserName:" + user?.GetValueOrDefault("username")); + PrintfLog("Issue-Labels:" + labelList?.Select(u => u.GetValueOrDefault("name")).Join()); + PrintfLog("Issue-Title:" + issue?.GetValueOrDefault("title")); + PrintfLog("Issue-Time:" + issue?.GetValueOrDefault("created_at")); + PrintfLog("Issue-Body:" + issue?.GetValueOrDefault("body")); + return; + } + // 评论 + case "note_hooks": + { + var comment = input.GetValueOrDefault("comment")?.Adapt>(); + var user = input.GetValueOrDefault("user")?.Adapt>(); + PrintfLog("comment-UserName:" + user?.GetValueOrDefault("username")); + PrintfLog("comment-Time:" + comment?.GetValueOrDefault("created_at")); + PrintfLog("comment-Content:" + comment?.GetValueOrDefault("body")); + return; + } + default: + return; + } + + var updateInterval = _cdConfigOptions.UpdateInterval; + try + { + _cdConfigOptions.UpdateInterval = 0; + await Update(); + } + finally + { + _cdConfigOptions.UpdateInterval = updateInterval; + } + } + finally + { + PrintfLog("----------------------------收到WebHook请求-结束----------------------------"); + } + } + + /// + /// 获取WebHook接口密钥 + /// + /// + [DisplayName("获取WebHook接口密钥")] + [ApiDescriptionSettings(Name = "WebHookKey"), HttpGet] + public string GetWebHookKey() + { + return CryptogramUtil.Encrypt(_cdConfigOptions.AccessToken); + } + + /// + /// 获取日志列表 + /// + /// + [DisplayName("获取日志列表")] + [ApiDescriptionSettings(Name = "Logs"), HttpGet] + public List LogList() + { + return _sysCacheService.Get>(CacheConst.KeySysUpdateLog) ?? new(); + } + + /// + /// 清空日志 + /// + /// + [DisplayName("清空日志")] + [ApiDescriptionSettings(Name = "Clear"), HttpGet] + public void ClearLog() + { + _sysCacheService.Remove(CacheConst.KeySysUpdateLog); + } + + /// + /// 检查参数 + /// + /// + private void CheckConfig() + { + PrintfLog("检查CD配置参数..."); + + if (_cdConfigOptions == null) throw Oops.Oh("CDConfig配置不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Owner)) throw Oops.Oh("仓库用户名不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Repo)) throw Oops.Oh("仓库名不能为空"); + + // if (string.IsNullOrWhiteSpace(_cdConfigOptions.Branch)) throw Oops.Oh("分支名不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.AccessToken)) throw Oops.Oh("授权信息不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.BackendOutput)) throw Oops.Oh("部署目录不能为空"); + + if (_cdConfigOptions.Publish == null) throw Oops.Oh("编译配置不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.Configuration)) throw Oops.Oh("运行环境编译配置不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.TargetFramework)) throw Oops.Oh(".NET版本编译配置不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.RuntimeIdentifier)) throw Oops.Oh("运行平台配置不能为空"); + } + + /// + /// 打印日志 + /// + /// + private void PrintfLog(string message) + { + var logList = _sysCacheService.Get>(CacheConst.KeySysUpdateLog) ?? new(); + + var content = $"【{DateTime.Now}】 {message}"; + + Console.WriteLine(content); + + logList.Add(content); + + _sysCacheService.Set(CacheConst.KeySysUpdateLog, logList); + } + + /// + /// 执行命令 + /// + /// 命令 + /// 参数 + /// 工作目录 + private async Task RunCommandAsync(string command, string arguments, string workingDirectory) + { + var processStartInfo = new ProcessStartInfo + { + FileName = command, + Arguments = arguments, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = new Process(); + process.StartInfo = processStartInfo; + process.Start(); + + while (!process.StandardOutput.EndOfStream) + { + string line = await process.StandardOutput.ReadLineAsync(); + if (string.IsNullOrEmpty(line)) continue; + PrintfLog(line.Trim()); + } + await process.WaitForExitAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/Dto/UserExtOrgInput.cs b/Admin.NET.Core/Service/User/Dto/UserExtOrgInput.cs new file mode 100644 index 0000000..1a5f906 --- /dev/null +++ b/Admin.NET.Core/Service/User/Dto/UserExtOrgInput.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class UserExtOrgInput : BaseIdInput +{ + /// + /// 机构Id + /// + public long OrgId { get; set; } + + /// + /// 职位Id + /// + public long PosId { get; set; } + + /// + /// 工号 + /// + public string JobNum { get; set; } + + /// + /// 职级 + /// + public string PosLevel { get; set; } + + /// + /// 入职日期 + /// + public DateTime? JoinDate { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/Dto/UserInput.cs b/Admin.NET.Core/Service/User/Dto/UserInput.cs new file mode 100644 index 0000000..2516f35 --- /dev/null +++ b/Admin.NET.Core/Service/User/Dto/UserInput.cs @@ -0,0 +1,133 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 设置用户状态输入参数 +/// +public class UserInput : BaseStatusInput +{ +} + +/// +/// 获取用户分页列表输入参数 +/// +public class PageUserInput : BasePageInput +{ + /// + /// 租户Id + /// + public long TenantId { get; set; } + + /// + /// 账号 + /// + public string Account { get; set; } + + /// + /// 姓名 + /// + public string RealName { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } + + /// + /// 手机号 + /// + public string Phone { get; set; } + + /// + /// 查询时所选机构Id + /// + public long OrgId { get; set; } +} + +/// +/// 增加用户输入参数 +/// +public class AddUserInput : SysUser +{ + /// + /// 账号 + /// + [Required(ErrorMessage = "账号不能为空")] + public override string Account { get; set; } + + /// + /// 真实姓名 + /// + [Required(ErrorMessage = "真实姓名不能为空")] + public override string RealName { get; set; } + + /// + /// 域用户 + /// + public string DomainAccount { get; set; } + + /// + /// 角色集合 + /// + public List RoleIdList { get; set; } + + /// + /// 扩展机构集合 + /// + public List ExtOrgIdList { get; set; } +} + +/// +/// 更新用户输入参数 +/// +public class UpdateUserInput : AddUserInput +{ +} + +/// +/// 删除用户输入参数 +/// +public class DeleteUserInput : BaseIdInput +{ + /// + /// 机构Id + /// + public long OrgId { get; set; } +} + +/// +/// 重置用户密码输入参数 +/// +public class ResetPwdUserInput : BaseIdInput +{ +} + +/// +/// 修改用户密码输入参数 +/// +public class ChangePwdInput +{ + /// + /// 当前密码 + /// + [Required(ErrorMessage = "当前密码不能为空")] + public string PasswordOld { get; set; } + + /// + /// 新密码 + /// + [Required(ErrorMessage = "新密码不能为空"), MinLength(5, ErrorMessage = "密码需要大于5个字符")] + public string PasswordNew { get; set; } +} + +/// +/// 解除登录锁定输入参数 +/// +public class UnlockLoginInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/Dto/UserMenuInput.cs b/Admin.NET.Core/Service/User/Dto/UserMenuInput.cs new file mode 100644 index 0000000..ef388c4 --- /dev/null +++ b/Admin.NET.Core/Service/User/Dto/UserMenuInput.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户菜单快捷导航输入 +/// +public class UserMenuInput +{ + /// + /// 收藏菜单Id集合 + /// + public List MenuIdList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/Dto/UserOutput.cs b/Admin.NET.Core/Service/User/Dto/UserOutput.cs new file mode 100644 index 0000000..4c508f8 --- /dev/null +++ b/Admin.NET.Core/Service/User/Dto/UserOutput.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class UserOutput : SysUser +{ + /// + /// 机构名称 + /// + public string OrgName { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } + + /// + /// 角色名称 + /// + public string RoleName { get; set; } + + /// + /// 域用户 + /// + public string DomainAccount { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/Dto/UserRegWayInput.cs b/Admin.NET.Core/Service/User/Dto/UserRegWayInput.cs new file mode 100644 index 0000000..9f50216 --- /dev/null +++ b/Admin.NET.Core/Service/User/Dto/UserRegWayInput.cs @@ -0,0 +1,72 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 注册方案分页查询输入参数 +/// +public class PageUserRegWayInput : BasePageInput +{ + /// + /// 方案名称 + /// + public string? Name { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +/// +/// 注册方案增加输入参数 +/// +public class AddUserRegWayInput : SysUserRegWay +{ + /// + /// 方案名称 + /// + [Required(ErrorMessage = "方案名称不能为空")] + [MaxLength(32, ErrorMessage = "方案名称字符长度不能超过32")] + public override string Name { get; set; } + + /// + /// 账号类型 + /// + [Enum(ErrorMessage = "账号类型不正确")] + public override AccountTypeEnum AccountType { get; set; } + + /// + /// 角色 + /// + [Required(ErrorMessage = "角色不能为空")] + public override long RoleId { get; set; } + + /// + /// 机构 + /// + [Required(ErrorMessage = "机构不能为空")] + public override long OrgId { get; set; } + + /// + /// 职位 + /// + [Required(ErrorMessage = "职位不能为空")] + public override long PosId { get; set; } +} + +/// +/// 注册方案更新输入参数 +/// +public class UpdateUserRegWayInput : AddUserRegWayInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public override long Id { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/Dto/UserRegWayOutput.cs b/Admin.NET.Core/Service/User/Dto/UserRegWayOutput.cs new file mode 100644 index 0000000..9de8132 --- /dev/null +++ b/Admin.NET.Core/Service/User/Dto/UserRegWayOutput.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 注册方案输出参数 +/// +public class UserRegWayOutput : SysUserRegWay +{ + /// + /// 角色名称 + /// + public string RoleName { get; set; } + + /// + /// 机构名称 + /// + public string OrgName { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/Dto/UserRoleInput.cs b/Admin.NET.Core/Service/User/Dto/UserRoleInput.cs new file mode 100644 index 0000000..5182775 --- /dev/null +++ b/Admin.NET.Core/Service/User/Dto/UserRoleInput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 授权用户角色 +/// +public class UserRoleInput +{ + /// + /// 用户Id + /// + public long UserId { get; set; } + + /// + /// 角色Id集合 + /// + public List RoleIdList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/SysUserExtOrgService.cs b/Admin.NET.Core/Service/User/SysUserExtOrgService.cs new file mode 100644 index 0000000..9a96394 --- /dev/null +++ b/Admin.NET.Core/Service/User/SysUserExtOrgService.cs @@ -0,0 +1,88 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户扩展机构服务 +/// +public class SysUserExtOrgService : ITransient +{ + private readonly SqlSugarRepository _sysUserExtOrgRep; + + public SysUserExtOrgService(SqlSugarRepository sysUserExtOrgRep) + { + _sysUserExtOrgRep = sysUserExtOrgRep; + } + + /// + /// 获取用户扩展机构集合 + /// + /// + /// + public async Task> GetUserExtOrgList(long userId) + { + return await _sysUserExtOrgRep.GetListAsync(u => u.UserId == userId); + } + + /// + /// 更新用户扩展机构 + /// + /// + /// + /// + public async Task UpdateUserExtOrg(long userId, List extOrgList) + { + await _sysUserExtOrgRep.DeleteAsync(u => u.UserId == userId); + + if (extOrgList == null || extOrgList.Count < 1) return; + extOrgList.ForEach(u => + { + u.UserId = userId; + }); + await _sysUserExtOrgRep.InsertRangeAsync(extOrgList); + } + + /// + /// 根据机构Id集合删除扩展机构 + /// + /// + /// + public async Task DeleteUserExtOrgByOrgIdList(List orgIdList) + { + await _sysUserExtOrgRep.DeleteAsync(u => orgIdList.Contains(u.OrgId)); + } + + /// + /// 根据用户Id删除扩展机构 + /// + /// + /// + public async Task DeleteUserExtOrgByUserId(long userId) + { + await _sysUserExtOrgRep.DeleteAsync(u => u.UserId == userId); + } + + /// + /// 根据机构Id判断是否有用户 + /// + /// + /// + public async Task HasUserOrg(long orgId) + { + return await _sysUserExtOrgRep.IsAnyAsync(u => u.OrgId == orgId); + } + + /// + /// 根据职位Id判断是否有用户 + /// + /// + /// + public async Task HasUserPos(long posId) + { + return await _sysUserExtOrgRep.IsAnyAsync(u => u.PosId == posId); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/SysUserLdapService.cs b/Admin.NET.Core/Service/User/SysUserLdapService.cs new file mode 100644 index 0000000..3f1e1e3 --- /dev/null +++ b/Admin.NET.Core/Service/User/SysUserLdapService.cs @@ -0,0 +1,66 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 用户域账号服务 +/// +public class SysUserLdapService : ITransient +{ + private readonly SqlSugarRepository _sysUserLdapRep; + + public SysUserLdapService(SqlSugarRepository sysUserLdapRep) + { + _sysUserLdapRep = sysUserLdapRep; + } + + /// + /// 批量插入域账号 + /// + /// + /// + /// + public async Task InsertUserLdapList(long tenantId, List sysUserLdapList) + { + await _sysUserLdapRep.DeleteAsync(u => u.TenantId == tenantId); + + await _sysUserLdapRep.InsertRangeAsync(sysUserLdapList); + + await _sysUserLdapRep.AsUpdateable() + .InnerJoin((l, u) => l.EmployeeId == u.Account) + .SetColumns((l, u) => new SysUserLdap { UserId = u.Id }) + .Where((l, u) => l.TenantId == tenantId && u.Status == StatusEnum.Enable) + .ExecuteCommandAsync(); + } + + /// + /// 增加域账号 + /// + /// + /// + /// + /// + /// + public async Task AddUserLdap(long tenantId, long userId, string account, string domainAccount) + { + var userLdap = await _sysUserLdapRep.GetFirstAsync(u => u.TenantId == tenantId && (u.Account == account || u.UserId == userId || u.EmployeeId == domainAccount)); + if (userLdap != null) await _sysUserLdapRep.DeleteByIdAsync(userLdap.Id); + + if (!string.IsNullOrWhiteSpace(domainAccount)) + await _sysUserLdapRep.InsertAsync(new SysUserLdap { EmployeeId = account, TenantId = tenantId, UserId = userId, Account = domainAccount }); + } + + /// + /// 删除域账号 + /// + /// + /// + public async Task DeleteUserLdapByUserId(long userId) + { + await _sysUserLdapRep.DeleteAsync(u => u.UserId == userId); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/SysUserMenuService.cs b/Admin.NET.Core/Service/User/SysUserMenuService.cs new file mode 100644 index 0000000..b09b912 --- /dev/null +++ b/Admin.NET.Core/Service/User/SysUserMenuService.cs @@ -0,0 +1,101 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户菜单快捷导航服务 🧩 +/// +[ApiDescriptionSettings(Order = 445)] +public class SysUserMenuService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysUserMenuRep; + private readonly UserManager _userManager; + + public SysUserMenuService(SqlSugarRepository sysUserMenuRep, UserManager userManager) + { + _sysUserMenuRep = sysUserMenuRep; + _userManager = userManager; + } + + /// + /// 收藏菜单 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("收藏菜单")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task AddUserMenu(UserMenuInput input) + { + await _sysUserMenuRep.DeleteAsync(u => u.UserId == _userManager.UserId); + + if (input.MenuIdList == null || input.MenuIdList.Count == 0) return; + var menus = input.MenuIdList.Select(u => new SysUserMenu + { + UserId = _userManager.UserId, + MenuId = u + }).ToList(); + await _sysUserMenuRep.InsertRangeAsync(menus); + } + + /// + /// 取消收藏菜单 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "DeleteUserMenu"), HttpPost] + [DisplayName("取消收藏菜单")] + public async Task DeleteUserMenu(UserMenuInput input) + { + await _sysUserMenuRep.DeleteAsync(u => u.UserId == _userManager.UserId && input.MenuIdList.Contains(u.MenuId)); + } + + /// + /// 获取当前用户收藏的菜单集合 🔖 + /// + /// + [DisplayName("获取当前用户收藏的菜单集合")] + public async Task> GetUserMenuList() + { + var sysUserMenuList = await _sysUserMenuRep.AsQueryable() + .Includes(u => u.SysMenu) + .Where(u => u.UserId == _userManager.UserId).ToListAsync(); + return sysUserMenuList.Where(u => u.SysMenu != null).Select(u => u.SysMenu).ToList().Adapt>(); + } + + /// + /// 获取当前用户收藏的菜单Id集合 🔖 + /// + /// + [DisplayName("获取当前用户收藏的菜单Id集合")] + public async Task> GetUserMenuIdList() + { + return await _sysUserMenuRep.AsQueryable() + .Where(u => u.UserId == _userManager.UserId).Select(u => u.MenuId).ToListAsync(); + } + + /// + /// 删除指定用户的收藏菜单 + /// + /// + [NonAction] + public async Task DeleteUserMenuList(long userId) + { + await _sysUserMenuRep.DeleteAsync(u => u.UserId == userId); + } + + /// + /// 批量删除收藏菜单 + /// + /// + [NonAction] + public async Task DeleteMenuList(List ids) + { + if (ids == null || ids.Count == 0) return; + await _sysUserMenuRep.DeleteAsync(u => ids.Contains(u.MenuId)); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/SysUserRegWayService.cs b/Admin.NET.Core/Service/User/SysUserRegWayService.cs new file mode 100644 index 0000000..9ad8ecb --- /dev/null +++ b/Admin.NET.Core/Service/User/SysUserRegWayService.cs @@ -0,0 +1,118 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户注册方案服务 🧩 +/// +[ApiDescriptionSettings(Order = 490)] +public class SysUserRegWayService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysUserRegWayRep; + private readonly UserManager _userManager; + + public SysUserRegWayService(SqlSugarRepository sysUserRegWayRep, UserManager userManager) + { + _sysUserRegWayRep = sysUserRegWayRep; + _userManager = userManager; + } + + /// + /// 查询注册方案列表 🔖 + /// + /// + /// + [DisplayName("查询注册方案列表")] + [ApiDescriptionSettings(Name = "List"), HttpPost] + public async Task> List(PageUserRegWayInput input) + { + input.Keyword = input.Keyword?.Trim(); + var query = _sysUserRegWayRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Name.Contains(input.Keyword)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name.Trim())) + .LeftJoin((u, a) => u.RoleId == a.Id) + .LeftJoin((u, a, b) => u.OrgId == b.Id) + .LeftJoin((u, a, b, c) => u.PosId == c.Id) + .Select((u, a, b, c) => new UserRegWayOutput + { + RoleName = a.Name, + OrgName = b.Name, + PosName = c.Name, + }, true); + return await query.OrderBuilder(input).ToListAsync(); + } + + /// + /// 增加注册方案 ➕ + /// + /// + /// + [DisplayName("增加注册方案")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(AddUserRegWayInput input) + { + var entity = input.Adapt(); + if (await _sysUserRegWayRep.IsAnyAsync(u => u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D2101); + + await CheckData(input); + return await _sysUserRegWayRep.InsertAsync(entity) ? entity.Id : 0; + } + + /// + /// 更新注册方案 ✏️ + /// + /// + /// + [DisplayName("更新注册方案")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(UpdateUserRegWayInput input) + { + if (await _sysUserRegWayRep.IsAnyAsync(u => u.Id != input.Id && u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D2101); + + await CheckData(input); + await _sysUserRegWayRep.AsUpdateable(input).ExecuteCommandAsync(); + } + + /// + /// 检查数据 + /// + /// + [NonAction] + public async Task CheckData(AddUserRegWayInput input) + { + // 检查外键数据是否存在 + if (!await _sysUserRegWayRep.Context.Queryable().AnyAsync(u => u.Id == input.RoleId)) throw Oops.Oh(ErrorCodeEnum.D1036); + if (!await _sysUserRegWayRep.Context.Queryable().AnyAsync(u => u.Id == input.OrgId)) throw Oops.Oh(ErrorCodeEnum.D2011); + if (!await _sysUserRegWayRep.Context.Queryable().AnyAsync(u => u.Id == input.PosId)) throw Oops.Oh(ErrorCodeEnum.D6003); + + // 禁止注册超级管理员和系统管理员 + if (input.AccountType is AccountTypeEnum.SysAdmin or AccountTypeEnum.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D1037); + } + + /// + /// 删除注册方案 ❌ + /// + /// + /// + [UnitOfWork] + [DisplayName("删除注册方案")] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task Delete(BaseIdInput input) + { + var entity = await _sysUserRegWayRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + + // 关闭相关租户注册功能 + await _sysUserRegWayRep.Context.Updateable(new SysTenant { EnableReg = YesNoEnum.N, RegWayId = null }) + .UpdateColumns(u => new { u.EnableReg, u.RegWayId }) + .Where(u => u.RegWayId == input.Id) + .ExecuteCommandAsync(); + + // 删除方案 + await _sysUserRegWayRep.DeleteAsync(entity); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/SysUserRoleService.cs b/Admin.NET.Core/Service/User/SysUserRoleService.cs new file mode 100644 index 0000000..cc45391 --- /dev/null +++ b/Admin.NET.Core/Service/User/SysUserRoleService.cs @@ -0,0 +1,106 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户角色服务 +/// +public class SysUserRoleService : ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly SqlSugarRepository _sysUserRoleRep; + + public SysUserRoleService(SysCacheService sysCacheService, + SqlSugarRepository sysUserRoleRep) + { + _sysCacheService = sysCacheService; + _sysUserRoleRep = sysUserRoleRep; + } + + /// + /// 授权用户角色 + /// + /// + /// + public async Task GrantUserRole(UserRoleInput input) + { + await _sysUserRoleRep.DeleteAsync(u => u.UserId == input.UserId); + + if (input.RoleIdList == null || input.RoleIdList.Count < 1) return; + var roles = input.RoleIdList.Select(u => new SysUserRole + { + UserId = input.UserId, + RoleId = u + }).ToList(); + await _sysUserRoleRep.InsertRangeAsync(roles); + _sysCacheService.Remove(CacheConst.KeyUserButton + input.UserId); + } + + /// + /// 根据角色Id删除用户角色 + /// + /// + /// + public async Task DeleteUserRoleByRoleId(long roleId) + { + await _sysUserRoleRep.AsQueryable() + .Where(u => u.RoleId == roleId) + .Select(u => u.UserId) + .ForEachAsync(userId => + { + _sysCacheService.Remove(CacheConst.KeyUserButton + userId); + }); + + await _sysUserRoleRep.DeleteAsync(u => u.RoleId == roleId); + } + + /// + /// 根据用户Id删除用户角色 + /// + /// + /// + public async Task DeleteUserRoleByUserId(long userId) + { + await _sysUserRoleRep.DeleteAsync(u => u.UserId == userId); + _sysCacheService.Remove(CacheConst.KeyUserButton + userId); + } + + /// + /// 根据用户Id获取角色集合 + /// + /// + /// + public async Task> GetUserRoleList(long userId) + { + var sysUserRoleList = await _sysUserRoleRep.AsQueryable() + .Includes(u => u.SysRole) + .Where(u => u.UserId == userId).ToListAsync(); + return sysUserRoleList.Where(u => u.SysRole != null).Select(u => u.SysRole).ToList(); + } + + /// + /// 根据用户Id获取角色Id集合 + /// + /// + /// + public async Task> GetUserRoleIdList(long userId) + { + return await _sysUserRoleRep.AsQueryable() + .Where(u => u.UserId == userId).Select(u => u.RoleId).ToListAsync(); + } + + /// + /// 根据角色Id获取用户Id集合 + /// + /// + /// + public async Task> GetUserIdList(long roleId) + { + return await _sysUserRoleRep.AsQueryable() + .Where(u => u.RoleId == roleId).Select(u => u.UserId).ToListAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/SysUserService.cs b/Admin.NET.Core/Service/User/SysUserService.cs new file mode 100644 index 0000000..2b164e0 --- /dev/null +++ b/Admin.NET.Core/Service/User/SysUserService.cs @@ -0,0 +1,505 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户服务 🧩 +/// +[ApiDescriptionSettings(Order = 490)] +public class SysUserService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SysOrgService _sysOrgService; + private readonly SysUserExtOrgService _sysUserExtOrgService; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysConfigService _sysConfigService; + private readonly SysOnlineUserService _sysOnlineUserService; + private readonly SysUserMenuService _sysUserMenuService; + private readonly SysCacheService _sysCacheService; + private readonly SysUserLdapService _sysUserLdapService; + private readonly SqlSugarRepository _sysUserRep; + private readonly IEventPublisher _eventPublisher; + + public SysUserService(UserManager userManager, + SysOrgService sysOrgService, + SysUserExtOrgService sysUserExtOrgService, + SysUserRoleService sysUserRoleService, + SysConfigService sysConfigService, + SysOnlineUserService sysOnlineUserService, + SysCacheService sysCacheService, + SysUserLdapService sysUserLdapService, + SqlSugarRepository sysUserRep, + SysUserMenuService sysUserMenuService, + IEventPublisher eventPublisher) + { + _userManager = userManager; + _sysOrgService = sysOrgService; + _sysUserExtOrgService = sysUserExtOrgService; + _sysUserRoleService = sysUserRoleService; + _sysConfigService = sysConfigService; + _sysOnlineUserService = sysOnlineUserService; + _sysCacheService = sysCacheService; + _sysUserLdapService = sysUserLdapService; + _sysUserMenuService = sysUserMenuService; + _sysUserRep = sysUserRep; + _eventPublisher = eventPublisher; + } + + /// + /// 获取用户分页列表 🔖 + /// + /// + /// + [DisplayName("获取用户分页列表")] + public virtual async Task> Page(PageUserInput input) + { + //获取子节点Id集合(包含自己) + var orgList = await _sysOrgService.GetChildIdListWithSelfById(input.OrgId); + + return await _sysUserRep.AsQueryable() + .LeftJoin((u, a) => u.OrgId == a.Id) + .LeftJoin((u, a, b) => u.PosId == b.Id) + .Where(u => u.AccountType != AccountTypeEnum.SuperAdmin) + .WhereIF(input.OrgId > 0, u => orgList.Contains(u.OrgId)) + .WhereIF(!_userManager.SuperAdmin, u => u.AccountType != AccountTypeEnum.SysAdmin) + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account.Contains(input.Account)) + .WhereIF(!string.IsNullOrWhiteSpace(input.RealName), u => u.RealName.Contains(input.RealName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.PosName), (u, a, b) => b.Name.Contains(input.PosName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), u => u.Phone.Contains(input.Phone)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .Select((u, a, b) => new UserOutput + { + OrgName = a.Name, + PosName = b.Name, + RoleName = SqlFunc.Subqueryable().LeftJoin((m, n) => m.RoleId == n.Id).Where(m => m.UserId == u.Id).SelectStringJoin((m, n) => n.Name, ","), + DomainAccount = SqlFunc.Subqueryable().Where(m => m.UserId == u.Id).Select(m => m.Account) + }, true) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加用户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加用户")] + public virtual async Task AddUser(AddUserInput input) + { + var isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.Account); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1003); + + if (!string.IsNullOrWhiteSpace(input.Phone) && await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Phone == input.Phone)) + throw Oops.Oh(ErrorCodeEnum.D1032); + + // 禁止越权新增超级管理员和系统管理员 + if (_userManager.AccountType != AccountTypeEnum.SuperAdmin && input.AccountType is AccountTypeEnum.SuperAdmin or AccountTypeEnum.SysAdmin) throw Oops.Oh(ErrorCodeEnum.D1038); + + // 若没有设置密码则取默认密码 + var password = !string.IsNullOrWhiteSpace(input.Password) ? input.Password : await _sysConfigService.GetConfigValue(ConfigConst.SysPassword); + var user = input.Adapt(); + user.Password = CryptogramUtil.Encrypt(password); + var newUser = await _sysUserRep.AsInsertable(user).ExecuteReturnEntityAsync(); + + input.Id = newUser.Id; + await UpdateRoleAndExtOrg(input); + + // 增加域账号 + if (!string.IsNullOrWhiteSpace(input.DomainAccount)) + await _sysUserLdapService.AddUserLdap(newUser.TenantId!.Value, newUser.Id, newUser.Account, input.DomainAccount); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Add, new + { + Entity = newUser, + Input = input + }); + + return newUser.Id; + } + + /// + /// 注册用户 🔖 + /// + /// + /// + [NonAction] + public virtual async Task RegisterUser(AddUserInput input) + { + var isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.Account); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1003); + + if (!string.IsNullOrWhiteSpace(input.Phone) && await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Phone == input.Phone)) + throw Oops.Oh(ErrorCodeEnum.D1032); + + // 禁止越权注册 + if (input.AccountType is AccountTypeEnum.SuperAdmin or AccountTypeEnum.SysAdmin) throw Oops.Oh(ErrorCodeEnum.D1038); + + if (string.IsNullOrWhiteSpace(input.Password)) + { + var password = await _sysConfigService.GetConfigValue(ConfigConst.SysPassword); + input.Password = CryptogramUtil.Encrypt(password); + } + + var user = input.Adapt(); + var newUser = await _sysUserRep.AsInsertable(user).ExecuteReturnEntityAsync(); + + input.Id = newUser.Id; + await UpdateRoleAndExtOrg(input); + + // 增加域账号 + if (!string.IsNullOrWhiteSpace(input.DomainAccount)) + await _sysUserLdapService.AddUserLdap(newUser.TenantId!.Value, newUser.Id, newUser.Account, input.DomainAccount); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Register, new + { + Entity = newUser, + Input = input + }); + + return newUser.Id; + } + + /// + /// 更新用户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新用户")] + public virtual async Task UpdateUser(UpdateUserInput input) + { + if (await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.Account && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D1003); + + if (!string.IsNullOrWhiteSpace(input.Phone) && await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Phone == input.Phone && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D1032); + + // 禁止越权更新超级管理员或系统管理员信息 + if (_userManager.AccountType != AccountTypeEnum.SuperAdmin && input.AccountType is AccountTypeEnum.SuperAdmin or AccountTypeEnum.SysAdmin) throw Oops.Oh(ErrorCodeEnum.D1038); + + await _sysUserRep.AsUpdateable(input.Adapt()).IgnoreColumns(true) + .IgnoreColumns(u => new { u.Password, u.Status, u.TenantId }).ExecuteCommandAsync(); + + await UpdateRoleAndExtOrg(input); + + // 删除用户机构缓存 + SqlSugarFilter.DeleteUserOrgCache(input.Id, _sysUserRep.Context.CurrentConnectionConfig.ConfigId.ToString()); + + // 若账号的角色和组织架构发生变化,则强制下线账号进行权限更新 + var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == input.Id); + var roleIds = await GetOwnRoleList(input.Id); + if (input.OrgId != user.OrgId || !input.RoleIdList.OrderBy(u => u).SequenceEqual(roleIds.OrderBy(u => u))) + await _sysOnlineUserService.ForceOffline(input.Id); + // 更新域账号 + await _sysUserLdapService.AddUserLdap(user.TenantId!.Value, user.Id, user.Account, input.DomainAccount); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Update, new + { + Entity = user, + Input = input + }); + } + + /// + /// 更新当前用户语言 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "SetLangCode"), HttpPost] + [DisplayName("更新当前用户语言")] + public virtual async Task SetLangCode(string langCode) + { + var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401); + user.LangCode = langCode; + await _sysUserRep.AsUpdateable(user).UpdateColumns(it => it.LangCode).ExecuteCommandAsync(); + } + + /// + /// 更新角色和扩展机构 + /// + /// + /// + private async Task UpdateRoleAndExtOrg(AddUserInput input) + { + await GrantRole(new UserRoleInput { UserId = input.Id, RoleIdList = input.RoleIdList }); + + await _sysUserExtOrgService.UpdateUserExtOrg(input.Id, input.ExtOrgIdList); + } + + /// + /// 删除用户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除用户")] + public virtual async Task DeleteUser(DeleteUserInput input) + { + var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + user.ValidateIsSuperAdminAccountType(); + user.ValidateIsUserId(_userManager.UserId); + + // 若账号为租户默认账号则禁止删除 + var isTenantUser = await _sysUserRep.ChangeRepository>().IsAnyAsync(u => u.UserId == input.Id); + if (isTenantUser) throw Oops.Oh(ErrorCodeEnum.D1029); + + // 若账号为开放接口绑定账号则禁止删除 + var isOpenAccessUser = await _sysUserRep.ChangeRepository>().IsAnyAsync(u => u.BindUserId == input.Id); + if (isOpenAccessUser) throw Oops.Oh(ErrorCodeEnum.D1030); + + // 强制下线 + await _sysOnlineUserService.ForceOffline(user.Id); + + await _sysUserRep.DeleteAsync(user); + + // 删除用户角色 + await _sysUserRoleService.DeleteUserRoleByUserId(input.Id); + + // 删除用户扩展机构 + await _sysUserExtOrgService.DeleteUserExtOrgByUserId(input.Id); + + // 删除域账号 + await _sysUserLdapService.DeleteUserLdapByUserId(input.Id); + + // 删除用户收藏菜单 + await _sysUserMenuService.DeleteUserMenuList(input.Id); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Delete, new + { + Entity = user, + Input = input + }); + } + + /// + /// 查看用户基本信息 🔖 + /// + /// + [DisplayName("查看用户基本信息")] + public virtual async Task GetBaseInfo() + { + return await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(c => c.Id == _userManager.UserId); + } + + /// + /// 查询用户组织机构信息 🔖 + /// + /// + [DisplayName("查询用户组织机构信息")] + public virtual async Task> GetOrgInfo() + { + return await _sysOrgService.GetTree(new OrgInput { Id = 0 }); + } + + /// + /// 更新用户基本信息 🔖 + /// + /// + [ApiDescriptionSettings(Name = "BaseInfo"), HttpPost] + [DisplayName("更新用户基本信息")] + public virtual async Task UpdateBaseInfo(SysUser user) + { + return await _sysUserRep.AsUpdateable(user) + .IgnoreColumns(u => new { u.CreateTime, u.Account, u.Password, u.AccountType, u.OrgId, u.PosId }).ExecuteCommandAsync(); + } + + /// + /// 设置用户状态 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("设置用户状态")] + public virtual async Task SetStatus(UserInput input) + { + if (_userManager.UserId == input.Id) + throw Oops.Oh(ErrorCodeEnum.D1026); + + var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + user.ValidateIsSuperAdminAccountType(ErrorCodeEnum.D1015); + if (!Enum.IsDefined(typeof(StatusEnum), input.Status)) + throw Oops.Oh(ErrorCodeEnum.D3005); + + // 账号禁用则增加黑名单,账号启用则移除黑名单 + var sysCacheService = App.GetRequiredService(); + if (input.Status == StatusEnum.Disable) + { + sysCacheService.Set($"{CacheConst.KeyBlacklist}{user.Id}", $"{user.RealName}-{user.Phone}"); + + // 强制下线 + await _sysOnlineUserService.ForceOffline(user.Id); + } + else + { + sysCacheService.Remove($"{CacheConst.KeyBlacklist}{user.Id}"); + } + + user.Status = input.Status; + var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync(); + + // 发布系统用户操作事件 + if (rows > 0) + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.SetStatus, new + { + Entity = user, + Input = input + }); + + return rows; + } + + /// + /// 授权用户角色 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权用户角色")] + public async Task GrantRole(UserRoleInput input) + { + //var user = await _sysUserRep.GetFirstAsync(u => u.Id == input.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + //if (user.AccountType == AccountTypeEnum.SuperAdmin) + // throw Oops.Oh(ErrorCodeEnum.D1022); + + await _sysUserRoleService.GrantUserRole(input); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.UpdateRole, input); + } + + /// + /// 修改用户密码 🔖 + /// + /// + /// + [DisplayName("修改用户密码")] + public virtual async Task ChangePwd(ChangePwdInput input) + { + // 国密SM2解密(前端密码传输SM2加密后的) + input.PasswordOld = CryptogramUtil.SM2Decrypt(input.PasswordOld); + input.PasswordNew = CryptogramUtil.SM2Decrypt(input.PasswordNew); + + var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(c => c.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString()) + { + if (user.Password != MD5Encryption.Encrypt(input.PasswordOld)) + throw Oops.Oh(ErrorCodeEnum.D1004); + } + else + { + if (CryptogramUtil.Decrypt(user.Password) != input.PasswordOld) + throw Oops.Oh(ErrorCodeEnum.D1004); + } + + if (input.PasswordOld == input.PasswordNew) + throw Oops.Oh(ErrorCodeEnum.D1028); + + // 验证密码强度 + if (CryptogramUtil.StrongPassword) + { + user.Password = input.PasswordNew.TryValidate(CryptogramUtil.PasswordStrengthValidation) + ? CryptogramUtil.Encrypt(input.PasswordNew) + : throw Oops.Oh(CryptogramUtil.PasswordStrengthValidationMsg); + } + else + { + user.Password = CryptogramUtil.Encrypt(input.PasswordNew); + } + + var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => u.Password).ExecuteCommandAsync(); + + // 发布系统用户操作事件 + if (rows > 0) + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.ChangePwd, new + { + Entity = user, + Input = input + }); + + return rows; + } + + /// + /// 重置用户密码 🔖 + /// + /// + /// + [DisplayName("重置用户密码")] + public virtual async Task ResetPwd(ResetPwdUserInput input) + { + var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + string randomPassword = new(Enumerable.Repeat("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 6).Select(s => s[Random.Shared.Next(s.Length)]).ToArray()); + user.Password = CryptogramUtil.Encrypt(randomPassword); + await _sysUserRep.AsUpdateable(user).UpdateColumns(u => u.Password).ExecuteCommandAsync(); + + // 清空密码错误次数 + var keyErrorPasswordCount = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; + _sysCacheService.Remove(keyErrorPasswordCount); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.ResetPwd, new + { + Entity = user, + Input = input + }); + + return randomPassword; + } + + /// + /// 解除登录锁定 🔖 + /// + /// + /// + [DisplayName("解除登录锁定")] + public virtual async Task UnlockLogin(UnlockLoginInput input) + { + var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + + // 清空密码错误次数 + var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; + _sysCacheService.Remove(keyPasswordErrorTimes); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.UnlockLogin, new + { + Entity = user, + Input = input + }); + } + + /// + /// 获取用户拥有角色集合 🔖 + /// + /// + /// + [DisplayName("获取用户拥有角色集合")] + public async Task> GetOwnRoleList(long userId) + { + return await _sysUserRoleService.GetUserRoleIdList(userId); + } + + /// + /// 获取用户扩展机构集合 🔖 + /// + /// + /// + [DisplayName("获取用户扩展机构集合")] + public async Task> GetOwnExtOrgList(long userId) + { + return await _sysUserExtOrgService.GetUserExtOrgList(userId); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/User/UserManager.cs b/Admin.NET.Core/Service/User/UserManager.cs new file mode 100644 index 0000000..81d7d8e --- /dev/null +++ b/Admin.NET.Core/Service/User/UserManager.cs @@ -0,0 +1,67 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 当前登录用户 +/// +public class UserManager : IScoped +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// 用户ID + /// + public long UserId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.UserId)?.Value).ToLong(); + + /// + /// 租户ID + /// + public long TenantId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.TenantId)?.Value).ToLong(); + + /// + /// 用户账号 + /// + public string Account => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.Account)?.Value; + + /// + /// 真实姓名 + /// + public string RealName => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.RealName)?.Value; + + /// + /// 账号类型 + /// + public AccountTypeEnum? AccountType => int.TryParse(_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value, out var val) ? (AccountTypeEnum?)val : null; + + /// + /// 是否超级管理员 + /// + public bool SuperAdmin => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString(); + + /// + /// 是否系统管理员 + /// + public bool SysAdmin => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SysAdmin).ToString(); + + /// + /// 组织机构Id + /// + public long OrgId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OrgId)?.Value).ToLong(); + + /// + /// 微信OpenId + /// + public string OpenId => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OpenId)?.Value; + + public string LangCode => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.LangCode)?.Value ?? "zh_CN"; + + public UserManager(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs b/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs new file mode 100644 index 0000000..87fcb47 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs @@ -0,0 +1,149 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 生成网页授权Url +/// +public class GenAuthUrlInput +{ + /// + /// RedirectUrl + /// + public string RedirectUrl { get; set; } + + /// + /// Scope + /// + public string Scope { get; set; } + + /// + /// State + /// + public string State { get; set; } +} + +/// +/// 获取微信用户OpenId +/// +public class WechatOAuth2Input +{ + /// + /// Code + /// + [Required(ErrorMessage = "Code不能为空"), MinLength(10, ErrorMessage = "Code错误")] + public string Code { get; set; } +} + +/// +/// 微信用户登录 +/// +public class WechatUserLogin +{ + /// + /// OpenId + /// + [Required(ErrorMessage = "微信标识不能为空"), MinLength(10, ErrorMessage = "微信标识长错误")] + public string OpenId { get; set; } +} + +/// +/// 获取配置签名 +/// +public class SignatureInput +{ + /// + /// Url + /// + public string Url { get; set; } +} + +/// +/// 获取消息模板列表 +/// +public class MessageTemplateSendInput +{ + /// + /// 订阅模板Id + /// + [Required(ErrorMessage = "订阅模板Id不能为空")] + public string TemplateId { get; set; } + + /// + /// 接收者的OpenId + /// + [Required(ErrorMessage = "接收者的OpenId不能为空")] + public string ToUserOpenId { get; set; } + + /// + /// 模板数据,格式形如 { "key1": { "value": any }, "key2": { "value": any } } + /// + [Required(ErrorMessage = "模板数据不能为空")] + public Dictionary Data { get; set; } + + /// + /// 模板跳转链接 + /// + public string Url { get; set; } + + /// + /// 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar) + /// + public string MiniProgramPagePath { get; set; } +} + +/// +/// 删除消息模板 +/// +public class DeleteMessageTemplateInput +{ + /// + /// 订阅模板Id + /// + [Required(ErrorMessage = "订阅模板Id不能为空")] + public string TemplateId { get; set; } +} + +public class UploadAvatarInput +{ + /// + /// 小程序用户身份标识 + /// + [Required(ErrorMessage = "OpenId不能为空")] + public string OpenId { get; set; } + + /// + /// 文件 + /// + [Required] + public IFormFile File { get; set; } + + /// + /// 文件类型 + /// + public string FileType { get; set; } + + /// + /// 文件路径 + /// + public string Path { get; set; } +} + +public class SetNickNameInput +{ + /// + /// 小程序用户身份标识 + /// + [Required(ErrorMessage = "OpenId不能为空")] + public string OpenId { get; set; } + + /// + /// 昵称 + /// + [Required(ErrorMessage = "昵称不能为空")] + public string NickName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/Dto/WechatPayInput.cs b/Admin.NET.Core/Service/Wechat/Dto/WechatPayInput.cs new file mode 100644 index 0000000..8ba4663 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/Dto/WechatPayInput.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class WechatPayTransactionInput +{ + /// + /// OpenId + /// + public string OpenId { get; set; } + + /// + /// 订单金额 + /// + public int Total { get; set; } + + /// + /// 商品描述 + /// + public string Description { get; set; } + + /// + /// 附加数据 + /// + public string Attachment { get; set; } + + /// + /// 优惠标记 + /// + public string GoodsTag { get; set; } + + /// + /// 业务标签,用来区分做什么业务 + /// + public string Tags { get; set; } + + /// + /// 对应业务的主键 + /// + public long BusinessId { get; set; } +} + +public class WechatPayParaInput +{ + /// + /// 订单Id + /// + public string PrepayId { get; set; } +} + +public class WechatPayRefundDomesticInput +{ + /// + /// 商户端生成的业务流水号 + /// + [Required] + public string TradeId { get; set; } + + /// + /// 退款原因 + /// + public string Reason { get; set; } + + /// + /// 退款金额 + /// + [Required] + public int Refund { get; set; } + + /// + /// 原订单金额 + /// + [Required] + public int Total { get; set; } +} + +public class WechatPayPageInput : BasePageInput +{ + /// + /// 添加时间范围 + /// + public List CreateTimeRange { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/Dto/WechatPayOutput.cs b/Admin.NET.Core/Service/Wechat/Dto/WechatPayOutput.cs new file mode 100644 index 0000000..d423f00 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/Dto/WechatPayOutput.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class WechatPayOutput +{ + /// + /// OpenId + /// + public string OpenId { get; set; } + + /// + /// 订单金额 + /// + public int Total { get; set; } + + /// + /// 附加数据 + /// + public string Attachment { get; set; } + + /// + /// 优惠标记 + /// + public string GoodsTag { get; set; } +} + +public class WechatPayTransactionOutput +{ + public string PrepayId { get; set; } + + public string OutTradeNumber { get; set; } + + public WechatPayParaOutput SingInfo { get; set; } +} + +public class WechatPayParaOutput +{ + public string AppId { get; set; } + + public string TimeStamp { get; set; } + + public string NonceStr { get; set; } + + public string Package { get; set; } + + public string SignType { get; set; } + + public string PaySign { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/Dto/WechatUserInput.cs b/Admin.NET.Core/Service/Wechat/Dto/WechatUserInput.cs new file mode 100644 index 0000000..f7aa580 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/Dto/WechatUserInput.cs @@ -0,0 +1,24 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class WechatUserInput : BasePageInput +{ + /// + /// 昵称 + /// + public string NickName { get; set; } + + /// + /// 手机号码 + /// + public string Mobile { get; set; } +} + +public class DeleteWechatUserInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/Dto/WxOpenInput.cs b/Admin.NET.Core/Service/Wechat/Dto/WxOpenInput.cs new file mode 100644 index 0000000..0f15627 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/Dto/WxOpenInput.cs @@ -0,0 +1,150 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 获取微信用户OpenId +/// +public class JsCode2SessionInput +{ + /// + /// JsCode + /// + [Required(ErrorMessage = "JsCode不能为空"), MinLength(10, ErrorMessage = "JsCode错误")] + public string JsCode { get; set; } +} + +/// +/// 获取微信用户电话号码 +/// +public class WxPhoneInput : WxOpenIdLoginInput +{ + /// + /// Code + /// + [Required(ErrorMessage = "Code不能为空"), MinLength(10, ErrorMessage = "Code错误")] + public string Code { get; set; } +} + +/// +/// 微信小程序登录 +/// +public class WxOpenIdLoginInput +{ + /// + /// OpenId + /// + [Required(ErrorMessage = "微信标识不能为空"), MinLength(10, ErrorMessage = "微信标识错误")] + public string OpenId { get; set; } +} + +/// +/// 微信手机号登录 +/// +public class WxPhoneLoginInput +{ + /// + /// 电话号码 + /// + [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "电话号码错误")] + public string PhoneNumber { get; set; } +} + +/// +/// 发送订阅消息 +/// +public class SendSubscribeMessageInput +{ + /// + /// 订阅模板Id + /// + [Required(ErrorMessage = "订阅模板Id不能为空")] + public string TemplateId { get; set; } + + /// + /// 接收者的OpenId + /// + [Required(ErrorMessage = "接收者的OpenId不能为空")] + public string ToUserOpenId { get; set; } + + /// + /// 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } + /// + [Required(ErrorMessage = "模板内容不能为空")] + public Dictionary Data { get; set; } + + /// + /// 跳转小程序类型 + /// + public string MiniprogramState { get; set; } + + /// + /// 语言类型 + /// + public string Language { get; set; } + + /// + /// 点击模板卡片后的跳转页面(仅限本小程序内的页面),支持带参数(示例pages/app/index?foo=bar) + /// + public string MiniProgramPagePath { get; set; } +} + +/// +/// 增加订阅消息模板 +/// +public class AddSubscribeMessageTemplateInput +{ + /// + /// 模板标题Id + /// + [Required(ErrorMessage = "模板标题Id不能为空")] + public string TemplateTitleId { get; set; } + + /// + /// 模板关键词列表,例如 [3,5,4] + /// + [Required(ErrorMessage = "模板关键词列表不能为空")] + public List KeyworkIdList { get; set; } + + /// + /// 服务场景描述,15个字以内 + /// + [Required(ErrorMessage = "服务场景描述不能为空")] + public string SceneDescription { get; set; } +} + +/// +/// 生成带参数小程序二维码(总共生成的码数量限制为 100,000) +/// +public class GenerateQRImageInput +{ + /// + /// 扫码进入的小程序页面路径,最大长度 128 个字符,不能为空; eg: pages/index?id=0001 + /// + public string PagePath { get; set; } + + /// + /// 文件保存的名称 + /// + public string ImageName { get; set; } + + /// + /// 图片宽度 默认430 + /// + public int Width { get; set; } = 430; +} + +/// +/// 生成带参数小程序二维码(获取不受限制的小程序码) +/// +public class GenerateQRImageUnLimitInput : GenerateQRImageInput +{ + /// + /// 二维码携带的参数 eg:a=1(最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:) + /// + public string Scene { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/Dto/WxOpenOutput.cs b/Admin.NET.Core/Service/Wechat/Dto/WxOpenOutput.cs new file mode 100644 index 0000000..6b83bb3 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/Dto/WxOpenOutput.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class WxOpenIdOutput +{ + public string OpenId { get; set; } +} + +public class WxPhoneOutput +{ + public string PhoneNumber { get; set; } +} + +public class GenerateQRImageOutput +{ + /// + /// 生成状态 + /// + public bool Success { get; set; } = false; + + /// + /// 生成图片的绝对路径 + /// + public string ImgPath { get; set; } = ""; + + /// + /// 生成图片的相对路径 + /// + public string RelativeImgPath { get; set; } = ""; + + /// + /// 生成图片的错误信息 + /// + public string Message { get; set; } = ""; +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs b/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs new file mode 100644 index 0000000..31aac14 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs @@ -0,0 +1,520 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.Logging.Extensions; +using Newtonsoft.Json; + +namespace Admin.NET.Core.Service; + +/// +/// 微信支付服务 🧩 +/// +[ApiDescriptionSettings(Order = 210)] +public class SysWechatPayService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysWechatPayRep; + private readonly SqlSugarRepository _sysWechatRefundRep; + + private readonly WechatPayOptions _wechatPayOptions; + private readonly PayCallBackOptions _payCallBackOptions; + + private readonly WechatTenpayClient _wechatTenpayClient; + + public SysWechatPayService(SqlSugarRepository sysWechatPayUserRep, + SqlSugarRepository sysWechatRefundRep, + IOptions wechatPayOptions, + IOptions payCallBackOptions) + { + _sysWechatPayRep = sysWechatPayUserRep; + _sysWechatRefundRep = sysWechatRefundRep; + _wechatPayOptions = wechatPayOptions.Value; + _payCallBackOptions = payCallBackOptions.Value; + + _wechatTenpayClient = CreateTenpayClient(); + } + + /// + /// 初始化微信支付客户端 + /// + /// + private WechatTenpayClient CreateTenpayClient() + { + var cerFilePath = Path.Combine(App.WebHostEnvironment.ContentRootPath, _wechatPayOptions.MerchantCertificatePrivateKey.Replace("/", Path.DirectorySeparatorChar.ToString())); + + var tenpayClientOptions = new WechatTenpayClientOptions() + { + MerchantId = _wechatPayOptions.MerchantId, + MerchantV3Secret = _wechatPayOptions.MerchantV3Secret, + MerchantCertificateSerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber, + MerchantCertificatePrivateKey = File.Exists(cerFilePath) ? File.ReadAllText(cerFilePath) : "", + PlatformCertificateManager = new InMemoryCertificateManager() + }; + return new WechatTenpayClient(tenpayClientOptions); + } + + /// + /// 分页查询支付列表 🔖 + /// + /// + /// + [HttpPost] + [ApiDescriptionSettings(Name = "Page")] + public async Task> Page(WechatPayPageInput input) + { + var query = _sysWechatPayRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.OutTradeNumber == input.Keyword || u.TransactionId == input.Keyword) + .WhereIF(input.CreateTimeRange != null && input.CreateTimeRange.Count > 0 && input.CreateTimeRange[0].HasValue, x => x.CreateTime >= input.CreateTimeRange[0]) + .WhereIF(input.CreateTimeRange != null && input.CreateTimeRange.Count > 1 && input.CreateTimeRange[1].HasValue, x => x.CreateTime < ((DateTime)input.CreateTimeRange[1]).AddDays(1)); + return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 查询退款信息列表 + /// + /// + /// + [HttpPost] + [DisplayName("根据支付id获取退款信息列表")] + public async Task> ListRefund([FromBody] string id) + { + var query = _sysWechatRefundRep.AsQueryable() + .Where(u => u.TransactionId == id); + return await query.ToListAsync(); + } + + /// + /// 生成JSAPI调起支付所需参数 🔖 + /// + /// + /// + [DisplayName("生成JSAPI调起支付所需参数")] + public WechatPayParaOutput GenerateParametersForJsapiPay(WechatPayParaInput input) + { + var data = _wechatTenpayClient.GenerateParametersForJsapiPayRequest(_wechatPayOptions.AppId, input.PrepayId); + return new WechatPayParaOutput() + { + AppId = data["appId"], + TimeStamp = data["timeStamp"], + NonceStr = data["nonceStr"], + Package = data["package"], + SignType = data["signType"], + PaySign = data["paySign"] + }; + } + + /// + /// 微信支付下单(商户直连) 🔖 + /// + [DisplayName("微信支付下单(商户直连)")] + public async Task CreatePayTransaction([FromBody] WechatPayTransactionInput input) + { + var request = new CreatePayTransactionJsapiRequest() + { + OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号 + AppId = _wechatPayOptions.AppId, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + ExpireTime = DateTimeOffset.Now.AddMinutes(10), + NotifyUrl = _payCallBackOptions.WechatPayUrl, + Amount = new CreatePayTransactionJsapiRequest.Types.Amount() { Total = input.Total }, + Payer = new CreatePayTransactionJsapiRequest.Types.Payer() { OpenId = input.OpenId } + }; + var response = await _wechatTenpayClient.ExecuteCreatePayTransactionJsapiAsync(request); + if (!response.IsSuccessful()) + throw Oops.Oh(response.ErrorMessage); + + var singInfo = this.GenerateParametersForJsapiPay(new WechatPayParaInput() { PrepayId = response.PrepayId }); + // 保存订单信息 + var wechatPay = new SysWechatPay() + { + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + OutTradeNumber = request.OutTradeNumber, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + Total = input.Total, + OpenId = input.OpenId, + TransactionId = "", + Tags = input.Tags, + BusinessId = input.BusinessId, + }; + await _sysWechatPayRep.InsertAsync(wechatPay); + + return new WechatPayTransactionOutput() + { + PrepayId = response.PrepayId, + OutTradeNumber = request.OutTradeNumber, + SingInfo = singInfo + }; + } + + /// + /// 微信支付下单(商户直连)Native + /// + [DisplayName("微信支付下单(商户直连)Native")] + public async Task CreatePayTransactionNative([FromBody] WechatPayTransactionInput input) + { + var request = new CreatePayTransactionNativeRequest() + { + OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号 + AppId = _wechatPayOptions.AppId, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + ExpireTime = DateTimeOffset.Now.AddMinutes(10), + NotifyUrl = _payCallBackOptions.WechatPayUrl, + Amount = new CreatePayTransactionNativeRequest.Types.Amount() { Total = input.Total }, + //Payer = new CreatePayTransactionNativeRequest.Types.Payer() { OpenId = input.OpenId } + Scene = new CreatePayTransactionNativeRequest.Types.Scene() { ClientIp = "127.0.0.1" } + }; + var response = await _wechatTenpayClient.ExecuteCreatePayTransactionNativeAsync(request); + if (!response.IsSuccessful()) + { + JsonConvert.SerializeObject(response).LogInformation(); + throw Oops.Oh(response.ErrorMessage); + } + // 保存订单信息 + var wechatPay = new SysWechatPay() + { + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + OutTradeNumber = request.OutTradeNumber, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + Total = input.Total, + //OpenId = input.OpenId, + TransactionId = "", + QrcodeContent = response.QrcodeUrl, + Tags = input.Tags, + BusinessId = input.BusinessId, + }; + await _sysWechatPayRep.InsertAsync(wechatPay); + return new + { + request.OutTradeNumber, + response.QrcodeUrl + }; + } + + /// + /// 微信支付下单(服务商模式) 🔖 + /// + [DisplayName("微信支付下单(服务商模式)")] + public async Task CreatePayPartnerTransaction([FromBody] WechatPayTransactionInput input) + { + var request = new CreatePayPartnerTransactionJsapiRequest() + { + OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号 + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + SubAppId = _wechatPayOptions.AppId, + SubMerchantId = _wechatPayOptions.MerchantId, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + ExpireTime = DateTimeOffset.Now.AddMinutes(10), + NotifyUrl = _payCallBackOptions.WechatPayUrl, + Amount = new CreatePayPartnerTransactionJsapiRequest.Types.Amount() { Total = input.Total }, + Payer = new CreatePayPartnerTransactionJsapiRequest.Types.Payer() { OpenId = input.OpenId } + }; + var response = await _wechatTenpayClient.ExecuteCreatePayPartnerTransactionJsapiAsync(request); + if (!response.IsSuccessful()) + throw Oops.Oh(response.ErrorMessage); + var singInfo = this.GenerateParametersForJsapiPay(new WechatPayParaInput() { PrepayId = response.PrepayId }); + // 保存订单信息 + var wechatPay = new SysWechatPay() + { + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + SubAppId = _wechatPayOptions.AppId, + SubMerchantId = _wechatPayOptions.MerchantId, + OutTradeNumber = request.OutTradeNumber, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + Total = input.Total, + OpenId = input.OpenId, + TransactionId = "" + }; + await _sysWechatPayRep.InsertAsync(wechatPay); + + return new + { + response.PrepayId, + request.OutTradeNumber, + singInfo + }; + } + + /// + /// 获取支付订单详情(本地库) 🔖 + /// + /// + /// + [DisplayName("获取支付订单详情(本地库)")] + public async Task GetPayInfo(string tradeId) + { + return await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == tradeId); + } + + /// + /// 获取支付订单详情(微信接口) 🔖 + /// + /// + /// + [DisplayName("获取支付订单详情(微信接口)")] + public async Task GetPayInfoFromWechat(string tradeId) + { + var request = new GetPayTransactionByOutTradeNumberRequest(); + request.OutTradeNumber = tradeId; + var response = await _wechatTenpayClient.ExecuteGetPayTransactionByOutTradeNumberAsync(request); + // 修改订单支付状态 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == response.OutTradeNumber + && u.MerchantId == response.MerchantId); + // 如果状态不一致就更新数据库中的记录 + if (wechatPay != null && wechatPay.TradeState != response.TradeState) + { + wechatPay.OpenId = response.Payer.OpenId; + wechatPay.TransactionId = response.TransactionId; // 支付订单号 + wechatPay.TradeType = response.TradeType; // 交易类型 + wechatPay.TradeState = response.TradeState; // 交易状态 + wechatPay.TradeStateDescription = response.TradeStateDescription; // 交易状态描述 + wechatPay.BankType = response.BankType; // 付款银行类型 + wechatPay.Total = response.Amount.Total; // 订单总金额 + wechatPay.PayerTotal = response.Amount.PayerTotal; // 用户支付金额 + wechatPay.SuccessTime = response.SuccessTime.Value.DateTime; // 支付完成时间 + await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync(); + } + wechatPay = new SysWechatPay() + { + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + SubAppId = _wechatPayOptions.AppId, + SubMerchantId = _wechatPayOptions.MerchantId, + OutTradeNumber = request.OutTradeNumber, + Attachment = response.Attachment, + Total = response.Amount.Total, // 订单总金额 + TransactionId = response.TransactionId, + TradeType = response.TradeType, // 交易类型 + TradeState = response.TradeState, // 交易状态 + TradeStateDescription = response.TradeStateDescription, // 交易状态描述 + BankType = response.BankType, // 付款银行类型 + PayerTotal = response.Amount.PayerTotal, // 用户支付金额 + SuccessTime = response.SuccessTime.Value.DateTime // 支付完成时间 + }; + return wechatPay; + } + + /// + /// 退款申请 + /// + /// + /// + [DisplayName("退款申请")] + [HttpPost] + public async Task CreateRefundDomestic([FromBody] WechatPayRefundDomesticInput input) + { + // refund/domestic/refunds + var request = new CreateRefundDomesticRefundRequest() + { + Amount = new CreateRefundDomesticRefundRequest.Types.Amount() + { + Refund = input.Refund, + Total = input.Total, + Currency = "CNY" + }, + + OutTradeNumber = input.TradeId, + OutRefundNumber = "R" + DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号 + NotifyUrl = _payCallBackOptions.WechatRefundUrl, // 应采用WechatRefundUrl参数,如与WechatPayUrl入口相同,也应分开设置参数 + Reason = input.Reason, + }; + var response = await _wechatTenpayClient.ExecuteCreateRefundDomesticRefundAsync(request); + if (string.IsNullOrEmpty(response.ErrorCode)) + { + // 成功了,这里应该保存退款订单信息 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == response.OutTradeNumber); + // 保存订单信息 + if (wechatPay != null) + { + var wechatRefund = new SysWechatRefund() + { + WechatPayId = wechatPay.Id, + TransactionId = response.TransactionId, + Refund = input.Refund, + Reason = input.Reason, + OutRefundNumber = request.OutRefundNumber, + Channel = response.Channel, + UserReceivedAccount = response.UserReceivedAccount, + }; + await _sysWechatRefundRep.InsertAsync(wechatRefund); + } + } + else + { + throw Oops.Bah($"[{response.ErrorCode}]{response.ErrorMessage}"); + } + return response; + } + + /// + /// 获取退款订单详情(微信接口) + /// + /// + /// + [DisplayName("获取退款订单详情(微信接口)")] + public async Task GetRefundInfoFromWechat(string refundId) + { + var request = new GetRefundDomesticRefundByOutRefundNumberRequest(); + request.OutRefundNumber = refundId; + var response = await _wechatTenpayClient.ExecuteGetRefundDomesticRefundByOutRefundNumberAsync(request); + // 修改订单支付状态 + var wechatRefund = await _sysWechatRefundRep.GetFirstAsync(u => u.OutRefundNumber == refundId); + // 如果状态不一致就更新数据库中的记录 + if (wechatRefund != null && wechatRefund.TradeState != response.Status) + { + wechatRefund.TransactionId = response.TransactionId; // 支付订单号 + wechatRefund.TradeState = response.Status; // 交易状态 + wechatRefund.SuccessTime = response.SuccessTime.Value.DateTime; // 支付完成时间 + await _sysWechatRefundRep.AsUpdateable(wechatRefund).IgnoreColumns(true).ExecuteCommandAsync(); + // 有退款,刷新一下订单状态 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.Id == wechatRefund.WechatPayId); + if (wechatPay != null) + await GetPayInfoFromWechat(wechatPay.OutTradeNumber); + } + wechatRefund = new SysWechatRefund() + { + TransactionId = response.TransactionId, + Refund = response.Amount.Refund, + OutRefundNumber = request.OutRefundNumber, + Channel = response.Channel, + UserReceivedAccount = response.UserReceivedAccount, + TradeState = response.Status, // 交易状态 + SuccessTime = response.SuccessTime.Value.DateTime, // 支付完成时间 + }; + return wechatRefund; + } + + /// + /// 微信支付成功回调(商户直连) + /// + /// + [AllowAnonymous] + [DisplayName("微信支付成功回调(商户直连)")] + public async Task PayCallBack() + { + using var ms = new MemoryStream(); + await App.HttpContext.Request.Body.CopyToAsync(ms); + var b = ms.ToArray(); + var callbackJson = Encoding.UTF8.GetString(b); + + var callbackModel = _wechatTenpayClient.DeserializeEvent(callbackJson); + if ("TRANSACTION.SUCCESS".Equals(callbackModel.EventType)) + { + try + { + var callbackPayResource = _wechatTenpayClient.DecryptEventResource(callbackModel); + + // 修改订单支付状态 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == callbackPayResource.OutTradeNumber + && u.MerchantId == callbackPayResource.MerchantId); + if (wechatPay == null) return null; + wechatPay.OpenId = callbackPayResource.Payer.OpenId; // 支付者标识 + //wechatPay.MerchantId = callbackResource.MerchantId; // 微信商户号 + //wechatPay.OutTradeNumber = callbackResource.OutTradeNumber; // 商户订单号 + wechatPay.TransactionId = callbackPayResource.TransactionId; // 支付订单号 + wechatPay.TradeType = callbackPayResource.TradeType; // 交易类型 + wechatPay.TradeState = callbackPayResource.TradeState; // 交易状态 + wechatPay.TradeStateDescription = callbackPayResource.TradeStateDescription; // 交易状态描述 + wechatPay.BankType = callbackPayResource.BankType; // 付款银行类型 + wechatPay.Total = callbackPayResource.Amount.Total; // 订单总金额 + wechatPay.PayerTotal = callbackPayResource.Amount.PayerTotal; // 用户支付金额 + wechatPay.SuccessTime = callbackPayResource.SuccessTime.DateTime; // 支付完成时间 + + await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync(); + + return new WechatPayOutput() + { + Total = wechatPay.Total, + Attachment = wechatPay.Attachment, + GoodsTag = wechatPay.GoodsTag + }; + } + catch (Exception ex) + { + "微信支付回调时出错:".LogError(ex); + } + } + else if ("REFUND.SUCCESS".Equals(callbackModel.EventType)) + { + //参考:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/refund-result-notice.html + try + { + var callbackRefundResource = _wechatTenpayClient.DecryptEventResource(callbackModel); + // 修改订单支付状态 + var wechatRefund = await _sysWechatRefundRep.GetFirstAsync(u => u.OutRefundNumber == callbackRefundResource.OutRefundNumber); + if (wechatRefund == null) return null; + wechatRefund.TradeState = callbackRefundResource.RefundStatus; // 交易状态 + wechatRefund.SuccessTime = callbackRefundResource.SuccessTime.Value.DateTime; // 支付完成时间 + + await _sysWechatRefundRep.AsUpdateable(wechatRefund).IgnoreColumns(true).ExecuteCommandAsync(); + // 有退款,刷新一下订单状态 + await GetPayInfoFromWechat(callbackRefundResource.OutTradeNumber); + } + catch (Exception ex) + { + "微信退款回调时出错:".LogError(ex); + } + } + else + { + callbackModel.EventType.LogInformation(); + } + + return null; + } + + /// + /// 微信支付成功回调(服务商模式) 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("微信支付成功回调(服务商模式)")] + public async Task PayPartnerCallBack() + { + using var ms = new MemoryStream(); + await App.HttpContext.Request.Body.CopyToAsync(ms); + var b = ms.ToArray(); + var callbackJson = Encoding.UTF8.GetString(b); + + var callbackModel = _wechatTenpayClient.DeserializeEvent(callbackJson); + if ("TRANSACTION.SUCCESS".Equals(callbackModel.EventType)) + { + var callbackResource = _wechatTenpayClient.DecryptEventResource(callbackModel); + + // 修改订单支付状态 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == callbackResource.OutTradeNumber + && u.MerchantId == callbackResource.MerchantId); + if (wechatPay == null) return; + //wechatPay.OpenId = callbackResource.Payer.OpenId; // 支付者标识 + //wechatPay.MerchantId = callbackResource.MerchantId; // 微信商户号 + //wechatPay.OutTradeNumber = callbackResource.OutTradeNumber; // 商户订单号 + wechatPay.TransactionId = callbackResource.TransactionId; // 支付订单号 + wechatPay.TradeType = callbackResource.TradeType; // 交易类型 + wechatPay.TradeState = callbackResource.TradeState; // 交易状态 + wechatPay.TradeStateDescription = callbackResource.TradeStateDescription; // 交易状态描述 + wechatPay.BankType = callbackResource.BankType; // 付款银行类型 + wechatPay.Total = callbackResource.Amount.Total; // 订单总金额 + wechatPay.PayerTotal = callbackResource.Amount.PayerTotal; // 用户支付金额 + wechatPay.SuccessTime = callbackResource.SuccessTime.DateTime; // 支付完成时间 + + await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync(); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/SysWechatService.cs b/Admin.NET.Core/Service/Wechat/SysWechatService.cs new file mode 100644 index 0000000..881b9a0 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/SysWechatService.cs @@ -0,0 +1,202 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 微信公众号服务 🧩 +/// +[ApiDescriptionSettings(Order = 230)] +public class SysWechatService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysWechatUserRep; + private readonly SysConfigService _sysConfigService; + private readonly WechatApiClientFactory _wechatApiClientFactory; + private readonly WechatApiClient _wechatApiClient; + + public SysWechatService(SqlSugarRepository sysWechatUserRep, + SysConfigService sysConfigService, + WechatApiClientFactory wechatApiClientFactory, + SysCacheService sysCacheService) + { + _sysWechatUserRep = sysWechatUserRep; + _sysConfigService = sysConfigService; + _wechatApiClientFactory = wechatApiClientFactory; + _wechatApiClient = wechatApiClientFactory.CreateWechatClient(); + } + + /// + /// 生成网页授权Url 🔖 + /// + /// + /// + [AllowAnonymous] + [DisplayName("生成网页授权Url")] + public string GenAuthUrl(GenAuthUrlInput input) + { + return _wechatApiClient.GenerateParameterizedUrlForConnectOAuth2Authorize(input.RedirectUrl, input.Scope, input.State); + } + + /// + /// 获取微信用户OpenId 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("获取微信用户OpenId")] + public async Task SnsOAuth2([FromQuery] WechatOAuth2Input input) + { + var reqOAuth2 = new SnsOAuth2AccessTokenRequest() + { + Code = input.Code, + }; + var resOAuth2 = await _wechatApiClient.ExecuteSnsOAuth2AccessTokenAsync(reqOAuth2); + if (resOAuth2.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resOAuth2.ErrorMessage + " " + resOAuth2.ErrorCode); + + var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == resOAuth2.OpenId); + if (wxUser == null) + { + var reqUserInfo = new SnsUserInfoRequest() + { + OpenId = resOAuth2.OpenId, + AccessToken = resOAuth2.AccessToken, + }; + var resUserInfo = await _wechatApiClient.ExecuteSnsUserInfoAsync(reqUserInfo); + wxUser = resUserInfo.Adapt(); + wxUser.Avatar = resUserInfo.HeadImageUrl; + wxUser.NickName = resUserInfo.Nickname; + wxUser.OpenId = resOAuth2.OpenId; + wxUser.UnionId = resOAuth2.UnionId; + wxUser.AccessToken = resOAuth2.AccessToken; + wxUser.RefreshToken = resOAuth2.RefreshToken; + wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync(); + } + else + { + wxUser.AccessToken = resOAuth2.AccessToken; + wxUser.RefreshToken = resOAuth2.RefreshToken; + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + } + + return resOAuth2.OpenId; + } + + /// + /// 微信用户登录OpenId 🔖 + /// + /// + /// + [AllowAnonymous] + [DisplayName("微信用户登录OpenId")] + public async Task OpenIdLogin(WechatUserLogin input) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId); + if (wxUser == null) + throw Oops.Oh("微信用户登录OpenId错误"); + + var tokenExpire = await _sysConfigService.GetTokenExpire(); + return new + { + wxUser.Avatar, + accessToken = JWTEncryption.Encrypt(new Dictionary + { + { ClaimConst.UserId, wxUser.Id }, + { ClaimConst.NickName, wxUser.NickName }, + { ClaimConst.LoginMode, LoginModeEnum.APP }, + }, tokenExpire) + }; + } + + /// + /// 获取配置签名参数(wx.config) 🔖 + /// + /// + [DisplayName("获取配置签名参数(wx.config)")] + public async Task GenConfigPara(SignatureInput input) + { + string ticket = await _wechatApiClientFactory.TryGetWechatJsApiTicketAsync(); + return _wechatApiClient.GenerateParametersForJSSDKConfig(ticket, input.Url); + } + + /// + /// 获取模板列表 🔖 + /// + [DisplayName("获取模板列表")] + public async Task GetMessageTemplateList() + { + var accessToken = await GetCgibinToken(); + var reqTemplate = new CgibinTemplateGetAllPrivateTemplateRequest() + { + AccessToken = accessToken + }; + var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateGetAllPrivateTemplateAsync(reqTemplate); + if (resTemplate.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resTemplate.ErrorMessage + " " + resTemplate.ErrorCode); + + return resTemplate.TemplateList; + } + + /// + /// 发送模板消息 🔖 + /// + /// + /// + [DisplayName("发送模板消息")] + public async Task SendTemplateMessage(MessageTemplateSendInput input) + { + var dataInfo = input.Data.ToDictionary(k => k.Key, k => k.Value); + var messageData = new Dictionary(); + foreach (var item in dataInfo) + { + messageData.Add(item.Key, new CgibinMessageTemplateSendRequest.Types.DataItem() { Value = "" + item.Value.Value.ToString() + "" }); + } + + var accessToken = await GetCgibinToken(); + var reqMessage = new CgibinMessageTemplateSendRequest() + { + AccessToken = accessToken, + TemplateId = input.TemplateId, + ToUserOpenId = input.ToUserOpenId, + Url = input.Url, + MiniProgram = new CgibinMessageTemplateSendRequest.Types.MiniProgram + { + AppId = _wechatApiClientFactory._wechatOptions.WxOpenAppId, + PagePath = input.MiniProgramPagePath, + }, + Data = messageData + }; + var resMessage = await _wechatApiClient.ExecuteCgibinMessageTemplateSendAsync(reqMessage); + return resMessage; + } + + /// + /// 删除模板 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "DeleteMessageTemplate"), HttpPost] + [DisplayName("删除模板")] + public async Task DeleteMessageTemplate(DeleteMessageTemplateInput input) + { + var accessToken = await GetCgibinToken(); + var reqMessage = new CgibinTemplateDeletePrivateTemplateRequest() + { + AccessToken = accessToken, + TemplateId = input.TemplateId + }; + var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateDeletePrivateTemplateAsync(reqMessage); + return resTemplate; + } + + /// + /// 获取Access_token + /// + [NonAction] + public async Task GetCgibinToken() + { + return await _wechatApiClientFactory.TryGetWechatAccessTokenAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/SysWechatUserService.cs b/Admin.NET.Core/Service/Wechat/SysWechatUserService.cs new file mode 100644 index 0000000..3e3f38a --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/SysWechatUserService.cs @@ -0,0 +1,73 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 微信账号服务 🧩 +/// +[ApiDescriptionSettings(Order = 220)] +public class SysWechatUserService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysWechatUserRep; + + public SysWechatUserService(SqlSugarRepository sysWechatUserRep) + { + _sysWechatUserRep = sysWechatUserRep; + } + + /// + /// 获取微信用户列表 🔖 + /// + /// + /// + [DisplayName("获取微信用户列表")] + public async Task> Page(WechatUserInput input) + { + return await _sysWechatUserRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.NickName), u => u.NickName.Contains(input.NickName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Mobile), u => u.Mobile.Contains(input.Mobile)) + .OrderBy(u => u.Id, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加微信用户 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加微信用户")] + public async Task AddWechatUser(SysWechatUser input) + { + await _sysWechatUserRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新微信用户 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新微信用户")] + public async Task UpdateWechatUser(SysWechatUser input) + { + var weChatUser = input.Adapt(); + await _sysWechatUserRep.AsUpdateable(weChatUser).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除微信用户 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除微信用户")] + public async Task DeleteWechatUser(DeleteWechatUserInput input) + { + await _sysWechatUserRep.DeleteAsync(u => u.Id == input.Id); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs b/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs new file mode 100644 index 0000000..24e2694 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs @@ -0,0 +1,407 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 微信小程序服务 🧩 +/// +[ApiDescriptionSettings(Order = 240)] +public class SysWxOpenService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysWechatUserRep; + private readonly SysConfigService _sysConfigService; + private readonly WechatApiClient _wechatApiClient; + private readonly SysFileService _sysFileService; + private readonly WechatApiClientFactory _wechatApiClientFactory; + + public SysWxOpenService(SqlSugarRepository sysWechatUserRep, + SysConfigService sysConfigService, + WechatApiClientFactory wechatApiClientFactory, + SysFileService sysFileService) + { + _sysWechatUserRep = sysWechatUserRep; + _sysConfigService = sysConfigService; + _wechatApiClient = wechatApiClientFactory.CreateWxOpenClient(); + _sysFileService = sysFileService; + _wechatApiClientFactory = wechatApiClientFactory; + } + + /// + /// 获取微信用户OpenId 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("获取微信用户OpenId")] + public async Task GetWxOpenId([FromQuery] JsCode2SessionInput input) + { + var reqJsCode2Session = new SnsJsCode2SessionRequest() + { + JsCode = input.JsCode, + }; + var resCode2Session = await _wechatApiClient.ExecuteSnsJsCode2SessionAsync(reqJsCode2Session); + if (resCode2Session.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resCode2Session.ErrorMessage + " " + resCode2Session.ErrorCode); + + var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == resCode2Session.OpenId); + if (wxUser == null) + { + wxUser = new SysWechatUser + { + OpenId = resCode2Session.OpenId, + UnionId = resCode2Session.UnionId, + SessionKey = resCode2Session.SessionKey, + PlatformType = PlatformTypeEnum.微信小程序 + }; + wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync(); + } + else + { + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + } + + return new WxOpenIdOutput + { + OpenId = resCode2Session.OpenId + }; + } + + /// + /// 获取微信用户电话号码 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("获取微信用户电话号码")] + public async Task GetWxPhone([FromQuery] WxPhoneInput input) + { + var accessToken = await GetCgibinToken(); + var reqUserPhoneNumber = new WxaBusinessGetUserPhoneNumberRequest() + { + Code = input.Code, + AccessToken = accessToken, + }; + var resUserPhoneNumber = await _wechatApiClient.ExecuteWxaBusinessGetUserPhoneNumberAsync(reqUserPhoneNumber); + if (resUserPhoneNumber.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resUserPhoneNumber.ErrorMessage + " " + resUserPhoneNumber.ErrorCode); + + var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId); + if (wxUser == null) + { + wxUser = new SysWechatUser + { + OpenId = input.OpenId, + Mobile = resUserPhoneNumber.PhoneInfo?.PhoneNumber, + PlatformType = PlatformTypeEnum.微信小程序 + }; + wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync(); + } + else + { + wxUser.Mobile = resUserPhoneNumber.PhoneInfo?.PhoneNumber; + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + } + + return new WxPhoneOutput + { + PhoneNumber = resUserPhoneNumber.PhoneInfo?.PhoneNumber + }; + } + + /// + /// 微信小程序登录OpenId 🔖 + /// + /// + /// + [AllowAnonymous] + [DisplayName("微信小程序登录OpenId")] + public async Task WxOpenIdLogin(WxOpenIdLoginInput input) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId); + if (wxUser == null) + throw Oops.Oh("微信小程序登录失败"); + + var tokenExpire = await _sysConfigService.GetTokenExpire(); + return new + { + wxUser.Avatar, + accessToken = JWTEncryption.Encrypt(new Dictionary + { + { ClaimConst.UserId, wxUser.Id }, + { ClaimConst.RealName, wxUser.NickName }, + { ClaimConst.LoginMode, LoginModeEnum.APP }, + }, tokenExpire) + }; + } + + /// + /// 上传小程序头像 + /// + /// + /// + [AllowAnonymous] + [DisplayName("上传小程序头像")] + public async Task UploadAvatar([FromForm] UploadAvatarInput input) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId); + if (wxUser == null) + throw Oops.Oh("未找到用户上传失败"); + + var res = await _sysFileService.UploadFile(new UploadFileInput { File = input.File, FileType = input.FileType }, "upload/wechatAvatar"); + wxUser.Avatar = res.Url; + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + + return res; + } + + /// + /// 设置小程序用户昵称 + /// + /// + /// + [AllowAnonymous] + [HttpPost] + public async Task SetNickName(SetNickNameInput input) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId); + if (wxUser == null) + throw Oops.Oh("未找到用户信息设置失败"); + wxUser.NickName = input.NickName; + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + return; + } + + /// + /// 获取小程序用户信息 + /// + /// + /// + [AllowAnonymous] + public async Task GetUserInfo(string openid) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == openid); + if (wxUser == null) + throw Oops.Oh("未找到用户信息获取失败"); + return new { nickName = wxUser.NickName, avator = wxUser.Avatar }; + } + + /// + /// 获取订阅消息模板列表 🔖 + /// + [DisplayName("获取订阅消息模板列表")] + public async Task GetMessageTemplateList() + { + var accessToken = await GetCgibinToken(); + var reqTemplate = new WxaApiNewTemplateGetTemplateRequest() + { + AccessToken = accessToken + }; + var resTemplate = await _wechatApiClient.ExecuteWxaApiNewTemplateGetTemplateAsync(reqTemplate); + if (resTemplate.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resTemplate.ErrorMessage + " " + resTemplate.ErrorCode); + + return resTemplate.TemplateList; + } + + /// + /// 发送订阅消息 🔖 + /// + /// + /// + [DisplayName("发送订阅消息")] + public async Task SendSubscribeMessage(SendSubscribeMessageInput input) + { + var accessToken = await GetCgibinToken(); + var reqMessage = new CgibinMessageSubscribeSendRequest() + { + AccessToken = accessToken, + TemplateId = input.TemplateId, + ToUserOpenId = input.ToUserOpenId, + Data = input.Data, + MiniProgramState = input.MiniprogramState, + Language = input.Language, + MiniProgramPagePath = input.MiniProgramPagePath + }; + var resMessage = await _wechatApiClient.ExecuteCgibinMessageSubscribeSendAsync(reqMessage); + return resMessage; + } + + /// + /// 增加订阅消息模板 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "AddSubscribeMessageTemplate"), HttpPost] + [DisplayName("增加订阅消息模板")] + public async Task AddSubscribeMessageTemplate(AddSubscribeMessageTemplateInput input) + { + var accessToken = await GetCgibinToken(); + var reqMessage = new WxaApiNewTemplateAddTemplateRequest() + { + AccessToken = accessToken, + TemplateTitleId = input.TemplateTitleId, + KeyworkIdList = input.KeyworkIdList, + SceneDescription = input.SceneDescription + }; + var resTemplate = await _wechatApiClient.ExecuteWxaApiNewTemplateAddTemplateAsync(reqMessage); + return resTemplate; + } + + /// + /// 生成带参数小程序二维码(总共生成的码数量限制为 100,000) + /// + /// 扫码进入的小程序页面路径,最大长度 128 个字符,不能为空; eg: pages / index ? id = AY000001 + /// + [DisplayName("生成小程序二维码")] + [ApiDescriptionSettings(Name = "GenerateQRImage")] + public async Task GenerateQRImageAsync(GenerateQRImageInput input) + { + GenerateQRImageOutput generateQRImageOutInput = new GenerateQRImageOutput(); + if (input.PagePath.IsNullOrEmpty()) + { + generateQRImageOutInput.Message = $"生成失败 页面路径不能为空"; + return generateQRImageOutInput; + } + + if (input.ImageName.IsNullOrEmpty()) + { + input.ImageName = DateTime.Now.ToString("yyyyMMddHHmmss"); + } + + var accessToken = await GetCgibinToken(); + var request = new CgibinWxaappCreateWxaQrcodeRequest + { + AccessToken = accessToken, + Path = input.PagePath, + Width = input.Width + }; + var response = await _wechatApiClient.ExecuteCgibinWxaappCreateWxaQrcodeAsync(request); + + if (response.IsSuccessful()) + { + var QRImagePath = App.GetConfig("Wechat:QRImagePath"); + var relativeImgPath = string.Empty; + + // 判断路径是绝对路径还是相对路径 + var isPathRooted = Path.IsPathRooted(QRImagePath); + if (!isPathRooted) + { + // 相对路径 + relativeImgPath = string.IsNullOrEmpty(QRImagePath) ? Path.Combine("upload", "QRImage") : QRImagePath; + QRImagePath = Path.Combine(App.WebHostEnvironment.WebRootPath, relativeImgPath); + } + + //判断文件存放路径是否存在 + if (!Directory.Exists(QRImagePath)) + { + Directory.CreateDirectory(QRImagePath); + } + // 将二维码图片数据保存为文件 + var fileName = $"{input.ImageName.ToUpper()}.png"; + var filePath = Path.Combine(QRImagePath, fileName); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + File.WriteAllBytes(filePath, response.GetRawBytes()); + + generateQRImageOutInput.Success = true; + generateQRImageOutInput.ImgPath = filePath; + generateQRImageOutInput.RelativeImgPath = Path.Combine(relativeImgPath, fileName); + generateQRImageOutInput.Message = "生成成功"; + } + else + { + // 处理错误情况 + generateQRImageOutInput.Message = $"生成失败 错误代码:{response.ErrorCode} 错误描述:{response.ErrorMessage}"; + } + return generateQRImageOutInput; + } + + /// + /// 生成二维码(获取不受限制的小程序码) + /// + /// 入参 + /// + [DisplayName("生成小程序二维码")] + [ApiDescriptionSettings(Name = "GenerateQRImageUnlimit")] + public async Task GenerateQRImageUnlimitAsync(GenerateQRImageUnLimitInput input) + { + GenerateQRImageOutput generateQRImageOutInput = new GenerateQRImageOutput(); + if (input.PagePath.IsNullOrEmpty()) + { + generateQRImageOutInput.Message = $"生成失败,页面路径不能为空"; + return generateQRImageOutInput; + } + + if (input.Scene.Length > 32) + { + generateQRImageOutInput.Message = $"生成失败,携带的参数长度超过限制"; + return generateQRImageOutInput; + } + + if (input.ImageName.IsNullOrEmpty()) + { + input.ImageName = DateTime.Now.ToString("yyyyMMddHHmmss"); + } + + var accessToken = await GetCgibinToken(); + var request = new WxaGetWxaCodeRequest + { + AccessToken = accessToken, + Width = input.Width, + PagePath = input.PagePath, + }; + var response = await _wechatApiClient.ExecuteWxaGetWxaCodeAsync(request); + + if (response.IsSuccessful()) + { + var QRImagePath = App.GetConfig("Wechat:QRImagePath"); + var relativeImgPath = string.Empty; + + // 判断路径是绝对路径还是相对路径 + var isPathRooted = Path.IsPathRooted(QRImagePath); + if (!isPathRooted) + { + // 相对路径 + relativeImgPath = string.IsNullOrEmpty(QRImagePath) ? Path.Combine("upload", "QRImageUnLimit") : QRImagePath; + QRImagePath = Path.Combine(App.WebHostEnvironment.WebRootPath, relativeImgPath); + } + + //判断文件存放路径是否存在 + if (!Directory.Exists(QRImagePath)) + { + Directory.CreateDirectory(QRImagePath); + } + // 将二维码图片数据保存为文件 + var fileName = $"{input.ImageName.ToUpper()}.png"; + var filePath = Path.Combine(QRImagePath, fileName); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + File.WriteAllBytes(filePath, response.GetRawBytes()); + + generateQRImageOutInput.Success = true; + generateQRImageOutInput.ImgPath = filePath; + generateQRImageOutInput.RelativeImgPath = Path.Combine(relativeImgPath, fileName); + generateQRImageOutInput.Message = "生成成功"; + } + else + { + // 处理错误情况 + generateQRImageOutInput.Message = $"生成失败 错误代码:{response.ErrorCode} 错误描述:{response.ErrorMessage}"; + } + return generateQRImageOutInput; + } + + /// + /// 获取Access_token + /// + private async Task GetCgibinToken() + { + return await _wechatApiClientFactory.TryGetWxOpenAccessTokenAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Service/Wechat/WechatApiHttpClient.cs b/Admin.NET.Core/Service/Wechat/WechatApiHttpClient.cs new file mode 100644 index 0000000..ae9a175 --- /dev/null +++ b/Admin.NET.Core/Service/Wechat/WechatApiHttpClient.cs @@ -0,0 +1,208 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core.Service; + +/// +/// 微信API客户端 +/// +public partial class WechatApiClientFactory : ISingleton +{ + private readonly IHttpClientFactory _httpClientFactory; + public readonly WechatOptions _wechatOptions; + private readonly SysCacheService _sysCacheService; + + public WechatApiClientFactory(IHttpClientFactory httpClientFactory, IOptions wechatOptions, SysCacheService sysCacheService) + { + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + _wechatOptions = wechatOptions.Value ?? throw new ArgumentNullException(nameof(wechatOptions)); + _sysCacheService = sysCacheService; + } + + /// + /// 微信公众号 + /// + /// + public WechatApiClient CreateWechatClient() + { + if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret)) + throw Oops.Oh("微信公众号配置错误"); + + var client = WechatApiClientBuilder.Create(new WechatApiClientOptions() + { + AppId = _wechatOptions.WechatAppId, + AppSecret = _wechatOptions.WechatAppSecret, + PushToken = _wechatOptions.WechatToken, + PushEncodingAESKey = _wechatOptions.WechatEncodingAESKey, + }) + .UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁 + .Build(); + + client.Configure(config => + { + JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings(); + jsonSerializerSettings.Formatting = Formatting.Indented; + config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化 + // config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json JSON序列化 + }); + + return client; + } + + /// + /// 微信小程序 + /// + /// + public WechatApiClient CreateWxOpenClient() + { + if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret)) + throw Oops.Oh("微信小程序配置错误"); + + var client = WechatApiClientBuilder.Create(new WechatApiClientOptions() + { + AppId = _wechatOptions.WxOpenAppId, + AppSecret = _wechatOptions.WxOpenAppSecret, + PushToken = _wechatOptions.WxToken, + PushEncodingAESKey = _wechatOptions.WxEncodingAESKey, + }) + .UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁 + .Build(); + + client.Configure(config => + { + JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings(); + jsonSerializerSettings.Formatting = Formatting.Indented; + config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化 + // config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json JSON序列化 + }); + + return client; + } + + /// + /// 获取微信公众号AccessToken + /// + /// + public async Task TryGetWechatAccessTokenAsync() + { + if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get($"WxAccessToken_{_wechatOptions.WechatAppId}"))) + { + var client = CreateWechatClient(); + var reqCgibinToken = new CgibinTokenRequest(); + var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken); + if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode); + _sysCacheService.Set($"WxAccessToken_{_wechatOptions.WechatAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60)); + } + + return _sysCacheService.Get($"WxAccessToken_{_wechatOptions.WechatAppId}"); + } + + /// + /// 获取微信小程序AccessToken + /// + /// + public async Task TryGetWxOpenAccessTokenAsync() + { + if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WxOpenAppId}") || string.IsNullOrEmpty(_sysCacheService.Get($"WxAccessToken_{_wechatOptions.WxOpenAppId}"))) + { + var client = CreateWxOpenClient(); + var reqCgibinToken = new CgibinTokenRequest(); + var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken); + if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode); + _sysCacheService.Set($"WxAccessToken_{_wechatOptions.WxOpenAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60)); + } + + return _sysCacheService.Get($"WxAccessToken_{_wechatOptions.WxOpenAppId}"); + } + + /// + /// 检查微信公众号AccessToken + /// + /// + public async Task CheckWechatAccessTokenAsync() + { + if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret)) return; + + var req = new CgibinOpenApiQuotaGetRequest + { + AccessToken = await TryGetWechatAccessTokenAsync(), + CgiPath = "/cgi-bin/token" + }; + var client = CreateWechatClient(); + var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req); + + var originColor = Console.ForegroundColor; + if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + { + _sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WechatAppId}"); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 无效"); + } + else + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 有效"); + } + Console.ForegroundColor = originColor; + } + + /// + /// 检查微信小程序AccessToken + /// + /// + public async Task CheckWxOpenAccessTokenAsync() + { + if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret)) return; + + var req = new CgibinOpenApiQuotaGetRequest + { + AccessToken = await TryGetWxOpenAccessTokenAsync(), + CgiPath = "/cgi-bin/token" + }; + var client = CreateWxOpenClient(); + var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req); + + var originColor = Console.ForegroundColor; + if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + { + _sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WxOpenAppId}"); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 无效"); + } + else + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 有效"); + } + Console.ForegroundColor = originColor; + } + + /// + /// 获取微信JS接口临时票据jsapi_ticket + /// + /// + public async Task TryGetWechatJsApiTicketAsync() + { + if (!_sysCacheService.ExistKey($"WxJsApiTicket_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get($"WxJsApiTicket_{_wechatOptions.WechatAppId}"))) + { + var accessToken = await TryGetWechatAccessTokenAsync(); + var client = CreateWechatClient(); + var request = new CgibinTicketGetTicketRequest() + { + AccessToken = accessToken + }; + var response = await client.ExecuteCgibinTicketGetTicketAsync(request); + if (!response.IsSuccessful()) + throw Oops.Oh(response.ErrorMessage + " " + response.ErrorCode); + _sysCacheService.Set($"WxJsApiTicket_{_wechatOptions.WechatAppId}", response.Ticket, TimeSpan.FromSeconds(response.ExpiresIn - 60)); + } + return _sysCacheService.Get($"WxJsApiTicket_{_wechatOptions.WechatAppId}"); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SignalR/SignalRSetup.cs b/Admin.NET.Core/SignalR/SignalRSetup.cs new file mode 100644 index 0000000..7029ac3 --- /dev/null +++ b/Admin.NET.Core/SignalR/SignalRSetup.cs @@ -0,0 +1,104 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.Logging.Extensions; +using Microsoft.AspNetCore.DataProtection; +using Newtonsoft.Json; +using StackExchange.Redis; + +namespace Admin.NET.Core; + +public static class SignalRSetup +{ + /// + /// 即时消息SignalR注册 + /// + /// + /// + public static void AddSignalR(this IServiceCollection services, Action SetNewtonsoftJsonSetting) + { + var signalRBuilder = services.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.ClientTimeoutInterval = TimeSpan.FromMinutes(2); + options.KeepAliveInterval = TimeSpan.FromMinutes(1); + options.MaximumReceiveMessageSize = 1024 * 1024 * 10; // 数据包大小10M,默认最大为32K + }).AddNewtonsoftJsonProtocol(options => SetNewtonsoftJsonSetting(options.PayloadSerializerSettings)); + + // 若未启用Redis缓存,直接返回 + var cacheOptions = App.GetConfig("Cache", true); + if (cacheOptions.CacheType != CacheTypeEnum.Redis.ToString()) + return; + + // 若已开启集群配置,则把SignalR配置为支持集群模式 + var clusterOpt = App.GetConfig("Cluster", true); + if (!clusterOpt.Enabled) + return; + + var redisOptions = clusterOpt.SentinelConfig; + ConnectionMultiplexer connection1; + if (clusterOpt.IsSentinel) // 哨兵模式 + { + var redisConfig = new ConfigurationOptions + { + AbortOnConnectFail = false, + ServiceName = redisOptions.ServiceName, + AllowAdmin = true, + DefaultDatabase = redisOptions.DefaultDb, + Password = redisOptions.Password + }; + redisOptions.EndPoints.ForEach(u => redisConfig.EndPoints.Add(u)); + connection1 = ConnectionMultiplexer.Connect(redisConfig); + } + else + { + connection1 = ConnectionMultiplexer.Connect(clusterOpt.SignalR.RedisConfiguration); + } + // 密钥存储(数据保护) + services.AddDataProtection().PersistKeysToStackExchangeRedis(connection1, clusterOpt.DataProtecteKey); + + signalRBuilder.AddStackExchangeRedis(options => + { + // 此处设置的ChannelPrefix并不会生效,如果两个不同的项目,且[程序集名+类名]一样,使用同一个redis服务,请注意修改 Hub/OnlineUserHub 的类名。 + // 原因请参考下边链接: + // https://github.com/dotnet/aspnetcore/blob/f9121bc3e976ec40a959818451d126d5126ce868/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs#L74 + // https://github.com/dotnet/aspnetcore/blob/f9121bc3e976ec40a959818451d126d5126ce868/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs#L33 + options.Configuration.ChannelPrefix = new RedisChannel(clusterOpt.SignalR.ChannelPrefix, RedisChannel.PatternMode.Auto); + options.ConnectionFactory = async writer => + { + ConnectionMultiplexer connection; + if (clusterOpt.IsSentinel) + { + var config = new ConfigurationOptions + { + AbortOnConnectFail = false, + ServiceName = redisOptions.ServiceName, + AllowAdmin = true, + DefaultDatabase = redisOptions.DefaultDb, + Password = redisOptions.Password + }; + redisOptions.EndPoints.ForEach(u => config.EndPoints.Add(u)); + connection = await ConnectionMultiplexer.ConnectAsync(config, writer); + } + else + { + connection = await ConnectionMultiplexer.ConnectAsync(clusterOpt.SignalR.RedisConfiguration); + } + + connection.ConnectionFailed += (_, e) => + { + "连接 Redis 失败".LogError(); + }; + + if (!connection.IsConnected) + { + "无法连接 Redis".LogError(); + } + return connection; + }; + }); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SignatureAuth/GetAccessSecretContext.cs b/Admin.NET.Core/SignatureAuth/GetAccessSecretContext.cs new file mode 100644 index 0000000..1708cf3 --- /dev/null +++ b/Admin.NET.Core/SignatureAuth/GetAccessSecretContext.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// 获取 AccessKey 关联 AccessSecret 方法的上下文 +/// +public class GetAccessSecretContext : BaseContext +{ + public GetAccessSecretContext(HttpContext context, + AuthenticationScheme scheme, + SignatureAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// + /// 身份标识 + /// + public string AccessKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/SignatureAuth/SignatureAuthenticationDefaults.cs b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationDefaults.cs new file mode 100644 index 0000000..ce42ff7 --- /dev/null +++ b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationDefaults.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证处理程序相关的默认值 +/// +public static class SignatureAuthenticationDefaults +{ + /// + /// SignatureAuthenticationOptions.AuthenticationScheme 使用的默认值 + /// + public const string AuthenticationScheme = "Signature"; + + /// + /// 附加在 HttpContext Item 中验证失败消息的 Key + /// + public const string AuthenticateFailMsgKey = "SignatureAuthenticateFailMsg"; +} \ No newline at end of file diff --git a/Admin.NET.Core/SignatureAuth/SignatureAuthenticationEvent.cs b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationEvent.cs new file mode 100644 index 0000000..281ced8 --- /dev/null +++ b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationEvent.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证事件 +/// +public class SignatureAuthenticationEvent +{ + public SignatureAuthenticationEvent() + { + } + + /// + /// 获取或设置获取 AccessKey 的 AccessSecret 的逻辑处理 + /// + public Func> OnGetAccessSecret { get; set; } + + /// + /// 获取或设置质询的逻辑处理 + /// + public Func OnChallenge { get; set; } = _ => Task.CompletedTask; + + /// + /// 获取或设置已验证的逻辑处理 + /// + public Func OnValidated { get; set; } = _ => Task.CompletedTask; + + /// + /// 获取 AccessKey 的 AccessSecret + /// + /// + /// + public virtual Task GetAccessSecret(GetAccessSecretContext context) => OnGetAccessSecret?.Invoke(context) ?? throw new NotImplementedException($"需要提供 {nameof(OnGetAccessSecret)} 实现"); + + /// + /// 质询 + /// + /// + /// + public virtual Task Challenge(SignatureChallengeContext context) => OnChallenge?.Invoke(context) ?? Task.CompletedTask; + + /// + /// 已验证成功 + /// + /// + /// + public virtual Task Validated(SignatureValidatedContext context) => OnValidated?.Invoke(context) ?? Task.CompletedTask; +} \ No newline at end of file diff --git a/Admin.NET.Core/SignatureAuth/SignatureAuthenticationExtensions.cs b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationExtensions.cs new file mode 100644 index 0000000..3910548 --- /dev/null +++ b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationExtensions.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证扩展 +/// +public static class SignatureAuthenticationExtensions +{ + /// + /// 注册 Signature 身份验证处理模块 + /// + /// + /// + public static AuthenticationBuilder AddSignatureAuthentication(this AuthenticationBuilder builder) + { + return builder.AddSignatureAuthentication(options => { }); + } + + /// + /// 注册 Signature 身份验证处理模块 + /// + /// + /// + /// + public static AuthenticationBuilder AddSignatureAuthentication(this AuthenticationBuilder builder, Action options) + { + return builder.AddScheme(SignatureAuthenticationDefaults.AuthenticationScheme, options); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs new file mode 100644 index 0000000..9db338c --- /dev/null +++ b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs @@ -0,0 +1,177 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text.Encodings.Web; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证处理 +/// +public sealed class SignatureAuthenticationHandler : AuthenticationHandler +{ +#if NET6_0 + + public SignatureAuthenticationHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + +#else + + public SignatureAuthenticationHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + +#endif + + private readonly SysCacheService _sysCacheService = App.GetRequiredService(); + + private new SignatureAuthenticationEvent Events + { + get => (SignatureAuthenticationEvent)base.Events; + set => base.Events = value; + } + + /// + /// 确保创建的 Event 类型是 DigestEvents + /// + /// + protected override Task CreateEventsAsync() => throw new NotImplementedException($"{nameof(SignatureAuthenticationOptions)}.{nameof(SignatureAuthenticationOptions.Events)} 需要提供一个实例"); + + protected override async Task HandleAuthenticateAsync() + { + var accessKey = Request.Headers["accessKey"].FirstOrDefault(); + var timestampStr = Request.Headers["timestamp"].FirstOrDefault(); // 精确到秒 + var nonce = Request.Headers["nonce"].FirstOrDefault(); + var sign = Request.Headers["sign"].FirstOrDefault(); + + if (string.IsNullOrEmpty(accessKey)) + return await AuthenticateResultFailAsync("accessKey 不能为空"); + if (string.IsNullOrEmpty(timestampStr)) + return await AuthenticateResultFailAsync("timestamp 不能为空"); + if (string.IsNullOrEmpty(nonce)) + return await AuthenticateResultFailAsync("nonce 不能为空"); + if (string.IsNullOrEmpty(sign)) + return await AuthenticateResultFailAsync("sign 不能为空"); + + // 验证请求数据是否在可接受的时间内 + if (!long.TryParse(timestampStr, out var timestamp)) + return await AuthenticateResultFailAsync("timestamp 值不合法"); + + var requestDate = DateTimeUtil.ConvertUnixTime(timestamp); + +#if NET6_0 + var utcNow = Clock.UtcNow; +#else + var utcNow = TimeProvider.GetUtcNow(); +#endif + if (requestDate > utcNow.Add(Options.AllowedDateDrift).LocalDateTime || requestDate < utcNow.Subtract(Options.AllowedDateDrift).LocalDateTime) + return await AuthenticateResultFailAsync("timestamp 值已超过允许的偏差范围"); + + // 获取 accessSecret + var getAccessSecretContext = new GetAccessSecretContext(Context, Scheme, Options) { AccessKey = accessKey }; + var accessSecret = await Events.GetAccessSecret(getAccessSecretContext); + if (string.IsNullOrEmpty(accessSecret)) + return await AuthenticateResultFailAsync("accessKey 无效"); + + // 校验签名 + var appSecretByte = Encoding.UTF8.GetBytes(accessSecret); + string serverSign = SignData(appSecretByte, GetMessageForSign(Context)); + + if (serverSign != sign) + return await AuthenticateResultFailAsync("sign 无效的签名"); + + // 重放检测 + var cacheKey = $"{CacheConst.KeyOpenAccessNonce}{accessKey}|{nonce}"; + if (_sysCacheService.ExistKey(cacheKey)) return await AuthenticateResultFailAsync("重复的请求"); + _sysCacheService.Set(cacheKey, null, Options.AllowedDateDrift * 2); // 缓存过期时间为偏差范围时间的2倍 + + // 已验证成功 + var signatureValidatedContext = new SignatureValidatedContext(Context, Scheme, Options) + { + Principal = new ClaimsPrincipal(new ClaimsIdentity(SignatureAuthenticationDefaults.AuthenticationScheme)), + AccessKey = accessKey + }; + await Events.Validated(signatureValidatedContext); + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (signatureValidatedContext.Result != null) + return signatureValidatedContext.Result; + + // ReSharper disable once HeuristicUnreachableCode + signatureValidatedContext.Success(); + return signatureValidatedContext.Result; + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + var authResult = await HandleAuthenticateOnceSafeAsync(); + var challengeContext = new SignatureChallengeContext(Context, Scheme, Options, properties) + { + AuthenticateFailure = authResult.Failure, + }; + await Events.Challenge(challengeContext); + // 质询已处理 + if (challengeContext.Handled) return; + + await base.HandleChallengeAsync(properties); + } + + /// + /// 获取用于签名的消息 + /// + /// + private static string GetMessageForSign(HttpContext context) + { + var method = context.Request.Method; // 请求方法(大写) + var url = context.Request.Path; // 请求 url,去除协议、域名、参数,以 / 开头 + var accessKey = context.Request.Headers["accessKey"].FirstOrDefault(); // 身份标识 + var timestamp = context.Request.Headers["timestamp"].FirstOrDefault(); // 时间戳,精确到秒 + var nonce = context.Request.Headers["nonce"].FirstOrDefault(); // 唯一随机数 + + return $"{method}&{url}&{accessKey}&{timestamp}&{nonce}"; + } + + /// + /// 对数据进行签名 + /// + /// + /// + /// + private static string SignData(byte[] secret, string data) + { + if (secret == null) + throw new ArgumentNullException(nameof(secret)); + + if (data == null) + throw new ArgumentNullException(nameof(data)); + + using HMAC hmac = new HMACSHA256(); + hmac.Key = secret; + return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(data))); + } + + /// + /// 返回验证失败结果,并在 Items 中增加 ,记录身份验证失败消息 + /// + /// + /// + private Task AuthenticateResultFailAsync(string message) + { + // 写入身份验证失败消息 + Context.Items[SignatureAuthenticationDefaults.AuthenticateFailMsgKey] = message; + return Task.FromResult(AuthenticateResult.Fail(message)); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SignatureAuth/SignatureAuthenticationOptions.cs b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationOptions.cs new file mode 100644 index 0000000..9b91ba2 --- /dev/null +++ b/Admin.NET.Core/SignatureAuth/SignatureAuthenticationOptions.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证选项 +/// +public class SignatureAuthenticationOptions : AuthenticationSchemeOptions +{ + /// + /// 请求时间允许的偏差范围 + /// + public TimeSpan AllowedDateDrift { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Signature 身份验证事件 + /// + public new SignatureAuthenticationEvent Events + { + get => (SignatureAuthenticationEvent)base.Events; + set => base.Events = value; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SignatureAuth/SignatureChallengeContext.cs b/Admin.NET.Core/SignatureAuth/SignatureChallengeContext.cs new file mode 100644 index 0000000..a975bd4 --- /dev/null +++ b/Admin.NET.Core/SignatureAuth/SignatureChallengeContext.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证质询上下文 +/// +public class SignatureChallengeContext : PropertiesContext +{ + public SignatureChallengeContext(HttpContext context, + AuthenticationScheme scheme, + SignatureAuthenticationOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) + { + } + + /// + /// 在认证期间出现的异常 + /// + public Exception AuthenticateFailure { get; set; } + + /// + /// 指定是否已被处理,如果已处理,则跳过默认认证逻辑 + /// + public bool Handled { get; private set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/SignatureAuth/SignatureValidatedContext.cs b/Admin.NET.Core/SignatureAuth/SignatureValidatedContext.cs new file mode 100644 index 0000000..7bad7e3 --- /dev/null +++ b/Admin.NET.Core/SignatureAuth/SignatureValidatedContext.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证已验证上下文 +/// +public class SignatureValidatedContext : ResultContext +{ + public SignatureValidatedContext(HttpContext context, + AuthenticationScheme scheme, + SignatureAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// + /// 身份标识 + /// + public string AccessKey { get; set; } + + /// + /// 密钥 + /// + public string AccessSecret { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/ISqlSugarEntitySeedData.cs b/Admin.NET.Core/SqlSugar/ISqlSugarEntitySeedData.cs new file mode 100644 index 0000000..bad2faa --- /dev/null +++ b/Admin.NET.Core/SqlSugar/ISqlSugarEntitySeedData.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 实体种子数据接口 +/// +/// +public interface ISqlSugarEntitySeedData + where TEntity : class, new() +{ + /// + /// 种子数据 + /// + /// + IEnumerable HasData(); +} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/ISqlSugarRepository.cs b/Admin.NET.Core/SqlSugar/ISqlSugarRepository.cs new file mode 100644 index 0000000..63fae4f --- /dev/null +++ b/Admin.NET.Core/SqlSugar/ISqlSugarRepository.cs @@ -0,0 +1,91 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 分表操作仓储接口 +/// +/// +public interface ISqlSugarRepository : ISugarRepository, ISimpleClient where T : class, new() +{ + /// + /// 创建数据 + /// + /// + /// + Task SplitTableInsertAsync(T input); + + /// + /// 批量创建数据 + /// + /// + /// + Task SplitTableInsertAsync(List input); + + /// + /// 更新数据 + /// + /// + /// + Task SplitTableUpdateAsync(T input); + + /// + /// 批量更新数据 + /// + /// + /// + Task SplitTableUpdateAsync(List input); + + /// + /// 删除数据 + /// + /// + /// + Task SplitTableDeleteableAsync(T input); + + /// + /// 批量删除数据 + /// + /// + /// + Task SplitTableDeleteableAsync(List input); + + /// + /// 获取第一条 + /// + /// + /// + Task SplitTableGetFirstAsync(Expression> whereExpression); + + /// + /// 判断是否存在 + /// + /// + /// + Task SplitTableIsAnyAsync(Expression> whereExpression); + + /// + /// 获取列表 + /// + /// + Task> SplitTableGetListAsync(); + + /// + /// 获取列表 + /// + /// + /// + Task> SplitTableGetListAsync(Expression> whereExpression); + + /// + /// 获取列表 + /// + /// + /// 表名 + /// + Task> SplitTableGetListAsync(Expression> whereExpression, string[] tableNames); +} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/ISqlSugarView.cs b/Admin.NET.Core/SqlSugar/ISqlSugarView.cs new file mode 100644 index 0000000..299c1a8 --- /dev/null +++ b/Admin.NET.Core/SqlSugar/ISqlSugarView.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 视图实体接口 +/// +public interface ISqlSugarView +{ + /// + /// 获取视图查询sql语句 + /// + /// + /// + public string GetQueryableSqlString(SqlSugarScopeProvider db); +} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs b/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs new file mode 100644 index 0000000..78c41ba --- /dev/null +++ b/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs @@ -0,0 +1,205 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class SqlSugarFilter +{ + /// + /// 缓存全局查询过滤器(内存缓存) + /// + private static readonly ICache Cache = NewLife.Caching.Cache.Default; + + private static readonly SysOrgService SysOrgService = App.GetRequiredService(); + private static readonly SysCacheService SysCacheService = App.GetRequiredService(); + + /// + /// 删除用户机构缓存 + /// + /// + /// + public static void DeleteUserOrgCache(long userId, string dbConfigId) + { + // 删除用户机构集合缓存 + SysCacheService.Remove($"{CacheConst.KeyUserOrg}{userId}"); + // 删除最大数据权限缓存 + SysCacheService.Remove($"{CacheConst.KeyRoleMaxDataScope}{userId}"); + // 用户权限缓存(按钮集合) + SysCacheService.Remove($"{CacheConst.KeyUserButton}{userId}"); + } + + /// + /// 删除自定义过滤器缓存 + /// + /// + /// + public static void DeleteCustomCache(long userId, string dbConfigId) + { + // 删除自定义缓存——过滤器 + Cache.Remove($"db:{dbConfigId}:custom:{userId}"); + } + + /// + /// 配置用户机构集合过滤器 + /// + public static void SetOrgEntityFilter(SqlSugarScopeProvider db) + { + // 若仅本人数据,则直接返回 + var maxDataScope = SetDataScopeFilter(db); + // 获取用户最大数据范围,如果是全部数据、仅本人,则跳过 + if (maxDataScope is 0 or (int)DataScopeEnum.Self or (int)DataScopeEnum.All) return; + + // 获取用户所属机构,保证同一作用域 + var orgIds = new List(); + Scoped.Create((factory, scope) => + { + var services = scope.ServiceProvider; + orgIds = services.GetRequiredService().GetUserOrgIdList().GetAwaiter().GetResult(); + }); + if (orgIds == null || orgIds.Count == 0) return; + + //配置机构Id过滤器 + db.QueryFilter.AddTableFilter(o => SqlFunc.ContainsArray(orgIds, o.OrgId)); + } + + /// + /// 配置用户仅本人数据过滤器 + /// + private static int SetDataScopeFilter(SqlSugarScopeProvider db) + { + var maxDataScope = (int)DataScopeEnum.All; + + long.TryParse(App.HttpContext?.User.FindFirst(ClaimConst.UserId)?.Value, out var userId); + if (userId <= 0) return maxDataScope; + + // 获取用户最大数据范围---仅本人数据 + maxDataScope = App.GetRequiredService().Get(CacheConst.KeyRoleMaxDataScope + userId); + // 若为0则获取用户机构组织集合建立缓存 + if (maxDataScope == 0) + { + // 获取用户所属机构,保证同一作用域 + Scoped.Create((factory, scope) => + { + SysOrgService.GetUserOrgIdList().GetAwaiter().GetResult(); + maxDataScope = SysCacheService.Get(CacheConst.KeyRoleMaxDataScope + userId); + }); + } + if (maxDataScope != (int)DataScopeEnum.Self) return maxDataScope; + + // 配置用户数据范围缓存 + var cacheKey = $"db:{db.CurrentConnectionConfig.ConfigId}:dataScope:{userId}"; + var dataScopeFilter = Cache.Get>(cacheKey); + if (dataScopeFilter == null) + { + // 获取业务实体数据表 + var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass + && (u.IsSubclassOf(typeof(EntityBaseOrg)) || u.IsSubclassOf(typeof(EntityBaseOrgDel)))); + if (!entityTypes.Any()) return maxDataScope; + + dataScopeFilter = new ConcurrentDictionary(); + foreach (var entityType in entityTypes) + { + // 排除非当前数据库实体 + var tAtt = entityType.GetCustomAttribute(); + if ((tAtt != null && db.CurrentConnectionConfig.ConfigId.ToString() != tAtt.configId.ToString())) + continue; + + //var lambda = DynamicExpressionParser.ParseLambda(new[] { + // Expression.Parameter(entityType, "u") }, typeof(bool), $"u.{nameof(EntityBaseData.CreateUserId)}=@0", userId); + var lambda = entityType.GetConditionExpression(new List { userId }); + + db.QueryFilter.AddTableFilter(entityType, lambda); + dataScopeFilter.TryAdd(entityType, lambda); + } + Cache.Add(cacheKey, dataScopeFilter); + } + else + { + foreach (var filter in dataScopeFilter) + db.QueryFilter.AddTableFilter(filter.Key, filter.Value); + } + return maxDataScope; + } + + /// + /// 配置自定义过滤器 + /// + public static void SetCustomEntityFilter(SqlSugarScopeProvider db) + { + // 配置自定义缓存 + var userId = App.User?.FindFirst(ClaimConst.UserId)?.Value; + var cacheKey = $"db:{db.CurrentConnectionConfig.ConfigId}:custom:{userId}"; + var tableFilterItemList = Cache.Get>>(cacheKey); + if (tableFilterItemList == null) + { + // 获取自定义实体过滤器 + var entityFilterTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass + && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(IEntityFilter)))); + if (!entityFilterTypes.Any()) return; + + var tableFilterItems = new List>(); + foreach (var entityFilter in entityFilterTypes) + { + var instance = Activator.CreateInstance(entityFilter); + var entityFilterMethod = entityFilter.GetMethod("AddEntityFilter"); + var entityFilters = ((IList)entityFilterMethod?.Invoke(instance, null))?.Cast(); + if (entityFilters == null) continue; + + foreach (var u in entityFilters) + { + var tableFilterItem = (TableFilterItem)u; + var entityType = tableFilterItem.GetType().GetProperty("type", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(tableFilterItem, null) as Type; + // 排除非当前数据库实体 + var tAtt = entityType.GetCustomAttribute(); + if ((tAtt != null && db.CurrentConnectionConfig.ConfigId.ToString() != tAtt.configId.ToString()) || + (tAtt == null && db.CurrentConnectionConfig.ConfigId.ToString() != SqlSugarConst.MainConfigId)) + continue; + + tableFilterItems.Add(tableFilterItem); + db.QueryFilter.Add(tableFilterItem); + } + } + Cache.Add(cacheKey, tableFilterItems); + } + else + { + tableFilterItemList.ForEach(u => + { + db.QueryFilter.Add(u); + }); + } + } +} + +/// +/// 自定义实体过滤器接口 +/// +public interface IEntityFilter +{ + /// + /// 实体过滤器 + /// + /// + IEnumerable> AddEntityFilter(); +} + +///// +///// 自定义业务实体过滤器示例 +///// +//public class TestEntityFilter : IEntityFilter +//{ +// public IEnumerable> AddEntityFilter() +// { +// // 构造自定义条件的过滤器 +// Expression> dynamicExpression = u => u.Remark.Contains("xxx"); +// var tableFilterItem = new TableFilterItem(typeof(SysUser), dynamicExpression); + +// return new[] +// { +// tableFilterItem +// }; +// } +//} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs b/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs new file mode 100644 index 0000000..009b581 --- /dev/null +++ b/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs @@ -0,0 +1,183 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 分页泛型集合 +/// +/// +public class SqlSugarPagedList +{ + /// + /// 页码 + /// + public int Page { get; set; } + + /// + /// 页容量 + /// + public int PageSize { get; set; } + + /// + /// 总条数 + /// + public int Total { get; set; } + + /// + /// 总页数 + /// + public int TotalPages { get; set; } + + /// + /// 当前页集合 + /// + public IEnumerable Items { get; set; } + + /// + /// 是否有上一页 + /// + public bool HasPrevPage { get; set; } + + /// + /// 是否有下一页 + /// + public bool HasNextPage { get; set; } +} + +/// +/// 分页拓展类 +/// +public static class SqlSugarPagedExtensions +{ + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// 查询结果 Select 表达式 + /// + public static SqlSugarPagedList ToPagedList(this ISugarQueryable query, int pageIndex, int pageSize, + Expression> expression) + { + var total = 0; + var items = query.ToPageList(pageIndex, pageSize, ref total, expression); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static SqlSugarPagedList ToPagedList(this ISugarQueryable query, int pageIndex, int pageSize) + { + var total = 0; + var items = query.ToPageList(pageIndex, pageSize, ref total); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// 查询结果 Select 表达式 + /// + public static async Task> ToPagedListAsync(this ISugarQueryable query, int pageIndex, int pageSize, + Expression> expression) + { + RefAsync total = 0; + var items = await query.ToPageListAsync(pageIndex, pageSize, total, expression); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static async Task> ToPagedListAsync(this ISugarQueryable query, int pageIndex, int pageSize) + { + RefAsync total = 0; + var items = await query.ToPageListAsync(pageIndex, pageSize, total); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 脱敏分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static async Task> ToPagedListDataMaskAsync(this ISugarQueryable query, int pageIndex, int pageSize) where TEntity : class + { + RefAsync total = 0; + var items = await query.ToPageListAsync(pageIndex, pageSize, total); + items.ForEach(x => x.MaskSensitiveData()); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 脱敏分页拓展 + /// + /// 集合对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static SqlSugarPagedList ToPagedListDataMask(this IEnumerable list, int pageIndex, int pageSize) where TEntity : class + { + var total = list.Count(); + var items = list.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + items.ForEach(x => x.MaskSensitiveData()); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 集合对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static SqlSugarPagedList ToPagedList(this IEnumerable list, int pageIndex, int pageSize) + { + var total = list.Count(); + var items = list.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 创建 对象 + /// + /// + /// 分页内容的对象集合 + /// 总条数 + /// 当前页码,从1开始 + /// 页码容量 + /// + private static SqlSugarPagedList CreateSqlSugarPagedList(IEnumerable items, int total, int pageIndex, int pageSize) + { + var totalPages = pageSize > 0 ? (int)Math.Ceiling(total / (double)pageSize) : 0; + return new SqlSugarPagedList + { + Page = pageIndex, + PageSize = pageSize, + Items = items, + Total = total, + TotalPages = totalPages, + HasNextPage = pageIndex < totalPages, + HasPrevPage = pageIndex - 1 > 0 + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs b/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs new file mode 100644 index 0000000..42b587f --- /dev/null +++ b/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs @@ -0,0 +1,113 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// SqlSugar 实体仓储 +/// +/// +public class SqlSugarRepository : SimpleClient, ISqlSugarRepository where T : class, new() +{ + public SqlSugarRepository() + { + var iTenant = SqlSugarSetup.ITenant; // App.GetRequiredService().AsTenant(); + base.Context = iTenant.GetConnectionScope(SqlSugarConst.MainConfigId); + + // 若实体贴有多库特性,则返回指定库连接 + if (typeof(T).IsDefined(typeof(TenantAttribute), false)) + { + base.Context = iTenant.GetConnectionScopeWithAttr(); + return; + } + + // 若实体贴有日志表特性,则返回日志库连接 + if (typeof(T).IsDefined(typeof(LogTableAttribute), false)) + { + if (iTenant.IsAnyConnection(SqlSugarConst.LogConfigId)) + base.Context = iTenant.GetConnectionScope(SqlSugarConst.LogConfigId); + return; + } + + // 若实体贴有系统表特性,则返回默认库连接 + if (typeof(T).IsDefined(typeof(SysTableAttribute), false)) + return; + + // 看请求头有没有租户id + var tenantId = App.HttpContext?.Request.Headers[ClaimConst.TenantId].FirstOrDefault(); + if (tenantId == SqlSugarConst.MainConfigId) return; + else if (string.IsNullOrWhiteSpace(tenantId)) + { + // 若未贴任何表特性或当前未登录或是默认租户Id,则返回默认库连接 + tenantId = App.User?.FindFirst(ClaimConst.TenantId)?.Value; + if (string.IsNullOrWhiteSpace(tenantId) || tenantId == SqlSugarConst.MainConfigId) return; + } + + // 根据租户Id切换库连接 为空则返回默认库连接 + var sqlSugarScopeProviderTenant = App.GetRequiredService().GetTenantDbConnectionScope(long.Parse(tenantId)); + if (sqlSugarScopeProviderTenant == null) return; + base.Context = sqlSugarScopeProviderTenant; + } + + #region 分表操作 + + public async Task SplitTableInsertAsync(T input) + { + return await base.AsInsertable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + public async Task SplitTableInsertAsync(List input) + { + return await base.AsInsertable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + public async Task SplitTableUpdateAsync(T input) + { + return await base.AsUpdateable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + public async Task SplitTableUpdateAsync(List input) + { + return await base.AsUpdateable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + public async Task SplitTableDeleteableAsync(T input) + { + return await base.Context.Deleteable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + public async Task SplitTableDeleteableAsync(List input) + { + return await base.Context.Deleteable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + public Task SplitTableGetFirstAsync(Expression> whereExpression) + { + return base.AsQueryable().SplitTable().FirstAsync(whereExpression); + } + + public Task SplitTableIsAnyAsync(Expression> whereExpression) + { + return base.Context.Queryable().Where(whereExpression).SplitTable().AnyAsync(); + } + + public Task> SplitTableGetListAsync() + { + return Context.Queryable().SplitTable().ToListAsync(); + } + + public Task> SplitTableGetListAsync(Expression> whereExpression) + { + return Context.Queryable().Where(whereExpression).SplitTable().ToListAsync(); + } + + public Task> SplitTableGetListAsync(Expression> whereExpression, string[] tableNames) + { + return Context.Queryable().Where(whereExpression).SplitTable(t => t.InTableNames(tableNames)).ToListAsync(); + } + + #endregion 分表操作 +} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs b/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs new file mode 100644 index 0000000..1bb58db --- /dev/null +++ b/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs @@ -0,0 +1,730 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.Data.Sqlite; +using DbType = SqlSugar.DbType; + +namespace Admin.NET.Core; + +public static class SqlSugarSetup +{ + // 多租户实例 + public static ITenant ITenant { get; set; } + + // 是否正在处理种子数据 + private static bool _isHandlingSeedData = false; + + /// + /// SqlSugar 上下文初始化 + /// + /// + public static void AddSqlSugar(this IServiceCollection services) + { + // 注册雪花Id + var snowIdOpt = App.GetConfig("SnowId", true); + YitIdHelper.SetIdGenerator(snowIdOpt); + + // 自定义 SqlSugar 雪花ID算法 + SnowFlakeSingle.WorkId = snowIdOpt.WorkerId; + StaticConfig.CustomSnowFlakeFunc = YitIdHelper.NextId; + // 注册 MongoDb + InstanceFactory.CustomAssemblies = [typeof(SqlSugar.MongoDb.MongoDbProvider).Assembly]; + // 动态表达式 SqlFunc 支持,https://www.donet5.com/Home/Doc?typeId=2569 + StaticConfig.DynamicExpressionParserType = typeof(DynamicExpressionParser); + StaticConfig.DynamicExpressionParsingConfig = new ParsingConfig + { + CustomTypeProvider = new SqlSugarTypeProvider() + }; + + var dbOptions = App.GetConfig("DbConnection", true); + dbOptions.ConnectionConfigs.ForEach(SetDbConfig); + + SqlSugarScope sqlSugar = new(dbOptions.ConnectionConfigs.Adapt>(), db => + { + dbOptions.ConnectionConfigs.ForEach(config => + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + SetDbAop(dbProvider, dbOptions.EnableConsoleSql, dbOptions.SuperAdminIgnoreIDeletedFilter); + SetDbDiffLog(dbProvider, config); + }); + }); + ITenant = sqlSugar; + + services.AddSingleton(sqlSugar); // 单例注册 + services.AddScoped(typeof(SqlSugarRepository<>)); // 仓储注册 + services.AddUnitOfWork(); // 事务与工作单元注册 + + // 初始化数据库表结构及种子数据 + dbOptions.ConnectionConfigs.ForEach(config => + { + InitDatabase(sqlSugar, config); + }); + } + + /// + /// 配置连接属性 + /// + /// + public static void SetDbConfig(DbConnectionConfig config) + { + if (config.DbSettings.EnableConnStringEncrypt) + config.ConnectionString = CryptogramUtil.Decrypt(config.ConnectionString); + + var configureExternalServices = new ConfigureExternalServices + { + EntityNameService = (type, entity) => // 处理表 + { + entity.IsDisabledDelete = true; // 禁止删除非 sqlsugar 创建的列 + // 只处理贴了特性[SugarTable]表 + if (!type.GetCustomAttributes().Any()) + return; + if (config.DbSettings.EnableUnderLine && !entity.DbTableName.Contains('_')) + entity.DbTableName = entity.DbTableName.ToUnderLine(); // 驼峰转下划线 + }, + EntityService = (type, column) => // 处理列 + { + // 只处理贴了特性[SugarColumn]列 + if (!type.GetCustomAttributes().Any()) + return; + if (new NullabilityInfoContext().Create(type).WriteState is NullabilityState.Nullable) + column.IsNullable = true; + if (config.DbSettings.EnableUnderLine && !column.IsIgnore && !column.DbColumnName.Contains('_')) + column.DbColumnName = column.DbColumnName.ToUnderLine(); // 驼峰转下划线 + }, + DataInfoCacheService = new SqlSugarCache(), + }; + config.ConfigureExternalServices = configureExternalServices; + config.InitKeyType = InitKeyType.Attribute; + config.IsAutoCloseConnection = true; + config.MoreSettings = new ConnMoreSettings + { + IsAutoRemoveDataCache = true, // 启用自动删除缓存,所有增删改会自动调用.RemoveDataCache() + IsAutoDeleteQueryFilter = true, // 启用删除查询过滤器 + IsAutoUpdateQueryFilter = true, // 启用更新查询过滤器 + SqlServerCodeFirstNvarchar = true // 采用Nvarchar + }; + + // 若库类型是人大金仓则默认设置PG模式 + if (config.DbType == DbType.Kdbndp) + config.MoreSettings.DatabaseModel = DbType.PostgreSQL; // 配置PG模式主要是兼容系统表差异 + + // 若库类型是Oracle则默认主键名字和参数名字最大长度 + if (config.DbType == DbType.Oracle) + config.MoreSettings.MaxParameterNameLength = 30; + } + + /// + /// 配置Aop + /// + /// + /// + /// + public static void SetDbAop(SqlSugarScopeProvider db, bool enableConsoleSql, bool superAdminIgnoreIDeletedFilter) + { + // 设置超时时间 + db.Ado.CommandTimeOut = 30; + + // 打印SQL语句 + if (enableConsoleSql) + { + db.Aop.OnLogExecuting = (sql, pars) => + { + //// 若参数值超过100个字符则进行截取 + //foreach (var par in pars) + //{ + // if (par.DbType != System.Data.DbType.String || par.Value == null) continue; + // if (par.Value.ToString().Length > 100) + // par.Value = string.Concat(par.Value.ToString()[..100], "......"); + //} + + var log = $"【{DateTime.Now}——执行SQL】\r\n{UtilMethods.GetNativeSql(sql, pars)}\r\n"; + var originColor = Console.ForegroundColor; + if (sql.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Green; + if (sql.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase) || sql.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Yellow; + if (sql.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(log); + Console.ForegroundColor = originColor; + }; + } + db.Aop.OnError = ex => + { + if (ex.Parametres == null) return; + var log = $"【{DateTime.Now}——错误SQL】\r\n{UtilMethods.GetNativeSql(ex.Sql, (SugarParameter[])ex.Parametres)}\r\n"; + Log.Error(log, ex); + }; + db.Aop.OnLogExecuted = (sql, pars) => + { + //// 若参数值超过100个字符则进行截取 + //foreach (var par in pars) + //{ + // if (par.DbType != System.Data.DbType.String || par.Value == null) continue; + // if (par.Value.ToString().Length > 100) + // par.Value = string.Concat(par.Value.ToString()[..100], "......"); + //} + + // 执行时间超过5秒时 + if (!(db.Ado.SqlExecutionTime.TotalSeconds > 5)) return; + + var fileName = db.Ado.SqlStackTrace.FirstFileName; // 文件名 + var fileLine = db.Ado.SqlStackTrace.FirstLine; // 行号 + var firstMethodName = db.Ado.SqlStackTrace.FirstMethodName; // 方法名 + var log = $"【{DateTime.Now}——超时SQL】\r\n【所在文件名】:{fileName}\r\n【代码行数】:{fileLine}\r\n【方法名】:{firstMethodName}\r\n" + $"【SQL语句】:{UtilMethods.GetNativeSql(sql, pars)}"; + Log.Warning(log); + }; + + // 数据审计 + db.Aop.DataExecuting = (_, entityInfo) => + { + // 若正在处理种子数据则直接返回 + if (_isHandlingSeedData) return; + + // 新增/插入 + if (entityInfo.OperationType == DataFilterType.InsertByObject) + { + // 若主键是长整型且空则赋值雪花Id + if (entityInfo.EntityColumnInfo.IsPrimarykey && !entityInfo.EntityColumnInfo.IsIdentity && entityInfo.EntityColumnInfo.PropertyInfo.PropertyType == typeof(long)) + { + var id = entityInfo.EntityColumnInfo.PropertyInfo.GetValue(entityInfo.EntityValue); + if (id == null || (long)id == 0) + entityInfo.SetValue(YitIdHelper.NextId()); + } + // 若创建时间为空则赋值当前时间 + else if (entityInfo.PropertyName == nameof(EntityBase.CreateTime)) + { + var createTime = entityInfo.EntityColumnInfo.PropertyInfo.GetValue(entityInfo.EntityValue)!; + if (createTime == null || createTime.Equals(DateTime.MinValue)) + entityInfo.SetValue(DateTime.Now); + } + // 若当前用户为空(非web线程时) + if (App.User == null) return; + + dynamic entityValue = entityInfo.EntityValue; + if (entityInfo.PropertyName == nameof(EntityBaseTenantId.TenantId)) + { + var tenantId = entityValue.TenantId; + if (tenantId == null || tenantId == 0) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.TenantId)?.Value); + } + else if (entityInfo.PropertyName == nameof(EntityBase.CreateUserId)) + { + var createUserId = entityValue.CreateUserId; + if (createUserId == 0 || createUserId == null) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.UserId)?.Value); + } + else if (entityInfo.PropertyName == nameof(EntityBase.CreateUserName)) + { + var createUserName = entityValue.CreateUserName; + if (string.IsNullOrEmpty(createUserName)) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.RealName)?.Value); + } + else if (entityInfo.PropertyName == "CreateOrgId") + { + var createOrgId = entityValue.CreateOrgId; + if (createOrgId == 0 || createOrgId == null) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.OrgId)?.Value); + } + else if (entityInfo.PropertyName == "CreateOrgName") + { + var createOrgName = entityValue.CreateOrgName; + if (string.IsNullOrEmpty(createOrgName)) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.OrgName)?.Value); + } + } + // 编辑/更新 + else if (entityInfo.OperationType == DataFilterType.UpdateByObject) + { + if (entityInfo.PropertyName == nameof(EntityBase.UpdateTime)) + entityInfo.SetValue(DateTime.Now); + else if (entityInfo.PropertyName == nameof(EntityBase.UpdateUserId)) + entityInfo.SetValue(App.User?.FindFirst(ClaimConst.UserId)?.Value); + else if (entityInfo.PropertyName == nameof(EntityBase.UpdateUserName)) + entityInfo.SetValue(App.User?.FindFirst(ClaimConst.RealName)?.Value); + else if (entityInfo.PropertyName == nameof(EntityBaseDel.DeleteTime)) + { + dynamic entityValue = entityInfo.EntityValue; + var isDelete = entityValue.IsDelete; + if (isDelete == true) + { + entityInfo.SetValue(DateTime.Now); + } + } + } + }; + + // 是否为超级管理员 + var isSuperAdmin = App.User?.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString(); + + // 配置假删除过滤器,如果当前用户是超级管理员并且允许忽略软删除过滤器则不会应用 + if (!isSuperAdmin || !superAdminIgnoreIDeletedFilter) + db.QueryFilter.AddTableFilter(u => u.IsDelete == false); + + // 超管排除其他过滤器 + if (isSuperAdmin) return; + + // 配置租户过滤器 + var tenantId = App.User?.FindFirst(ClaimConst.TenantId)?.Value; + if (!string.IsNullOrWhiteSpace(tenantId)) + db.QueryFilter.AddTableFilter(u => u.TenantId == long.Parse(tenantId)); + + // 配置用户机构(数据范围)过滤器 + SqlSugarFilter.SetOrgEntityFilter(db); + + // 配置自定义过滤器 + SqlSugarFilter.SetCustomEntityFilter(db); + } + + /// + /// 开启库表差异化日志 + /// + /// + /// + private static void SetDbDiffLog(SqlSugarScopeProvider db, DbConnectionConfig config) + { + if (!config.DbSettings.EnableDiffLog) return; + + async void AopOnDiffLogEvent(DiffLogModel u) + { + // 记录差异数据 + var diffData = new List(); + for (int i = 0; i < u.AfterData.Count; i++) + { + var diffColumns = new List(); + var afterColumns = u.AfterData[i].Columns; + var beforeColumns = u.BeforeData[i].Columns; + for (int j = 0; j < afterColumns.Count; j++) + { + if (afterColumns[j].Value.Equals(beforeColumns[j].Value)) continue; + diffColumns.Add(new + { + afterColumns[j].IsPrimaryKey, + afterColumns[j].ColumnName, + afterColumns[j].ColumnDescription, + BeforeValue = beforeColumns[j].Value, + AfterValue = afterColumns[j].Value, + }); + } + + diffData.Add(new { u.AfterData[i].TableName, u.AfterData[i].TableDescription, Columns = diffColumns }); + } + + var logDiff = new SysLogDiff + { + // 差异数据(字段描述、列名、值、表名、表描述) + DiffData = JSON.Serialize(diffData), + // 传进来的对象(如果对象为空,则使用首个数据的表名作为业务对象) + BusinessData = u.BusinessData == null ? u.AfterData.FirstOrDefault()?.TableName : JSON.Serialize(u.BusinessData), + // 枚举(insert、update、delete) + DiffType = u.DiffType.ToString(), + Sql = u.Sql, + Parameters = JSON.Serialize(u.Parameters.Select(e => new { e.ParameterName, e.Value, TypeName = e.DbType.ToString() })), + Elapsed = u.Time == null ? 0 : (long)u.Time.Value.TotalMilliseconds + }; + var logDb = ITenant.IsAnyConnection(SqlSugarConst.LogConfigId) ? ITenant.GetConnectionScope(SqlSugarConst.LogConfigId) : ITenant.GetConnectionScope(SqlSugarConst.MainConfigId); + await logDb.CopyNew().Insertable(logDiff).ExecuteCommandAsync(); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(DateTime.Now + $"\r\n*****开始差异日志*****\r\n{Environment.NewLine}{JSON.Serialize(logDiff)}{Environment.NewLine}*****结束差异日志*****\r\n"); + } + + db.Aop.OnDiffLogEvent = AopOnDiffLogEvent; + } + + /// + /// 初始化视图 + /// + /// + private static void InitView(SqlSugarScopeProvider dbProvider) + { + var totalWatch = Stopwatch.StartNew(); // 开始总计时 + Log.Information($"初始化视图 {dbProvider.CurrentConnectionConfig.DbType} - {dbProvider.CurrentConnectionConfig.ConfigId}"); + var viewTypeList = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarView)))).ToList(); + + int taskIndex = 0, size = viewTypeList.Count; + var taskList = viewTypeList.Select(viewType => Task.Run(() => + { + // 开始计时 + var stopWatch = Stopwatch.StartNew(); + + // 获取视图实体和配置信息 + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(viewType) ?? throw new Exception("获取视图实体配置有误"); + + // 如果视图存在,则删除视图 + if (dbProvider.DbMaintenance.GetViewInfoList(false).Any(it => it.Name.EqualIgnoreCase(entityInfo.DbTableName))) + dbProvider.DbMaintenance.DropView(entityInfo.DbTableName); + + // 获取初始化视图查询SQL + var sql = viewType.GetMethod(nameof(ISqlSugarView.GetQueryableSqlString))?.Invoke(Activator.CreateInstance(viewType), [dbProvider]) as string; + if (string.IsNullOrWhiteSpace(sql)) throw new Exception("视图初始化Sql语句不能为空"); + + // 创建视图 + dbProvider.Ado.ExecuteCommand($"CREATE VIEW {entityInfo.DbTableName} AS " + Environment.NewLine + " " + sql); + + // 停止计时 + stopWatch.Stop(); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"初始化视图 {viewType.FullName,-58} ({dbProvider.CurrentConnectionConfig.ConfigId} - {Interlocked.Increment(ref taskIndex):D003}/{size:D003},耗时:{stopWatch.ElapsedMilliseconds:N0} ms)"); + })); + Task.WaitAll(taskList.ToArray()); + + totalWatch.Stop(); // 停止总计时 + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"初始化视图 {dbProvider.CurrentConnectionConfig.DbType} - {dbProvider.CurrentConnectionConfig.ConfigId} 总耗时:{totalWatch.ElapsedMilliseconds:N0} ms"); + } + + /// + /// 等待数据库就绪 + /// + /// + private static void WaitForDatabaseReady(SqlSugarScopeProvider dbProvider) + { + do + { + try + { + if (dbProvider.Ado.Connection.State != ConnectionState.Open) + dbProvider.Ado.Connection.Open(); + + // 如果连接成功,直接返回 + Log.Information("数据库连接成功。"); + return; + } + catch (Exception ex) + { + Log.Warning($"数据库尚未就绪,等待中... 错误:{ex.Message}"); + Thread.Sleep(1000); + } + } while (true); + } + + /// + /// 初始化数据库 + /// + /// SqlSugarScope 实例 + /// 数据库连接配置 + private static void InitDatabase(SqlSugarScope db, DbConnectionConfig config) + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + + // 初始化数据库 如果是没有数据库的话,是先初始化数据库再做连接 + if (config.DbSettings.EnableInitDb) + { + Log.Information($"初始化数据库 {config.DbType} - {config.ConfigId} - {config.ConnectionString}"); + if (config.DbType != DbType.Oracle) dbProvider.DbMaintenance.CreateDatabase(); + } + + // 等待数据库连接就绪 + WaitForDatabaseReady(dbProvider); + + // 初始化表结构 + if (config.TableSettings.EnableInitTable) + { + Log.Information($"初始化表结构 {config.DbType} - {config.ConfigId}"); + var entityTypes = GetEntityTypesForInit(config); + InitializeTables(dbProvider, entityTypes, config); + } + + // 初始化视图 + if (config.DbSettings.EnableInitView) InitView(dbProvider); + + // 初始化种子数据 + if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config); + } + + /// + /// 获取需要初始化的实体类型 + /// + /// 数据库连接配置 + /// 实体类型列表 + private static List GetEntityTypesForInit(DbConnectionConfig config) + { + return App.EffectiveTypes + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false)) + .Where(u => !u.GetCustomAttributes().Any()) + .WhereIF(config.TableSettings.EnableIncreTable, u => u.IsDefined(typeof(IncreTableAttribute), false)) + .Where(u => IsEntityForConfig(u, config)) + .ToList(); + } + + /// + /// 判断实体是否属于当前配置 + /// + /// 实体类型 + /// 数据库连接配置 + /// 是否属于当前配置 + private static bool IsEntityForConfig(Type entityType, DbConnectionConfig config) + { + switch (config.ConfigId.ToString()) + { + case SqlSugarConst.MainConfigId: + return entityType.GetCustomAttributes().Any() || + (!entityType.GetCustomAttributes().Any() && + !entityType.GetCustomAttributes().Any(o => o.configId.ToString() != config.ConfigId.ToString())); + + case SqlSugarConst.LogConfigId: + return entityType.GetCustomAttributes().Any(); + + default: + { + var tenantAttribute = entityType.GetCustomAttribute(); + return tenantAttribute != null && tenantAttribute.configId.ToString() == config.ConfigId.ToString(); + } + } + } + + /// + /// 初始化表结构 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型列表 + /// 数据库连接配置 + private static void InitializeTables(SqlSugarScopeProvider dbProvider, List entityTypes, DbConnectionConfig config) + { + // 删除视图再初始化表结构,防止因为视图导致无法同步表结构 + var viewTypeList = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarView)))).ToList(); + foreach (var viewType in viewTypeList) + { + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(viewType) ?? throw new Exception("获取视图实体配置有误"); + if (dbProvider.DbMaintenance.GetViewInfoList(false).Any(it => it.Name.EqualIgnoreCase(entityInfo.DbTableName))) + dbProvider.DbMaintenance.DropView(entityInfo.DbTableName); + } + + int count = 0, sum = entityTypes.Count; + var tasks = entityTypes.Select(entityType => Task.Run(() => + { + Console.WriteLine($"初始化表结构 {entityType.FullName,-64} ({config.ConfigId} - {Interlocked.Increment(ref count):D003}/{sum:D003})"); + UpdateNullableColumns(dbProvider, entityType); + InitializeTable(dbProvider, entityType); + })); + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + } + + /// + /// 更新表中不存在于实体的字段为可空 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型 + private static void UpdateNullableColumns(SqlSugarScopeProvider dbProvider, Type entityType) + { + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType); + var dbColumns = dbProvider.DbMaintenance.GetColumnInfosByTableName(entityInfo.DbTableName) ?? new List(); + + foreach (var dbColumn in dbColumns.Where(c => !c.IsPrimarykey && entityInfo.Columns.All(u => u.DbColumnName != c.DbColumnName))) + { + dbColumn.IsNullable = true; + Retry(() => + { + dbProvider.DbMaintenance.UpdateColumn(entityInfo.DbTableName, dbColumn); + }, maxRetry: 3, retryIntervalMs: 1000); + } + } + + /// + /// 初始化表 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型 + private static void InitializeTable(SqlSugarScopeProvider dbProvider, Type entityType) + { + Retry(() => + { + if (entityType.GetCustomAttribute() == null) + { + dbProvider.CodeFirst.InitTables(entityType); + } + else + { + dbProvider.CodeFirst.SplitTables().InitTables(entityType); + } + }, maxRetry: 3, retryIntervalMs: 1000); + } + + /// + /// 初始化种子数据 + /// + /// SqlSugarScope 实例 + /// 数据库连接配置 + private static void InitSeedData(SqlSugarScope db, DbConnectionConfig config) + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + _isHandlingSeedData = true; + + Log.Information($"初始化种子数据 {config.DbType} - {config.ConfigId}"); + var seedDataTypes = GetSeedDataTypes(config); + + int count = 0, sum = seedDataTypes.Count; + var tasks = seedDataTypes.Select(seedType => Task.Run(() => + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + if (!IsEntityForConfig(entityType, config)) return; + + var seedData = GetSeedData(seedType)?.ToList(); + if (seedData == null) return; + + AdjustSeedDataIds(seedData, config); + InsertOrUpdateSeedData(dbProvider, seedType, entityType, seedData, config, ref count, sum); + })); + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + _isHandlingSeedData = false; + } + + /// + /// 获取种子数据类型 + /// + /// 数据库连接配置 + /// 种子数据类型列表 + private static List GetSeedDataTypes(DbConnectionConfig config) + { + return App.EffectiveTypes + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>)))) + .WhereIF(config.SeedSettings.EnableIncreSeed, u => u.IsDefined(typeof(IncreSeedAttribute), false)) + .OrderBy(u => u.GetCustomAttributes(typeof(SeedDataAttribute), false).Length > 0 ? ((SeedDataAttribute)u.GetCustomAttributes(typeof(SeedDataAttribute), false)[0]).Order : 0) + .ToList(); + } + + /// + /// 获取种子数据 + /// + /// 种子数据类型 + /// 种子数据列表 + private static IEnumerable GetSeedData(Type seedType) + { + var instance = Activator.CreateInstance(seedType); + var hasDataMethod = seedType.GetMethod("HasData"); + return ((IEnumerable)hasDataMethod?.Invoke(instance, null))?.Cast(); + } + + /// + /// 调整种子数据的 ID + /// + /// 种子数据列表 + /// 数据库连接配置 + private static void AdjustSeedDataIds(IEnumerable seedData, DbConnectionConfig config) + { + var seedId = config.ConfigId.ToLong(); + foreach (var data in seedData) + { + var idProperty = data.GetType().GetProperty(nameof(EntityBaseId.Id)); + if (idProperty == null || idProperty.PropertyType != typeof(Int64)) continue; + + var idValue = idProperty.GetValue(data); + if (idValue == null || idValue.ToString() == "0" || string.IsNullOrWhiteSpace(idValue.ToString())) + { + idProperty.SetValue(data, ++seedId); + } + } + } + + /// + /// 插入或更新种子数据 + /// + /// SqlSugarScopeProvider 实例 + /// 种子数据类型 + /// 实体类型 + /// 种子数据列表 + /// 数据库连接配置 + /// 当前处理的数量 + /// 总数量 + private static void InsertOrUpdateSeedData(SqlSugarScopeProvider dbProvider, Type seedType, Type entityType, IEnumerable seedData, DbConnectionConfig config, ref int count, int sum) + { + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType); + var dataList = seedData.ToList(); + + if (entityType.GetCustomAttribute(true) != null) + { + var initMethod = seedType.GetMethod("Init"); + initMethod?.Invoke(Activator.CreateInstance(seedType), new object[] { dbProvider }); + } + else + { + int updateCount = 0, insertCount = 0; + if (entityInfo.Columns.Any(u => u.IsPrimarykey)) + { + var storage = dbProvider.StorageableByObject(dataList).ToStorage(); + if (seedType.GetCustomAttribute() == null) + { + updateCount = storage.AsUpdateable + .IgnoreColumns(entityInfo.Columns + .Where(u => u.PropertyInfo.GetCustomAttribute() != null) + .Select(u => u.PropertyName).ToArray()) + .ExecuteCommand(); + } + insertCount = storage.AsInsertable.ExecuteCommand(); + } + else + { + if (!dbProvider.Queryable(entityInfo.DbTableName, entityInfo.DbTableName).Any()) + { + insertCount = dataList.Count; + dbProvider.InsertableByObject(dataList).ExecuteCommand(); + } + } + Console.WriteLine($"添加数据 {entityInfo.DbTableName,-32} ({config.ConfigId} - {Interlocked.Increment(ref count):D003}/{sum:D003},数据量:{dataList.Count:D003},插入 {insertCount:D003} 条记录,修改 {updateCount:D003} 条记录)"); + } + } + + /// + /// 初始化租户业务数据库 + /// + /// + /// + public static void InitTenantDatabase(ITenant iTenant, DbConnectionConfig config) + { + SetDbConfig(config); + + if (!iTenant.IsAnyConnection(config.ConfigId.ToString())) + iTenant.AddConnection(config); + var db = iTenant.GetConnectionScope(config.ConfigId.ToString()); + db.DbMaintenance.CreateDatabase(); + + // 获取所有业务表-初始化租户库表结构(排除系统表、日志表、特定库表) + var entityTypes = App.EffectiveTypes + .Where(u => !u.GetCustomAttributes().Any()) + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false) && + !u.IsDefined(typeof(SysTableAttribute), false) && !u.IsDefined(typeof(LogTableAttribute), false) && !u.IsDefined(typeof(TenantAttribute), false)).ToList(); + if (entityTypes.Count == 0) return; + + foreach (var entityType in entityTypes) + { + var splitTable = entityType.GetCustomAttribute(); + if (splitTable == null) + db.CodeFirst.InitTables(entityType); + else + db.CodeFirst.SplitTables().InitTables(entityType); + } + } + + /// + /// 简单的重试机制 + /// + /// + /// + /// + private static void Retry(Action action, int maxRetry, int retryIntervalMs) + { + int attempt = 0; + while (true) + { + try + { + action(); + return; + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 5) // SQLITE_BUSY + { + if (++attempt >= maxRetry) + { + Log.Error($"简单的重试机制:{ex.Message}"); throw; + } + Log.Information($"数据库忙,正在重试... (尝试 {attempt}/{maxRetry})"); + Thread.Sleep(retryIntervalMs); + } + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/SqlSugarTypeProvider.cs b/Admin.NET.Core/SqlSugar/SqlSugarTypeProvider.cs new file mode 100644 index 0000000..2165426 --- /dev/null +++ b/Admin.NET.Core/SqlSugar/SqlSugarTypeProvider.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Linq.Dynamic.Core.CustomTypeProviders; + +namespace Admin.NET.Core; + +/// +/// 扩展支持 SqlFunc,不支持 Subqueryable +/// +public class SqlSugarTypeProvider : DefaultDynamicLinqCustomTypeProvider +{ + public SqlSugarTypeProvider(bool cacheCustomTypes = true) : base(ParsingConfig.Default, cacheCustomTypes) + { + } + + public override HashSet GetCustomTypes() + { + var customTypes = base.GetCustomTypes(); + customTypes.Add(typeof(SqlFunc)); + return customTypes; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/SqlSugar/SqlSugarUnitOfWork.cs b/Admin.NET.Core/SqlSugar/SqlSugarUnitOfWork.cs new file mode 100644 index 0000000..0975eef --- /dev/null +++ b/Admin.NET.Core/SqlSugar/SqlSugarUnitOfWork.cs @@ -0,0 +1,71 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// SqlSugar 事务和工作单元 +/// +public sealed class SqlSugarUnitOfWork : IUnitOfWork +{ + /// + /// SqlSugar 对象 + /// + private readonly ISqlSugarClient _sqlSugarClient; + + /// + /// 构造函数 + /// + /// + public SqlSugarUnitOfWork(ISqlSugarClient sqlSugarClient) + { + _sqlSugarClient = sqlSugarClient; + } + + /// + /// 开启工作单元处理 + /// + /// + /// + /// + public void BeginTransaction(FilterContext context, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().BeginTran(); + } + + /// + /// 提交工作单元处理 + /// + /// + /// + /// + public void CommitTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().CommitTran(); + } + + /// + /// 回滚工作单元处理 + /// + /// + /// + /// + public void RollbackTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().RollbackTran(); + } + + /// + /// 执行完毕(无论成功失败) + /// + /// + /// + /// + public void OnCompleted(FilterContext context, FilterContext resultContext) + { + _sqlSugarClient.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Update/AutoVersionUpdate.cs b/Admin.NET.Core/Update/AutoVersionUpdate.cs new file mode 100644 index 0000000..645acb4 --- /dev/null +++ b/Admin.NET.Core/Update/AutoVersionUpdate.cs @@ -0,0 +1,240 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +#if NET10_0_OR_GREATER + +using Microsoft.AspNetCore.Builder; +using XiHan.Framework.Utils.Logging; +using XiHan.Framework.Utils.Reflections; + +namespace Admin.NET.Core.Update; + +/// +/// 自动版本更新中间件拓展 +/// +/// +/// 使用方法 +/// 1.在 Admin.NET.Web.Core 的 Startup.cs 中的 Configure 方法中调用 app.UseAutoVersionUpdate()。 +/// 2.在入口项目 Admin.NET.Web.Entry 的根目录下创建一个名为 UpdateScripts 的文件夹,并在其中放置 .sql 后缀的脚本文件 +/// 3.脚本文件命名格式为版本号,例如 1.0.0.sql、1.0.1.sql 等,版本号应符合语义化版本规范。 +/// 4.脚本的属性:复制到输出目录,设置为:始终复制。 +/// 5.设置主节点的 Admin.NET.Application 的 Configuration/App.json 的 WorkerId 为 1。 +/// 6.设置入口项目 Admin.NET.Web.Entry.csproj 的 Version。 +/// ================================================== +/// 更新新版本时 +/// 1.需在 UpdateScripts 文件夹中添加新的脚本文件,脚本文件名应为新版本号。 +/// 2.设置入口项目 Admin.NET.Web.Entry.csproj 的 Version +/// +[SuppressSniffer] +public static class AutoVersionUpdate +{ + /// + /// 使用自动版本更新中间件 + /// + /// + /// + public static IApplicationBuilder UseAutoVersionUpdate(this IApplicationBuilder app) + { + LogHelper.Info("AutoVersionUpdate 中间件运行"); + + var snowIdOpt = App.GetConfig("SnowId", true); + if (snowIdOpt.WorkerId != 1) + { + LogHelper.Handle("非主节点,不执行脚本"); + return app; + } + + var currentVersion = GetEntryAssemblyCurrentVersion(); + LogHelper.Handle($"当前版本:{currentVersion}"); + + var historyVersionInfo = GetEntryAssemblyHistoryVersionInfo(); + var historyVersion = historyVersionInfo.Version; + var historyDate = historyVersionInfo.Date; + var historyIsRunScript = historyVersionInfo.IsRunScript; + + LogHelper.Handle($"历史版本:{historyVersion},更新时间:{historyDate},是否已执行{historyIsRunScript}"); + + // 历史版本为空、版本号相同,不执行脚本 + if (historyVersion == string.Empty) + { + LogHelper.Handle("历史版本为空,默认为最新版本,不执行脚本"); + + // 保存当前版本信息 + SetEntryAssemblyCurrentVersion(currentVersion, true); + + return app; + } + else if (currentVersion.CompareTo(historyVersion) <= 0 && historyIsRunScript) + { + LogHelper.Handle("当前版本号与历史版本号相同,且已执行过脚本,不再执行"); + + // 保存当前版本信息 + SetEntryAssemblyCurrentVersion(currentVersion, false); + + return app; + } + else + { + LogHelper.Handle("当前版本号与历史版本号不同,或版本号相同但未执行过脚本,开始执行脚本"); + + var scriptSqlVersions = GetScriptSqlVersions(); + + // 若不存在当前版本的脚本,则只保存当前版本信息,不执行脚本 + if (scriptSqlVersions.All(s => s.Version.CompareTo(currentVersion) < 0)) + { + LogHelper.Handle("不存在当前版本的脚本,只保存当前版本信息,不执行脚本"); + + // 保存当前版本信息 + SetEntryAssemblyCurrentVersion(currentVersion, false); + + return app; + } + + // 执行脚本 + foreach (var sqlFileInfo in scriptSqlVersions) + { + var sqlVersion = sqlFileInfo.Version; + + // 只执行大于历史版本的脚本,或者当前版本但未执行过 + if (sqlVersion.CompareTo(historyVersion) < 0) + { + LogHelper.Handle($"版本{sqlVersion}低于历史版本,跳过"); + continue; + } + if (sqlVersion == historyVersion && historyIsRunScript) + { + LogHelper.Handle($"版本{sqlVersion}等于历史版本,且已执行过脚本,跳过"); + continue; + } + + // 执行脚本 + var sql = File.ReadAllText(sqlFileInfo.FilePath); + if (sql != null) + { + LogHelper.Handle($"执行版本{sqlVersion}脚本"); + + HandleSqlScript(app, sql, sqlVersion); + } + } + } + + LogHelper.Success("AutoVersionUpdate 中间件结束"); + + return app; + } + + #region 辅助方法 + + /// + /// 获取入口程序集当前版本信息 + /// + /// + private static string GetEntryAssemblyCurrentVersion() + { + var entryAssemblyVersion = ReflectionHelper.GetEntryAssemblyVersion(); + return entryAssemblyVersion.ToString(3); + } + + /// + /// 设置入口程序集当前版本信息 + /// + /// + /// + private static void SetEntryAssemblyCurrentVersion(string version, bool isRunScript) + { + var path = Path.Combine(AppContext.BaseDirectory, "version.txt"); + var now = DateTime.Now; + File.WriteAllText(path, $"{version}^{now:yyyy-MM-dd HH:mm:ss}^{isRunScript}"); + } + + /// + /// 获取入口程序集上一次运行版本信息 + /// + /// + private static HistoryVersionInfo GetEntryAssemblyHistoryVersionInfo() + { + var path = Path.Combine(AppContext.BaseDirectory, "version.txt"); + + // 检查文件是否存在 + if (File.Exists(path)) + { + // 文件存在时读取内容 + var info = File.ReadAllText(path); + + if (info.Contains('^')) + { + var parts = info.Split('^'); + var version = parts.Length > 0 ? parts[0].ToString() : string.Empty; + var date = parts.Length > 1 ? parts[1] : string.Empty; + var isRunScript = parts.Length > 2 ? parts[2].ToBoolean() : false; + + return new HistoryVersionInfo(version, date, isRunScript); + } + } + + // 文件不存在或内容格式不正确时返回默认值 + return new HistoryVersionInfo(string.Empty, string.Empty, false); + } + + /// + /// 获取程序目录下的脚本 SQL 文件版本 + /// + /// + private static List GetScriptSqlVersions() + { + // 获取所有脚本文件 + var path = Path.Combine(AppContext.BaseDirectory, "UpdateScripts"); + var scriptFiles = Directory.GetFiles(path, "*.sql").ToList(); + + var sqlVersions = scriptFiles + .Select(s => new SqlFileInfo(Path.GetFileNameWithoutExtension(s), s)) + .OrderBy(s => s.Version).ToList(); + return sqlVersions; + } + + /// + /// 保存当前版本信息 + /// + /// + /// + /// + private static void HandleSqlScript(IApplicationBuilder app, string sql, string sqlVersion) + { + using var scope = App.GetRequiredService().CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var isSuccess = false; + + try + { + // 开启事务 + dbContext.Ado.BeginTran(); + dbContext.Ado.ExecuteCommand(sql); + dbContext.Ado.CommitTran(); + isSuccess = true; + } + catch (Exception ex) + { + dbContext.Ado.RollbackTran(); + LogHelper.Error($"AutoVersionUpdate 执行 SQL 脚本出错,版本:{sqlVersion},错误:{ex.Message}"); + } + finally + { + if (isSuccess) + { + // 保存当前版本信息 + SetEntryAssemblyCurrentVersion(sqlVersion, true); + } + } + } + + #endregion 辅助方法 +} + +public record SqlFileInfo(string Version, string FilePath); +public record HistoryVersionInfo(string Version, string Date, bool IsRunScript); + +#endif // NET10_0_OR_GREATER \ No newline at end of file diff --git a/Admin.NET.Core/Utils/AdminResultProvider.cs b/Admin.NET.Core/Utils/AdminResultProvider.cs new file mode 100644 index 0000000..7b3c0f4 --- /dev/null +++ b/Admin.NET.Core/Utils/AdminResultProvider.cs @@ -0,0 +1,193 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 全局规范化结果 +/// +[UnifyModel(typeof(AdminResult<>))] +public class AdminResultProvider : IUnifyResultProvider +{ + /// + /// JWT 授权异常返回值 + /// + /// + /// + /// + public IActionResult OnAuthorizeException(DefaultHttpContext context, ExceptionMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode, data: metadata.Data, msg: metadata.Errors), UnifyContext.GetSerializerSettings(context)); + } + + /// + /// 异常返回值 + /// + /// + /// + /// + public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode, data: metadata.Data, msg: metadata.Errors), UnifyContext.GetSerializerSettings(context)); + } + + /// + /// 成功返回值 + /// + /// + /// + /// + public IActionResult OnSucceeded(ActionExecutedContext context, object data) + { + return new JsonResult(RESTfulResult(StatusCodes.Status200OK, true, data), UnifyContext.GetSerializerSettings(context)); + } + + /// + /// 验证失败返回值 + /// + /// + /// + /// + public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode ?? StatusCodes.Status400BadRequest, data: metadata.Data, msg: metadata.ValidationResult), UnifyContext.GetSerializerSettings(context)); + } + + /// + /// 特定状态码返回值 + /// + /// + /// + /// + /// + public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings) + { + // 设置响应状态码 + UnifyContext.SetResponseStatusCodes(context, statusCode, unifyResultSettings); + + switch (statusCode) + { + // 处理 401 状态码 + case StatusCodes.Status401Unauthorized: + var msg = "401 登录已过期,请重新登录"; + // 若存在身份验证失败消息,则返回消息内容 + if (context.Items.TryGetValue(SignatureAuthenticationDefaults.AuthenticateFailMsgKey, out var authFailMsg)) + msg = authFailMsg + ""; + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, msg: msg), + App.GetOptions()?.JsonSerializerOptions); + break; + // 处理 403 状态码 + case StatusCodes.Status403Forbidden: + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, msg: "403 禁止访问,没有权限"), + App.GetOptions()?.JsonSerializerOptions); + break; + // 处理 302 状态码 + case StatusCodes.Status302Found: + if (context.Response.Headers.TryGetValue("Location", out var redirectUrl)) + { + context.Response.Redirect(redirectUrl); + } + else + { + var errorMessage = "302 跳转失败,没有提供 Location 头信息"; + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, msg: errorMessage), + App.GetOptions()?.JsonSerializerOptions); + } + break; + } + } + + /// + /// 返回成功结果集 + /// + /// + /// + /// + public static AdminResult Ok(string message, object data = default) + { + return RESTfulResult(StatusCodes.Status200OK, true, data, message); + } + + /// + /// 返回失败结果集 + /// + /// + /// + /// + /// + public static AdminResult Error(string message, int code = StatusCodes.Status400BadRequest, object data = default) + { + return RESTfulResult(code, false, data, message); + } + + /// + /// 返回 RESTful 风格结果集 + /// + /// + /// + /// + /// + /// + private static AdminResult RESTfulResult(int statusCode, bool succeeded = default, object data = default, object msg = default) + { + //// 统一返回值脱敏处理 + //if (data?.GetType() == typeof(String)) + //{ + // data = App.GetRequiredService().ReplaceAsync(data.ToString(), '*').GetAwaiter().GetResult(); + //} + //else if (data?.GetType() == typeof(JsonResult)) + //{ + // data = App.GetRequiredService().ReplaceAsync(JSON.Serialize(data), '*').GetAwaiter().GetResult(); + //} + + return new AdminResult + { + Code = statusCode, + Message = msg is null or string ? (msg + "") : JSON.Serialize(msg), + Result = data, + Type = succeeded ? "success" : "error", + Extras = UnifyContext.Take(), + Time = DateTime.Now + }; + } +} + +/// +/// 全局返回结果 +/// +/// +public class AdminResult +{ + /// + /// 状态码 + /// + public int Code { get; set; } + + /// + /// 类型success、warning、error + /// + public string Type { get; set; } + + /// + /// 错误信息 + /// + public string Message { get; set; } + + /// + /// 数据 + /// + public T Result { get; set; } + + /// + /// 附加数据 + /// + public object Extras { get; set; } + + /// + /// 时间 + /// + public DateTime Time { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/BaseFilter.cs b/Admin.NET.Core/Utils/BaseFilter.cs new file mode 100644 index 0000000..5ab7d4a --- /dev/null +++ b/Admin.NET.Core/Utils/BaseFilter.cs @@ -0,0 +1,75 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 模糊查询条件 +/// +public class Search +{ + /// + /// 字段名称集合 + /// + public List Fields { get; set; } + + /// + /// 关键字 + /// + public string? Keyword { get; set; } +} + +/// +/// 筛选过滤条件 +/// +public class Filter +{ + /// + /// 过滤条件 + /// + public FilterLogicEnum? Logic { get; set; } + + /// + /// 筛选过滤条件子项 + /// + public IEnumerable? Filters { get; set; } + + /// + /// 字段名称 + /// + public string? Field { get; set; } + + /// + /// 逻辑运算符 + /// + public FilterOperatorEnum? Operator { get; set; } + + /// + /// 字段值 + /// + public object? Value { get; set; } +} + +/// +/// 过滤条件基类 +/// +public abstract class BaseFilter +{ + /// + /// 模糊查询条件 + /// + public Search? Search { get; set; } + + /// + /// 模糊查询关键字 + /// + public string? Keyword { get; set; } + + /// + /// 筛选过滤条件 + /// + public Filter? Filter { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/BaseIdInput.cs b/Admin.NET.Core/Utils/BaseIdInput.cs new file mode 100644 index 0000000..bdc86b2 --- /dev/null +++ b/Admin.NET.Core/Utils/BaseIdInput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 主键Id输入参数 +/// +public class BaseIdInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "Id不能为空")] + [DataValidation(ValidationTypes.Numeric)] + public virtual long Id { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/BaseImportInput.cs b/Admin.NET.Core/Utils/BaseImportInput.cs new file mode 100644 index 0000000..dbd6ad0 --- /dev/null +++ b/Admin.NET.Core/Utils/BaseImportInput.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据导入输入参数 +/// +public class BaseImportInput +{ + /// + /// 记录Id + /// + [ImporterHeader(IsIgnore = true)] + [ExporterHeader(IsIgnore = true)] + public virtual long Id { get; set; } + + /// + /// 错误信息 + /// + [ImporterHeader(IsIgnore = true)] + [ExporterHeader("错误信息", ColumnIndex = 9999, IsBold = true, IsAutoFit = true)] + public virtual string Error { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/BasePageInput.cs b/Admin.NET.Core/Utils/BasePageInput.cs new file mode 100644 index 0000000..87a029f --- /dev/null +++ b/Admin.NET.Core/Utils/BasePageInput.cs @@ -0,0 +1,57 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 全局分页查询输入参数 +/// +public class BasePageInput : BaseFilter +{ + /// + /// 当前页码 + /// + [DataValidation(ValidationTypes.Numeric)] + public virtual int Page { get; set; } = 1; + + /// + /// 页码容量 + /// + //[Range(0, 100, ErrorMessage = "页码容量超过最大限制")] + [DataValidation(ValidationTypes.Numeric)] + public virtual int PageSize { get; set; } = 20; + + /// + /// 排序字段 + /// + public virtual string Field { get; set; } + + /// + /// 排序方向 + /// + public virtual string Order { get; set; } + + /// + /// 降序排序 + /// + public virtual string DescStr { get; set; } = "descending"; +} + +/// +/// 全局分页查询输入参数(带时间) +/// +public class BasePageTimeInput : BasePageInput +{ + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/BaseStatusInput.cs b/Admin.NET.Core/Utils/BaseStatusInput.cs new file mode 100644 index 0000000..317a907 --- /dev/null +++ b/Admin.NET.Core/Utils/BaseStatusInput.cs @@ -0,0 +1,19 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 设置状态输入参数 +/// +public class BaseStatusInput : BaseIdInput +{ + /// + /// 状态 + /// + [Enum] + public StatusEnum Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/ChinaDateTimeConverter.cs b/Admin.NET.Core/Utils/ChinaDateTimeConverter.cs new file mode 100644 index 0000000..8e9c05e --- /dev/null +++ b/Admin.NET.Core/Utils/ChinaDateTimeConverter.cs @@ -0,0 +1,64 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Admin.NET.Core; + +/// +/// JSON时间序列化yyyy-MM-dd HH:mm:ss +/// +public class ChinaDateTimeConverter : DateTimeConverterBase +{ + private static readonly IsoDateTimeConverter DtConverter = new() { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" }; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return DtConverter.ReadJson(reader, objectType, existingValue, serializer); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + DtConverter.WriteJson(writer, value, serializer); + } +} + +/// +/// JSON时间序列化yyyy-MM-dd HH:mm +/// +public class ChinaDateTimeConverterHH : DateTimeConverterBase +{ + private static readonly IsoDateTimeConverter DtConverter = new() { DateTimeFormat = "yyyy-MM-dd HH:mm" }; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return DtConverter.ReadJson(reader, objectType, existingValue, serializer); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + DtConverter.WriteJson(writer, value, serializer); + } +} + +/// +/// JSON时间序列化yyyy-MM-dd +/// +public class ChinaDateTimeConverterDate : DateTimeConverterBase +{ + private static readonly IsoDateTimeConverter DtConverter = new() { DateTimeFormat = "yyyy-MM-dd" }; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return DtConverter.ReadJson(reader, objectType, existingValue, serializer); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + DtConverter.WriteJson(writer, value, serializer); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/CodeGenUtil.cs b/Admin.NET.Core/Utils/CodeGenUtil.cs new file mode 100644 index 0000000..2ce24cf --- /dev/null +++ b/Admin.NET.Core/Utils/CodeGenUtil.cs @@ -0,0 +1,290 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using DbType = SqlSugar.DbType; + +namespace Admin.NET.Core; + +/// +/// 代码生成帮助类 +/// +public static class CodeGenUtil +{ + /// + /// 转换大驼峰法命名 + /// + /// 字段名 + /// EntityBase 实体属性名称 + /// + public static string CamelColumnName(string columnName, string[] dbColumnNames) + { + if (columnName.Contains('_')) + { + var arrColName = columnName.Split('_'); + var sb = new StringBuilder(); + foreach (var col in arrColName) + { + if (col.Length > 0) + sb.Append(col[..1].ToUpper() + col[1..].ToLower()); + } + columnName = sb.ToString(); + } + else + { + var propertyName = dbColumnNames.FirstOrDefault(c => c.ToLower() == columnName.ToLower()); + if (!string.IsNullOrEmpty(propertyName)) + { + columnName = propertyName; + } + else + { + columnName = columnName[..1].ToUpper() + columnName[1..].ToLower(); + } + } + return columnName; + } + + // 根据数据库类型来处理对应的数据字段类型 + public static string ConvertDataType(DbColumnInfo dbColumnInfo, DbType dbType = DbType.Custom) + { + if (dbType == DbType.Custom) + dbType = App.GetOptions().ConnectionConfigs[0].DbType; + + var dataType = dbType switch + { + DbType.Oracle => ConvertDataTypeOracleSql(string.IsNullOrEmpty(dbColumnInfo.OracleDataType) ? dbColumnInfo.DataType : dbColumnInfo.OracleDataType, dbColumnInfo.Length, dbColumnInfo.Scale), + DbType.PostgreSQL => ConvertDataTypePostgresSql(dbColumnInfo.DataType), + _ => ConvertDataTypeDefault(dbColumnInfo.DataType), + }; + return dataType + (dbColumnInfo.IsNullable ? "?" : ""); + } + + public static string ConvertDataTypeOracleSql(string dataType, int? length, int? scale) + { + switch (dataType.ToLower()) + { + case "interval year to month": return "int"; + + case "interval day to second": return "TimeSpan"; + + case "smallint": return "Int16"; + + case "int": + case "integer": return "int"; + + case "long": return "long"; + + case "float": return "float"; + + case "decimal": return "decimal"; + + case "number": + if (length == null) return "decimal"; + return scale switch + { + > 0 => "decimal", + 0 or null when length is > 1 and < 12 => "int", + 0 or null when length > 11 => "long", + _ => length == 1 ? "bool" : "decimal" + }; + + case "char": + case "clob": + case "nclob": + case "nchar": + case "nvarchar": + case "varchar": + case "nvarchar2": + case "varchar2": + case "rowid": + return "string"; + + case "timestamp": + case "timestamp with time zone": + case "timestamptz": + case "timestamp without time zone": + case "date": + case "time": + case "time with time zone": + case "timetz": + case "time without time zone": + return "DateTime"; + + case "bfile": + case "blob": + case "raw": + return "byte[]"; + + default: + return "object"; + } + } + + //PostgresSQL数据类型对应的字段类型 + public static string ConvertDataTypePostgresSql(string dataType) + { + switch (dataType) + { + case "int2": + case "smallint": + return "Int16"; + + case "int4": + case "integer": + return "int"; + + case "int8": + case "bigint": + return "long"; + + case "float4": + case "real": + return "float"; + + case "float8": + case "double precision": + return "double"; + + case "numeric": + case "decimal": + case "path": + case "point": + case "polygon": + case "interval": + case "lseg": + case "macaddr": + case "money": + return "decimal"; + + case "boolean": + case "bool": + case "box": + case "bytea": + return "bool"; + + case "varchar": + case "character varying": + case "geometry": + case "name": + case "text": + case "char": + case "character": + case "cidr": + case "circle": + case "tsquery": + case "tsvector": + case "txid_snapshot": + case "xml": + case "json": + return "string"; + + case "uuid": + return "Guid"; + + case "timestamp": + case "timestamp with time zone": + case "timestamptz": + case "timestamp without time zone": + case "date": + case "time": + case "time with time zone": + case "timetz": + case "time without time zone": + return "DateTime"; + + case "bit": + case "bit varying": + return "byte[]"; + + case "varbit": + return "byte"; + + default: + return "object"; + } + } + + public static string ConvertDataTypeDefault(string dataType) + { + return dataType.ToLower() switch + { + "tinytext" or "mediumtext" or "longtext" or "mid" or "text" or "varchar" or "char" or "nvarchar" or "nchar" or "string" or "timestamp" => "string", + "int" or "integer" or "int32" => "int", + "smallint" => "Int16", + //"tinyint" => "byte", + "tinyint" => "bool", // MYSQL + "bigint" or "int64" => "long", + "bit" or "boolean" => "bool", + "money" or "smallmoney" or "numeric" or "decimal" => "decimal", + "real" => "Single", + "datetime" or "datetime2" or "smalldatetime" => "DateTime", + "date" => "DateOnly",// MYSQL + "time" => "TimeOnly",// MYSQL + "float" or "double" => "double", + "image" or "binary" or "varbinary" => "byte[]", + "uniqueidentifier" => "Guid", + _ => "object", + }; + } + + /// + /// 数据类型转显示类型 + /// + /// + /// + public static string DataTypeToEff(string dataType) + { + if (string.IsNullOrEmpty(dataType)) return ""; + return dataType.TrimEnd('?') switch + { + "string" => "Input", + "int" => "InputNumber", + "long" => "Input", + "float" => "InputNumber", + "double" => "InputNumber", + "decimal" => "InputNumber", + "bool" => "Switch", + "Guid" => "Input", + "DateTime" => "DatePicker", + _ => "Input", + }; + } + + // 是否通用字段 + public static bool IsCommonColumn(string columnName) + { + var columnList = new List() + { + nameof(EntityBaseOrg.OrgId), + nameof(EntityBaseTenant.TenantId), + nameof(EntityBase.CreateTime), + nameof(EntityBase.UpdateTime), + nameof(EntityBase.CreateUserId), + nameof(EntityBase.UpdateUserId), + nameof(EntityBase.CreateUserName), + nameof(EntityBase.UpdateUserName), + nameof(EntityBaseDel.IsDelete) + }; + return columnList.Contains(columnName); + } + + /// + /// 获取类型的PropertyInfo列表 + /// + /// + /// + public static PropertyInfo[] GetPropertyInfoArray(Type type) + { + PropertyInfo[] props = null; + try + { + props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + } + catch + { } + return props; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/CommonUtil.cs b/Admin.NET.Core/Utils/CommonUtil.cs new file mode 100644 index 0000000..b2f824b --- /dev/null +++ b/Admin.NET.Core/Utils/CommonUtil.cs @@ -0,0 +1,503 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using IPTools.Core; +using Magicodes.ExporterAndImporter.Core.Models; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; + +namespace Admin.NET.Core; + +/// +/// 通用工具类 +/// +public static class CommonUtil +{ + private static readonly SysCacheService SysCacheService = App.GetRequiredService(); + private static readonly SysFileService SysFileService = App.GetRequiredService(); + private static readonly SqlSugarRepository SysDictDataRep = App.GetRequiredService>(); + + /// + /// 根据字符串获取固定整型哈希值 + /// + /// + /// + /// + public static long GetFixedHashCode(string str, long startNumber = 0) + { + if (string.IsNullOrWhiteSpace(str)) return 0; + unchecked + { + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; + for (int i = 0; i < str.Length; i += 2) + { + hash1 = ((hash1 << 5) + hash1) ^ str[i]; + if (i == str.Length - 1) break; + hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; + } + return startNumber + Math.Abs(hash1 + (hash2 * 1566083941)); + } + } + + /// + /// 生成百分数 + /// + /// + /// + /// + public static string ExecPercent(decimal passCount, decimal allCount) + { + string res = ""; + if (allCount > 0) + { + var value = (double)Math.Round(passCount / allCount * 100, 1); + if (value < 0) + res = Math.Round(value + 5 / Math.Pow(10, 0 + 1), 0, MidpointRounding.AwayFromZero).ToString(); + else + res = Math.Round(value, 0, MidpointRounding.AwayFromZero).ToString(); + } + if (res == "") res = "0"; + return res + "%"; + } + + /// + /// 获取服务地址 + /// + /// + public static string GetLocalhost() + { + string result = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}"; + + // 代理模式:获取真正的本机地址 + // X-Original-Host=原始请求 + // X-Forwarded-Server=从哪里转发过来 + if (App.HttpContext.Request.Headers.ContainsKey("Origin")) // 配置成完整的路径如(结尾不要带"/"),比如 https://www.abc.com + result = $"{App.HttpContext.Request.Headers["Origin"]}"; + else if (App.HttpContext.Request.Headers.ContainsKey("X-Original")) // 配置成完整的路径如(结尾不要带"/"),比如 https://www.abc.com + result = $"{App.HttpContext.Request.Headers["X-Original"]}"; + else if (App.HttpContext.Request.Headers.ContainsKey("X-Original-Host")) + result = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Headers["X-Original-Host"]}"; + return result + (string.IsNullOrWhiteSpace(App.Settings.VirtualPath) ? "" : App.Settings.VirtualPath); + } + + /// + /// 对象序列化XML + /// + /// + /// + /// + public static string SerializeObjectToXml(T obj) + { + if (obj == null) return string.Empty; + + var xs = new XmlSerializer(obj.GetType()); + var stream = new MemoryStream(); + var setting = new XmlWriterSettings + { + Encoding = new UTF8Encoding(false), // 不包含BOM + Indent = true // 设置格式化缩进 + }; + using (var writer = XmlWriter.Create(stream, setting)) + { + var ns = new XmlSerializerNamespaces(); + ns.Add("", ""); // 去除默认命名空间 + xs.Serialize(writer, obj, ns); + } + return Encoding.UTF8.GetString(stream.ToArray()); + } + + /// + /// 字符串转XML格式 + /// + /// + /// + public static XElement SerializeStringToXml(string xmlStr) + { + try + { + return XElement.Parse(xmlStr); + } + catch + { + return null; + } + } + + /// + /// 导出模板Excel + /// + /// + public static async Task ExportExcelTemplate(string fileName = null) where T : class, new() + { + IImporter importer = new ExcelImporter(); + var res = await importer.GenerateTemplateBytes(); + + return new FileContentResult(res, "application/octet-stream") { FileDownloadName = $"{(string.IsNullOrEmpty(fileName) ? typeof(T).Name : fileName)}.xlsx" }; + } + + /// + /// 导出数据excel + /// + /// + public static async Task ExportExcelData(ICollection data, string fileName = null) where T : class, new() + { + var export = new ExcelExporter(); + var res = await export.ExportAsByteArray(data); + + return new FileContentResult(res, "application/octet-stream") { FileDownloadName = $"{(string.IsNullOrEmpty(fileName) ? typeof(T).Name : fileName)}.xlsx" }; + } + + /// + /// 导出数据excel,包括字典转换 + /// + /// + public static async Task ExportExcelData(ISugarQueryable query, Func action = null) + where TSource : class, new() where TTarget : class, new() + { + var propMappings = GetExportPropertMap(); + var data = query.ToList(); + //相同属性复制值,字典值转换 + var result = new List(); + foreach (var item in data) + { + var newData = new TTarget(); + foreach (var dict in propMappings) + { + var targetProp = dict.Value.Item3; + if (targetProp != null) + { + var propertyInfo = dict.Value.Item2; + var sourceVal = propertyInfo.GetValue(item, null); + if (sourceVal == null) + { + continue; + } + + var map = dict.Value.Item1; + if (map != null && map.TryGetValue(sourceVal, out string newVal1)) + { + targetProp.SetValue(newData, newVal1); + } + else + { + if (targetProp.PropertyType.FullName == propertyInfo.PropertyType.FullName) + { + targetProp.SetValue(newData, sourceVal); + } + else + { + var newVal = sourceVal.ToString().ParseTo(targetProp.PropertyType); + targetProp.SetValue(newData, newVal); + } + } + } + if (action != null) + { + newData = action(item, newData); + } + } + result.Add(newData); + } + var export = new ExcelExporter(); + var res = await export.ExportAsByteArray(result); + + return new FileContentResult(res, "application/octet-stream") { FileDownloadName = typeof(TTarget).Name + ".xlsx" }; + } + + /// + /// 导入数据Excel + /// + /// + /// + public static async Task> ImportExcelData([Required] IFormFile file) where T : class, new() + { + IImporter importer = new ExcelImporter(); + var res = await importer.Import(file.OpenReadStream()); + var message = string.Empty; + + if (!res.HasError) return res.Data; + + if (res.Exception != null) + message += $"\r\n{res.Exception.Message}"; + foreach (DataRowErrorInfo drErrorInfo in res.RowErrors) + { + int rowNum = drErrorInfo.RowIndex; + foreach (var item in drErrorInfo.FieldErrors) + message += $"\r\n{item.Key}:{item.Value}(文件第{drErrorInfo.RowIndex}行)"; + } + message += "\r\n字段缺失:" + string.Join(",", res.TemplateErrors.Select(m => m.RequireColumnName).ToList()); + throw Oops.Oh("导入异常:" + message); + } + + /// + /// 导入Excel数据并错误标记 + /// + /// + /// + /// + /// + public static async Task> ImportExcelData([Required] IFormFile file, Func, ImportResult> importResultCallback = null) where T : class, new() + { + IImporter importer = new ExcelImporter(); + var resultStream = new MemoryStream(); + var res = await importer.Import(file.OpenReadStream(), resultStream, importResultCallback); + resultStream.Seek(0, SeekOrigin.Begin); + var userId = App.User?.FindFirst(ClaimConst.UserId)?.Value; + + SysCacheService.Remove(CacheConst.KeyExcelTemp + userId); + SysCacheService.Set(CacheConst.KeyExcelTemp + userId, resultStream, TimeSpan.FromMinutes(5)); + + var message = string.Empty; + if (!res.HasError) return res.Data; + + if (res.Exception != null) + message += $"\r\n{res.Exception.Message}"; + foreach (DataRowErrorInfo drErrorInfo in res.RowErrors) + { + message = drErrorInfo.FieldErrors.Aggregate(message, (current, item) => current + $"\r\n{item.Key}:{item.Value}(文件第{drErrorInfo.RowIndex}行)"); + } + if (res.TemplateErrors.Count > 0) + message += "\r\n字段缺失:" + string.Join(",", res.TemplateErrors.Select(m => m.RequireColumnName).ToList()); + + if (message.Length > 200) + message = message.Substring(0, 200) + "...\r\n异常过多,建议下载错误标记文件查看详细错误信息并重新导入。"; + throw Oops.Oh("导入异常:" + message); + } + + /// + /// 导入数据Excel + /// + /// + /// + /// + public static async Task> ImportExcelDataAsync([Required] IFormFile file) where T : class, new() + { + var newFile = await SysFileService.UploadFile(new UploadFileInput { File = file }); + + await using var fileStream = await SysFileService.GetFileStream(newFile); + + IImporter importer = new ExcelImporter(); + var res = await importer.Import(fileStream); + + // 删除文件 + _ = SysFileService.DeleteFile(new BaseIdInput { Id = newFile.Id }); + + if (res == null) + throw Oops.Oh("导入数据为空"); + if (res.Exception != null) + throw Oops.Oh("导入异常:" + res.Exception); + if (res.TemplateErrors?.Count > 0) + throw Oops.Oh("模板异常:" + res.TemplateErrors.Select(x => $"[{x.RequireColumnName}]{x.Message}").Join("\n")); + + return res.Data.ToList(); + } + + // 例:List ls = CommonUtil.ParseList(importResult.Data); + /// + /// 对象转换 含字典转换 + /// + /// + /// + /// + /// + /// + public static List ParseList(IEnumerable data, Func action = null) where TTarget : new() + { + var propMappings = GetImportPropertMap(); + // 相同属性复制值,字典值转换 + var result = new List(); + foreach (var item in data) + { + var newData = new TTarget(); + foreach (var dict in propMappings) + { + var targeProp = dict.Value.Item3; + if (targeProp != null) + { + var propertyInfo = dict.Value.Item2; + var sourceVal = propertyInfo.GetValue(item, null); + if (sourceVal == null) + continue; + + var map = dict.Value.Item1; + if (map != null && map.ContainsKey(sourceVal.ToString())) + { + var newVal = map[sourceVal.ToString()]; + targeProp.SetValue(newData, newVal); + } + else + { + if (targeProp.PropertyType.FullName == propertyInfo.PropertyType.FullName) + { + targeProp.SetValue(newData, sourceVal); + } + else + { + var newVal = sourceVal.ToString().ParseTo(targeProp.PropertyType); + targeProp.SetValue(newData, newVal); + } + } + } + } + if (action != null) + newData = action(item, newData); + + if (newData != null) + result.Add(newData); + } + return result; + } + + /// + /// 获取导入属性映射 + /// + /// + /// + /// 整理导入对象的 属性名称, 字典数据,原属性信息,目标属性信息 + private static Dictionary, PropertyInfo, PropertyInfo>> GetImportPropertMap() where TTarget : new() + { + // 整理导入对象的属性名称,<字典数据,原属性信息,目标属性信息> + var propMappings = new Dictionary, PropertyInfo, PropertyInfo>>(); + + var dictService = App.GetRequiredService>(); + var tSourceProps = typeof(TSource).GetProperties().ToList(); + var tTargetProps = typeof(TTarget).GetProperties().ToDictionary(u => u.Name); + foreach (var propertyInfo in tSourceProps) + { + var attrs = propertyInfo.GetCustomAttribute(); + if (attrs != null && !string.IsNullOrWhiteSpace(attrs.TypeCode)) + { + var targetProp = tTargetProps[attrs.TargetPropName]; + var mappingValues = dictService.Context.Queryable((u, a) => + new JoinQueryInfos(JoinType.Inner, u.Id == a.DictTypeId)) + .Where(u => u.Code == attrs.TypeCode) + .Where((u, a) => u.Status == StatusEnum.Enable && a.Status == StatusEnum.Enable) + .Select((u, a) => new + { + Label = a.Label, + Value = a.Value + }).ToList() + .ToDictionary(u => u.Label, u => u.Value.ParseTo(targetProp.PropertyType)); + propMappings.Add(propertyInfo.Name, new Tuple, PropertyInfo, PropertyInfo>(mappingValues, propertyInfo, targetProp)); + } + else + { + propMappings.Add(propertyInfo.Name, new Tuple, PropertyInfo, PropertyInfo>( + null, propertyInfo, tTargetProps.ContainsKey(propertyInfo.Name) ? tTargetProps[propertyInfo.Name] : null)); + } + } + + return propMappings; + } + + /// + /// 获取导出属性映射 + /// + /// + /// + /// 整理导入对象的 属性名称, 字典数据,原属性信息,目标属性信息 + private static Dictionary, PropertyInfo, PropertyInfo>> GetExportPropertMap() where TTarget : new() + { + // 整理导入对象的属性名称,<字典数据,原属性信息,目标属性信息> + var propMappings = new Dictionary, PropertyInfo, PropertyInfo>>(); + + var targetProps = typeof(TTarget).GetProperties().ToList(); + var sourceProps = typeof(TSource).GetProperties().ToDictionary(u => u.Name); + foreach (var propertyInfo in targetProps) + { + var attrs = propertyInfo.GetCustomAttribute(); + if (attrs != null && !string.IsNullOrWhiteSpace(attrs.TypeCode)) + { + var targetProp = sourceProps[attrs.TargetPropName]; + var mappingValues = SysDictDataRep.Context.Queryable((u, a) => + new JoinQueryInfos(JoinType.Inner, u.Id == a.DictTypeId)) + .Where(u => u.Code == attrs.TypeCode) + .Where((u, a) => u.Status == StatusEnum.Enable && a.Status == StatusEnum.Enable) + .Select((u, a) => new + { + a.Label, + a.Value + }).ToList() + .ToDictionary(u => u.Value.ParseTo(targetProp.PropertyType), u => u.Label); + propMappings.Add(propertyInfo.Name, new Tuple, PropertyInfo, PropertyInfo>(mappingValues, targetProp, propertyInfo)); + } + else + { + propMappings.Add(propertyInfo.Name, new Tuple, PropertyInfo, PropertyInfo>( + null, sourceProps.TryGetValue(propertyInfo.Name, out PropertyInfo prop) ? prop : null, propertyInfo)); + } + } + + return propMappings; + } + + /// + /// 获取属性映射 + /// + /// + /// 整理导入对象的 属性名称, 字典数据,原属性信息,目标属性信息 + private static Dictionary> GetExportDictMap() where TTarget : new() + { + // 整理导入对象的属性名称,目标属性名,字典Code + var propMappings = new Dictionary>(); + var tTargetProps = typeof(TTarget).GetProperties(); + foreach (var propertyInfo in tTargetProps) + { + var attrs = propertyInfo.GetCustomAttribute(); + if (attrs != null && !string.IsNullOrWhiteSpace(attrs.TypeCode)) + { + propMappings.Add(propertyInfo.Name, new Tuple(attrs.TargetPropName, attrs.TypeCode)); + } + } + + return propMappings; + } + + /// + /// 解析IP地址 + /// + /// + /// + public static (string ipLocation, double? longitude, double? latitude) GetIpAddress(string ip) + { + try + { + var ipInfo = IpTool.SearchWithI18N(ip); // 国际化查询,默认中文 中文zh-CN、英文en + var addressList = new List() { ipInfo.Country, ipInfo.Province, ipInfo.City, ipInfo.NetworkOperator }; + return (string.Join(" ", addressList.Where(u => u != "0" && !string.IsNullOrWhiteSpace(u)).ToList()), ipInfo.Longitude, ipInfo.Latitude); // 去掉0及空并用空格连接 + } + catch + { + // 不做处理 + } + return ("未知", 0, 0); + } + + /// + /// 获取客户端设备信息(操作系统+浏览器) + /// + /// + /// + public static string GetClientDeviceInfo(string userAgent) + { + try + { + if (userAgent != null) + { + var client = Parser.GetDefault().Parse(userAgent); + if (client.Device.IsSpider) + return "爬虫"; + return $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}" + + $"|{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}"; + } + } + catch + { } + return "未知"; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/ComputerUtil.cs b/Admin.NET.Core/Utils/ComputerUtil.cs new file mode 100644 index 0000000..78d05ec --- /dev/null +++ b/Admin.NET.Core/Utils/ComputerUtil.cs @@ -0,0 +1,550 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class ComputerUtil +{ + /// + /// 内存信息 + /// + /// + public static MemoryMetrics GetComputerInfo() + { + MemoryMetrics memoryMetrics; + if (IsMacOS()) + { + memoryMetrics = MemoryMetricsClient.GetMacOSMetrics(); + } + else if (IsUnix()) + { + memoryMetrics = MemoryMetricsClient.GetUnixMetrics(); + } + else + { + memoryMetrics = MemoryMetricsClient.GetWindowsMetrics(); + } + memoryMetrics.FreeRam = Math.Round(memoryMetrics.Free / 1024, 2) + "GB"; + memoryMetrics.UsedRam = Math.Round(memoryMetrics.Used / 1024, 2) + "GB"; + memoryMetrics.TotalRam = Math.Round(memoryMetrics.Total / 1024, 2) + "GB"; + memoryMetrics.RamRate = Math.Ceiling(100 * memoryMetrics.Used / memoryMetrics.Total) + "%"; + var cpuRates = GetCPURates(); + if (cpuRates != null) + { + memoryMetrics.CpuRates = cpuRates.Select(u => Math.Ceiling(u.ParseToDouble()) + "%").ToList(); + } + memoryMetrics.CpuRate = memoryMetrics.CpuRates[0]; + return memoryMetrics; + } + + /// + /// 获取正确的操作系统版本(Linux获取发行版本) + /// + /// + public static String GetOSInfo() + { + string operation = string.Empty; + if (IsMacOS()) + { + var output = ShellUtil.Bash("sw_vers | awk 'NR<=2{printf \"%s \", $NF}'"); + if (output != null) + { + operation = output.Replace("%", string.Empty); + } + } + else if (IsUnix()) + { + var output = ShellUtil.Bash("awk -F= '/^VERSION_ID/ {print $2}' /etc/os-release | tr -d '\"'"); + operation = output ?? string.Empty; + } + else + { + operation = RuntimeInformation.OSDescription; + } + return operation; + } + + /// + /// 磁盘信息 + /// + /// + public static List GetDiskInfos() + { + var diskInfos = new List(); + if (IsMacOS()) + { + var output = ShellUtil.Bash(@"df -m | awk '/^\/dev\/disk/ {print $1,$2,$3,$4,$5}'"); + var disks = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + if (disks.Length < 1) return diskInfos; + foreach (var item in disks) + { + var disk = item.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + if (disk.Length < 5) continue; + + var diskInfo = new DiskInfo() + { + DiskName = disk[0], + TypeName = ShellUtil.Bash("diskutil info " + disk[0] + " | awk '/File System Personality/ {print $4}'").Replace("\n", string.Empty), + TotalSize = Math.Round(long.Parse(disk[1]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + Used = Math.Round(long.Parse(disk[2]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailableFreeSpace = Math.Round(long.Parse(disk[3]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailablePercent = decimal.Parse(disk[4].Replace("%", "")) + }; + diskInfos.Add(diskInfo); + } + } + else if (IsUnix()) + { + var output = ShellUtil.Bash(@"df -mT | awk '/^\/dev\/(sd|vd|xvd|nvme|sda|vda|mapper)/ {print $1,$2,$3,$4,$5,$6}'"); + var disks = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + if (disks.Length < 1) return diskInfos; + + //var rootDisk = disks[1].Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + //if (rootDisk == null || rootDisk.Length < 1) + // return diskInfos; + + foreach (var item in disks) + { + var disk = item.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + if (disk.Length < 6) continue; + + var diskInfo = new DiskInfo() + { + DiskName = disk[0], + TypeName = disk[1], + TotalSize = Math.Round(long.Parse(disk[2]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + Used = Math.Round(long.Parse(disk[3]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailableFreeSpace = Math.Round(long.Parse(disk[4]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailablePercent = decimal.Parse(disk[5].Replace("%", "")) + }; + diskInfos.Add(diskInfo); + } + } + else + { + var driveList = DriveInfo.GetDrives().Where(u => u.IsReady); + foreach (var item in driveList) + { + if (item.DriveType == DriveType.CDRom) continue; + var diskInfo = new DiskInfo() + { + DiskName = item.Name, + TypeName = item.DriveType.ToString(), + TotalSize = Math.Round(item.TotalSize / 1024 / 1024 / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailableFreeSpace = Math.Round(item.AvailableFreeSpace / 1024 / 1024 / 1024.0m, 2, MidpointRounding.AwayFromZero), + }; + diskInfo.Used = diskInfo.TotalSize - diskInfo.AvailableFreeSpace; + diskInfo.AvailablePercent = decimal.Ceiling(diskInfo.Used / (decimal)diskInfo.TotalSize * 100); + diskInfos.Add(diskInfo); + } + } + return diskInfos; + } + + /// + /// 获取外网IP地址 + /// + /// + public static string GetIpFromOnline() + { + try + { + var url = "https://4.ipw.cn"; + var httpRemoteService = App.GetRequiredService(); + var ip = httpRemoteService.GetAsString(url); + var (ipLocation, _, _) = CommonUtil.GetIpAddress(ip); + return ip + " " + ipLocation; + } + catch + { + return "unknow"; + } + } + + public static bool IsUnix() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } + + public static bool IsMacOS() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + + public static List GetCPURates() + { + var cpuRates = new List(); + string output = ""; + if (IsMacOS()) + { + output = ShellUtil.Bash("top -l 1 | grep \"CPU usage\" | awk '{print $3 + $5}'"); + cpuRates.Add(output.Trim()); + } + else if (IsUnix()) + { + output = ShellUtil.Bash("awk '{u=$2+$4; t=$2+$4+$5; if (NR==1){u1=u; t1=t;} else print ($2+$4-u1) * 100 / (t-t1); }' <(grep 'cpu ' /proc/stat) <(sleep 1;grep 'cpu ' /proc/stat)"); + cpuRates.Add(output.Trim()); + } + else + { + try + { + output = ShellUtil.Cmd("wmic", "cpu get LoadPercentage"); + } + catch (Exception) + { + output = ShellUtil.PowerShell("Get-CimInstance -ClassName Win32_Processor | Select-Object LoadPercentage"); + output = output.Replace("@", string.Empty).Replace("{", string.Empty).Replace("}", string.Empty).Replace("=", string.Empty).Trim(); + } + cpuRates.AddRange(output.Replace("LoadPercentage", string.Empty).Trim().Split("\r\r\n")); + } + return cpuRates; + } + + /// + /// 获取系统运行时间 + /// + /// + public static string GetRunTime() + { + string runTime = string.Empty; + string output = ""; + if (IsMacOS()) + { + // macOS 获取系统启动时间: + // sysctl -n kern.boottime | awk '{print $4}' | tr -d ',' + // 返回:1705379131 + // 使用date格式化即可 + output = ShellUtil.Bash("date -r $(sysctl -n kern.boottime | awk '{print $4}' | tr -d ',') +\"%Y-%m-%d %H:%M:%S\"").Trim(); + runTime = DateTimeUtil.FormatTime((DateTime.Now - output.ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + else if (IsUnix()) + { + output = ShellUtil.Bash("date -d \"$(awk -F. '{print $1}' /proc/uptime) second ago\" +\"%Y-%m-%d %H:%M:%S\"").Trim(); + runTime = DateTimeUtil.FormatTime((DateTime.Now - output.ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + else + { + try + { + output = ShellUtil.Cmd("wmic", "OS get LastBootUpTime/Value"); + string[] outputArr = output.Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + if (outputArr.Length == 2) + runTime = DateTimeUtil.FormatTime((DateTime.Now - outputArr[1].Split('.')[0].ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + catch (Exception) + { + output = ShellUtil.PowerShell("Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object LastBootUpTime"); + output = output.Replace("LastBootUpTime", string.Empty).Replace("@", string.Empty).Replace("{", string.Empty).Replace("}", string.Empty).Replace("=", string.Empty).Trim(); + runTime = DateTimeUtil.FormatTime((DateTime.Now - output.ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + } + return runTime; + } +} + +/// +/// 内存信息 +/// +public class MemoryMetrics +{ + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public double Total { get; set; } + + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public double Used { get; set; } + + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public double Free { get; set; } + + /// + /// 已用内存 + /// + public string UsedRam { get; set; } + + /// + /// CPU使用率% + /// + public List CpuRates { get; set; } + + public string CpuRate { get; set; } + + /// + /// 总内存 GB + /// + public string TotalRam { get; set; } + + /// + /// 内存使用率 % + /// + public string RamRate { get; set; } + + /// + /// 空闲内存 + /// + public string FreeRam { get; set; } +} + +/// +/// 磁盘信息 +/// +public class DiskInfo +{ + /// + /// 磁盘名 + /// + public string DiskName { get; set; } + + /// + /// 类型名 + /// + public string TypeName { get; set; } + + /// + /// 总剩余 + /// + public decimal TotalFree { get; set; } + + /// + /// 总量 + /// + public decimal TotalSize { get; set; } + + /// + /// 已使用 + /// + public decimal Used { get; set; } + + /// + /// 可使用 + /// + public decimal AvailableFreeSpace { get; set; } + + /// + /// 使用百分比 + /// + public decimal AvailablePercent { get; set; } +} + +public class MemoryMetricsClient +{ + /// + /// windows系统获取内存信息 + /// + /// + public static MemoryMetrics GetWindowsMetrics() + { + string output = ""; + var metrics = new MemoryMetrics(); + try + { + output = ShellUtil.Cmd("wmic", "OS get FreePhysicalMemory,TotalVisibleMemorySize /Value"); + var lines = output.Trim().Split('\n', (char)StringSplitOptions.RemoveEmptyEntries); + if (lines.Length <= 1) return metrics; + + var freeMemoryParts = lines[0].Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + var totalMemoryParts = lines[1].Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + metrics.Total = Math.Round(double.Parse(totalMemoryParts[1]) / 1024, 0); + metrics.Free = Math.Round(double.Parse(freeMemoryParts[1]) / 1024, 0);//m + } + catch (Exception) + { + output = ShellUtil.PowerShell("Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object FreePhysicalMemory, TotalVisibleMemorySize"); + output = output.Replace("@", string.Empty).Replace("{", string.Empty).Replace("}", string.Empty).Trim(); + var lines = output.Trim().Split(';', (char)StringSplitOptions.RemoveEmptyEntries); + + // 跳过表头与分隔线(通常为前两行) + if (lines.Length >= 2) + { + // 解析并转换为MB(原单位为KB) + metrics.Free = Math.Round(double.Parse(lines[0].Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries)[1]) / 1024, 0); + metrics.Total = Math.Round(double.Parse(lines[1].Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries)[1]) / 1024, 0); + } + } + metrics.Used = metrics.Total - metrics.Free; + + return metrics; + } + + /// + /// Unix系统获取 + /// + /// + public static MemoryMetrics GetUnixMetrics() + { + string output = ShellUtil.Bash("awk '/MemTotal/ {total=$2} /MemAvailable/ {available=$2} END {print total,available}' /proc/meminfo"); + var metrics = new MemoryMetrics(); + var memory = output.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + if (memory.Length != 2) return metrics; + + metrics.Total = double.Parse(memory[0]) / 1024; + metrics.Free = double.Parse(memory[1]) / 1024; + metrics.Used = metrics.Total - metrics.Free; + return metrics; + } + + /// + /// macOS系统获取 + /// + /// + public static MemoryMetrics GetMacOSMetrics() + { + var metrics = new MemoryMetrics(); + //物理内存大小 + var total = ShellUtil.Bash("sysctl -n hw.memsize | awk '{printf \"%.2f\", $1/1024/1024}'"); + metrics.Total = float.Parse(total.Replace("%", string.Empty)); + //TODO:占用内存,检查效率 + var free = ShellUtil.Bash("top -l 1 -s 0 | awk '/PhysMem/ {print $6+$8}'"); + metrics.Free = float.Parse(free); + metrics.Used = metrics.Total - metrics.Free; + return metrics; + } +} + +public class ShellUtil +{ + /// + /// linux 系统命令 + /// + /// + /// + public static string Bash(string command) + { + var escapedArgs = command.Replace("\"", "\\\""); + var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + process.Dispose(); + return result; + } + + /// + /// windows CMD 系统命令 + /// + /// + /// + /// + public static string Cmd(string fileName, string args) + { + var info = new ProcessStartInfo + { + FileName = fileName, + Arguments = args, + RedirectStandardOutput = true + }; + + var output = string.Empty; + using (var process = Process.Start(info)) + { + output = process.StandardOutput.ReadToEnd(); + } + return output; + } + + /// + /// Windows POWERSHELL 系统命令 + /// + /// + /// + public static string PowerShell(string script) + { + using var PowerShellInstance = System.Management.Automation.PowerShell.Create(); + PowerShellInstance.AddScript(script); + var PSOutput = PowerShellInstance.Invoke(); + + var output = new StringBuilder(); + foreach (var outputItem in PSOutput) + { + output.AppendLine(outputItem.ToString()); + } + return output.ToString(); + } +} + +public class ShellHelper +{ + /// + /// Linux 系统命令 + /// + /// + /// + public static string Bash(string command) + { + var escapedArgs = command.Replace("\"", "\\\""); + var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + process.Dispose(); + return result; + } + + /// + /// Windows CMD 系统命令 + /// + /// + /// + /// + public static string Cmd(string fileName, string args) + { + var info = new ProcessStartInfo + { + FileName = fileName, + Arguments = args, + RedirectStandardOutput = true + }; + + var output = string.Empty; + using (var process = Process.Start(info)) + { + output = process.StandardOutput.ReadToEnd(); + } + return output; + } + + /// + /// Windows POWERSHELL 系统命令 + /// + /// + /// + public static string PowerShell(string script) + { + using var PowerShellInstance = System.Management.Automation.PowerShell.Create(); + PowerShellInstance.AddScript(script); + var PSOutput = PowerShellInstance.Invoke(); + + var output = new StringBuilder(); + foreach (var outputItem in PSOutput) + { + output.AppendLine(outputItem.BaseObject.ToString()); + } + return output.ToString(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/CryptogramUtil.cs b/Admin.NET.Core/Utils/CryptogramUtil.cs new file mode 100644 index 0000000..ebbeded --- /dev/null +++ b/Admin.NET.Core/Utils/CryptogramUtil.cs @@ -0,0 +1,120 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class CryptogramUtil +{ + public static readonly bool StrongPassword = App.GetConfig("Cryptogram:StrongPassword"); // 是否开启密码强度验证 + public static readonly string PasswordStrengthValidation = App.GetConfig("Cryptogram:PasswordStrengthValidation"); // 密码强度验证正则表达式 + public static readonly string PasswordStrengthValidationMsg = App.GetConfig("Cryptogram:PasswordStrengthValidationMsg"); // 密码强度验证提示 + public static readonly string CryptoType = App.GetConfig("Cryptogram:CryptoType"); // 加密类型 + public static readonly string PublicKey = App.GetConfig("Cryptogram:PublicKey"); // 公钥 + public static readonly string PrivateKey = App.GetConfig("Cryptogram:PrivateKey"); // 私钥 + + public static readonly string SM4_key = "0123456789abcdeffedcba9876543210"; + public static readonly string SM4_iv = "595298c7c6fd271f0402f804c33d3f66"; + + /// + /// 加密 + /// + /// + /// + public static string Encrypt(string plainText) + { + if (CryptoType == CryptogramEnum.MD5.ToString()) + { + return MD5Encryption.Encrypt(plainText); + } + else if (CryptoType == CryptogramEnum.SM2.ToString()) + { + return SM2Encrypt(plainText); + } + else if (CryptoType == CryptogramEnum.SM4.ToString()) + { + return SM4EncryptECB(plainText); + } + return plainText; + } + + /// + /// 解密 + /// + /// + /// + public static string Decrypt(string cipherText) + { + if (CryptoType == CryptogramEnum.SM2.ToString()) + { + return SM2Decrypt(cipherText); + } + else if (CryptoType == CryptogramEnum.SM4.ToString()) + { + return SM4DecryptECB(cipherText); + } + return cipherText; + } + + /// + /// SM2加密 + /// + /// + /// + public static string SM2Encrypt(string plainText) + { + return GMUtil.SM2Encrypt(PublicKey, plainText); + } + + /// + /// SM2解密 + /// + /// + /// + public static string SM2Decrypt(string cipherText) + { + return GMUtil.SM2Decrypt(PrivateKey, cipherText); + } + + /// + /// SM4加密(ECB) + /// + /// + /// + public static string SM4EncryptECB(string plainText) + { + return GMUtil.SM4EncryptECB(SM4_key, plainText); + } + + /// + /// SM4解密(ECB) + /// + /// + /// + public static string SM4DecryptECB(string cipherText) + { + return GMUtil.SM4DecryptECB(SM4_key, cipherText); + } + + /// + /// SM4加密(CBC) + /// + /// + /// + public static string SM4EncryptCBC(string plainText) + { + return GMUtil.SM4EncryptCBC(SM4_key, SM4_iv, plainText); + } + + /// + /// SM4解密(CBC) + /// + /// + /// + public static string SM4DecryptCBC(string cipherText) + { + return GMUtil.SM4DecryptCBC(SM4_key, SM4_iv, cipherText); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/CustomJsonPropertyConverter.cs b/Admin.NET.Core/Utils/CustomJsonPropertyConverter.cs new file mode 100644 index 0000000..464090f --- /dev/null +++ b/Admin.NET.Core/Utils/CustomJsonPropertyConverter.cs @@ -0,0 +1,188 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Admin.NET.Core; + +/// +/// 自定义属性名称转换器 +/// +public class CustomJsonPropertyConverter : JsonConverter +{ + public static readonly JsonSerializerOptions Options = new() + { + Converters = { new CustomJsonPropertyConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + // 缓存类型信息避免重复反射 + private static readonly ConcurrentDictionary> PropertyCache = new(); + + // 日期时间格式化配置 + private readonly string _dateTimeFormat; + + public CustomJsonPropertyConverter(string dateTimeFormat = "yyyy-MM-dd HH:mm:ss") + { + _dateTimeFormat = dateTimeFormat; + } + + public override bool CanConvert(Type typeToConvert) + { + return PropertyCache.GetOrAdd(typeToConvert, type => + type.GetProperties() + .Where(p => p.GetCustomAttribute() != null) + .Select(p => new PropertyMeta(p)) + .ToList().AsReadOnly() + ).Count > 0; + } + + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var jsonDoc = JsonDocument.ParseValue(ref reader); + var instance = Activator.CreateInstance(typeToConvert); + var properties = PropertyCache.GetOrAdd(typeToConvert, BuildPropertyMeta); + + foreach (var prop in properties) + { + if (jsonDoc.RootElement.TryGetProperty(prop.JsonName, out var value)) + { + object propertyValue; + + // 特殊处理日期时间类型 + if (IsDateTimeType(prop.PropertyType)) + { + propertyValue = HandleDateTimeValue(value, prop.PropertyType); + } + else + { + propertyValue = JsonSerializer.Deserialize( + value.GetRawText(), + prop.PropertyType, + options + ); + } + + prop.SetValue(instance, propertyValue); + } + } + + return instance; + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + var properties = PropertyCache.GetOrAdd(value.GetType(), BuildPropertyMeta); + + foreach (var prop in properties) + { + var propertyValue = prop.GetValue(value); + + writer.WritePropertyName(prop.JsonName); + + // 特殊处理日期时间类型 + if (propertyValue != null && IsDateTimeType(prop.PropertyType)) + { + writer.WriteStringValue(FormatDateTime(propertyValue)); + } + else + { + JsonSerializer.Serialize(writer, propertyValue, options); + } + } + + writer.WriteEndObject(); + } + + private static IReadOnlyList BuildPropertyMeta(Type type) + { + return type.GetProperties() + .Select(p => new PropertyMeta(p)) + .ToList().AsReadOnly(); + } + + private object HandleDateTimeValue(JsonElement value, Type targetType) + { + var dateStr = value.GetString(); + if (string.IsNullOrEmpty(dateStr)) return null; + + var date = DateTime.Parse(dateStr); + return targetType == typeof(DateTimeOffset) + ? new DateTimeOffset(date) + : (object)date; + } + + private string FormatDateTime(object dateTime) + { + return dateTime switch + { + DateTime dt => dt.ToString(_dateTimeFormat), + DateTimeOffset dto => dto.ToString(_dateTimeFormat), + _ => dateTime?.ToString() + }; + } + + private static bool IsDateTimeType(Type type) + { + var actualType = Nullable.GetUnderlyingType(type) ?? type; + return actualType == typeof(DateTime) || actualType == typeof(DateTimeOffset); + } + + private class PropertyMeta + { + private readonly PropertyInfo _property; + private readonly Func _getter; + private readonly Action _setter; + + public string JsonName { get; } + public Type PropertyType => _property.PropertyType; + + public PropertyMeta(PropertyInfo property) + { + _property = property; + JsonName = property.GetCustomAttribute()?.Name ?? property.Name; + + // 编译表达式树优化属性访问 + var instanceParam = Expression.Parameter(typeof(object), "instance"); + var valueParam = Expression.Parameter(typeof(object), "value"); + + // Getter + var getterExpr = Expression.Lambda>( + Expression.Convert( + Expression.Property( + Expression.Convert(instanceParam, property.DeclaringType), + property), + typeof(object)), + instanceParam); + _getter = getterExpr.Compile(); + + // Setter + if (property.CanWrite) + { + var setterExpr = Expression.Lambda>( + Expression.Assign( + Expression.Property( + Expression.Convert(instanceParam, property.DeclaringType), + property), + Expression.Convert(valueParam, property.PropertyType)), + instanceParam, valueParam); + _setter = setterExpr.Compile(); + } + } + + public object GetValue(object instance) => _getter(instance); + + public void SetValue(object instance, object value) + { + if (_setter != null) + { + _setter(instance, value); + } + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/DateTimeUtil.cs b/Admin.NET.Core/Utils/DateTimeUtil.cs new file mode 100644 index 0000000..e0175ae --- /dev/null +++ b/Admin.NET.Core/Utils/DateTimeUtil.cs @@ -0,0 +1,416 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 时间帮助类 +/// +public class DateTimeUtil +{ + public readonly DateTime Date; + + private DateTimeUtil(TimeSpan timeSpan = default) + { + Date = DateTime.Now.AddTicks(timeSpan.Ticks); + } + + private DateTimeUtil(DateTime time) + { + Date = time; + } + + /// + /// 实例化类 + /// + /// + /// + public static DateTimeUtil Init(TimeSpan timeSpan = default) + { + return new DateTimeUtil(timeSpan); + } + + /// + /// 实例化类 + /// + /// + /// + public static DateTimeUtil Init(DateTime time) + { + return new DateTimeUtil(time); + } + + /// + /// 根据unix时间戳的长度自动判断是秒还是以毫秒为单位 + /// + /// + /// + public static DateTime ConvertUnixTime(long unixTime) + { + // 判断时间戳长度 + bool isMilliseconds = unixTime > 9999999999; + + if (isMilliseconds) + { + return DateTimeOffset.FromUnixTimeMilliseconds(unixTime).ToLocalTime().DateTime; + } + else + { + return DateTimeOffset.FromUnixTimeSeconds(unixTime).ToLocalTime().DateTime; + } + } + + /// + /// 获取开始时间 + /// + /// + /// + /// + public static DateTime GetBeginTime(DateTime? dateTime, int days = 0) + { + return dateTime == DateTime.MinValue || dateTime == null ? DateTime.Now.AddDays(days) : (DateTime)dateTime; + } + + /// + /// 时间戳转本地时间-时间戳精确到秒 + /// + public static DateTime ToLocalTimeDateBySeconds(long unix) + { + return DateTimeOffset.FromUnixTimeSeconds(unix).ToLocalTime().DateTime; + } + + /// + /// 时间转时间戳Unix-时间戳精确到秒 + /// + public static long ToUnixTimestampBySeconds(DateTime dt) + { + return new DateTimeOffset(dt).ToUnixTimeSeconds(); + } + + /// + /// 时间戳转本地时间-时间戳精确到毫秒 + /// + public static DateTime ToLocalTimeDateByMilliseconds(long unix) + { + return DateTimeOffset.FromUnixTimeMilliseconds(unix).ToLocalTime().DateTime; + } + + /// + /// 时间转时间戳Unix-时间戳精确到毫秒 + /// + public static long ToUnixTimestampByMilliseconds(DateTime dt) + { + return new DateTimeOffset(dt).ToUnixTimeMilliseconds(); + } + + /// + /// 毫秒转天时分秒 + /// + /// TotalMilliseconds + /// 是否简化显示 + /// + public static string FormatTime(long ms, bool isSimplify = false) + { + int ss = 1000; + int mi = ss * 60; + int hh = mi * 60; + int dd = hh * 24; + + long day = ms / dd; + long hour = (ms - day * dd) / hh; + long minute = (ms - day * dd - hour * hh) / mi; + long second = (ms - day * dd - hour * hh - minute * mi) / ss; + //long milliSecond = ms - day * dd - hour * hh - minute * mi - second * ss; + + string sDay = day < 10 ? "0" + day : "" + day; //天 + string sHour = hour < 10 ? "0" + hour : "" + hour;//小时 + string sMinute = minute < 10 ? "0" + minute : "" + minute;//分钟 + string sSecond = second < 10 ? "0" + second : "" + second;//秒 + //string sMilliSecond = milliSecond < 10 ? "0" + milliSecond : "" + milliSecond;//毫秒 + //sMilliSecond = milliSecond < 100 ? "0" + sMilliSecond : "" + sMilliSecond; + + if (!isSimplify) + return $"{sDay} 天 {sHour} 小时 {sMinute} 分 {sSecond} 秒"; + else + { + string result = string.Empty; + if (day > 0) + result = $"{sDay}天"; + if (hour > 0) + result = $"{result}{sHour}小时"; + if (minute > 0) + result = $"{result}{sMinute}分"; + if (!result.IsNullOrEmpty()) + result = $"{result}{sSecond}秒"; + else + result = $"{sSecond}秒"; + return result; + } + } + + /// + /// 获取unix时间戳 + /// + /// + /// + public static long GetUnixTimeStamp(DateTime dt) + { + return ((DateTimeOffset)dt).ToUnixTimeMilliseconds(); + } + + /// + /// 获取日期天的最小时间 + /// + /// + /// + public static DateTime GetDayMinDate(DateTime dt) + { + return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0); + } + + /// + /// 获取日期天的最大时间 + /// + /// + /// + + public static DateTime GetDayMaxDate(DateTime dt) + { + return new DateTime(dt.Year, dt.Month, dt.Day, 23, 59, 59); + } + + /// + /// 根据日期是否在当前年份来格式化日期 + /// + /// + /// + public static string FormatDateTime(DateTime? dt) + { + return dt == null ? string.Empty : dt.Value.ToString(dt.Value.Year == DateTime.Now.Year ? "MM-dd HH:mm" : "yyyy-MM-dd HH:mm"); + } + + /// + /// 获取日期范围00:00:00 - 23:59:59 + /// + /// + public static List GetTodayTimeList(DateTime time) + { + return new List + { + Convert.ToDateTime(time.ToString("D")), + Convert.ToDateTime(time.AddDays(1).ToString("D")).AddSeconds(-1) + }; + } + + /// + /// 获取星期几 + /// + /// + /// + public static string GetWeekByDate(DateTime dt) + { + var day = new[] { "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六" }; + return day[Convert.ToInt32(dt.DayOfWeek.ToString("d"))]; + } + + /// + /// 获取这个月的第几周 + /// + /// + /// + public static int GetWeekNumInMonth(DateTime daytime) + { + int dayInMonth = daytime.Day; + // 本月第一天 + DateTime firstDay = daytime.AddDays(1 - daytime.Day); + // 本月第一天是周几 + int weekday = (int)firstDay.DayOfWeek == 0 ? 7 : (int)firstDay.DayOfWeek; + // 本月第一周有几天 + int firstWeekEndDay = 7 - (weekday - 1); + // 当前日期和第一周之差 + int diffday = dayInMonth - firstWeekEndDay; + diffday = diffday > 0 ? diffday : 1; + // 当前是第几周,若整除7就减一天 + return ((diffday % 7) == 0 ? (diffday / 7 - 1) : (diffday / 7)) + 1 + (dayInMonth > firstWeekEndDay ? 1 : 0); + } + + /// + /// 获取今天的时间范围 + /// + /// 返回包含开始时间和结束时间的元组 + public (DateTime Start, DateTime End) GetTodayRange() + { + var start = Date.Date; // 当天开始时间 + var end = start.AddDays(1).AddSeconds(-1); // 当天结束时间 + return (start, end); + } + + /// + /// 获取本月的时间范围 + /// + /// 返回包含开始时间和结束时间的元组 + public (DateTime Start, DateTime End) GetMonthRange() + { + return (GetFirstDayOfMonth(), GetLastDayOfMonth()); + } + + /// + /// 获取本月的第一天开始时间 + /// + /// 返回当月的第一天 + public DateTime GetFirstDayOfMonth() + { + return new DateTime(Date.Year, Date.Month, 1); + } + + /// + /// 获取本月的最后一天截至时间 + /// + /// 返回当月的最后一天 + public DateTime GetLastDayOfMonth() + { + var firstDayOfNextMonth = new DateTime(Date.Year, Date.Month, 1).AddMonths(1); + return firstDayOfNextMonth.AddSeconds(-1); + } + + /// + /// 获取今年的时间范围 + /// + public (DateTime Start, DateTime End) GetYearRange() + { + return (GetFirstDayOfYear(), GetLastDayOfYear()); + } + + /// + /// 获取今年的第一天时间范围 + /// + public DateTime GetFirstDayOfYear() + { + return new DateTime(Date.Year, 1, 1); + } + + /// + /// 获取今年的最后一天时间范围 + /// + public DateTime GetLastDayOfYear() + { + return new DateTime(Date.Year, 12, 31, 23, 59, 59); + } + + /// + /// 获取前天时间范围 + /// + public (DateTime Start, DateTime End) GetDayBeforeYesterdayRange() + { + var start = Date.Date.AddDays(-2); // 前天开始时间 + var end = start.AddDays(1).AddSeconds(-1); // 前天结束时间 + return (start, end); + } + + /// + /// 获取昨天时间范围 + /// + public (DateTime Start, DateTime End) GetYesterdayRange() + { + var start = Date.Date.AddDays(-1); // 昨天开始时间 + var end = start.AddDays(1).AddSeconds(-1); // 昨天结束时间 + return (start, end); + } + + /// + /// 获取上一周时间范围 + /// + public (DateTime Start, DateTime End) GetLastWeekRange() + { + // 计算上周的天数差 + var daysToSubtract = (int)Date.DayOfWeek + 7; // 确保周日也能正确计算 + var start = Date.Date.AddDays(-daysToSubtract); // 上周第一天 + var end = start.AddDays(7).AddSeconds(-1); // 上周最后一天 + return (start, end); + } + + /// + /// 获取本周时间范围 + /// + public (DateTime Start, DateTime End) GetThisWeekRange() + { + // 计算本周的天数差 + var daysToSubtract = (int)Date.DayOfWeek; + var start = Date.Date.AddDays(-daysToSubtract); // 本周第一天 + var end = start.AddDays(7).AddSeconds(-1); // 本周最后一天 + return (start, end); + } + + /// + /// 获取上月时间范围 + /// + public (DateTime Start, DateTime End) GetLastMonthRange() + { + var firstDayOfLastMonth = new DateTime(Date.Year, Date.Month, 1).AddMonths(-1); // 上月第一天 + var lastDayOfLastMonth = firstDayOfLastMonth.AddMonths(1).AddSeconds(-1); // 上月最后一天 + return (firstDayOfLastMonth, lastDayOfLastMonth); + } + + /// + /// 获取近3天的时间范围 + /// + public (DateTime Start, DateTime End) GetLast3DaysRange() + { + var start = Date.Date.AddDays(-2); // 3天前的开始时间 + var end = Date.Date.AddDays(1).AddSeconds(-1); // 当前日期的结束时间 + return (start, end); + } + + /// + /// 获取近7天的时间范围 + /// + public (DateTime Start, DateTime End) GetLast7DaysRange() + { + var start = Date.Date.AddDays(-6); // 7天前的开始时间 + var end = Date.Date.AddDays(1).AddSeconds(-1); // 当前日期的结束时间 + return (start, end); + } + + /// + /// 获取近15天的时间范围 + /// + public (DateTime Start, DateTime End) GetLast15DaysRange() + { + var start = Date.Date.AddDays(-14); // 15天前的开始时间 + var end = Date.Date.AddDays(1).AddSeconds(-1); // 当前日期的结束时间 + return (start, end); + } + + /// + /// 获取近3个月的时间范围 + /// + public (DateTime Start, DateTime End) GetLast3MonthsRange() + { + var start = Date.Date.AddMonths(-3); // 3个月前的开始时间 + var end = Date.Date.AddDays(1).AddSeconds(-1); // 当前日期的结束时间 + return (start, end); + } + + /// + /// 获取上半年的时间范围 + /// + public (DateTime Start, DateTime End) GetFirstHalfYearRange() + { + var start = new DateTime(Date.Year, 1, 1); // 上半年开始时间 + var end = new DateTime(Date.Year, 6, 30, 23, 59, 59); // 上半年结束时间 + return (start, end); + } + + /// + /// 获取下半年的时间范围 + /// + public (DateTime Start, DateTime End) GetSecondHalfYearRange() + { + var start = new DateTime(Date.Year, 7, 1); // 下半年开始时间 + var end = new DateTime(Date.Year, 12, 31, 23, 59, 59); // 下半年结束时间 + return (start, end); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/ExcelHelper.cs b/Admin.NET.Core/Utils/ExcelHelper.cs new file mode 100644 index 0000000..d9fa2c8 --- /dev/null +++ b/Admin.NET.Core/Utils/ExcelHelper.cs @@ -0,0 +1,147 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OfficeOpenXml; + +namespace Admin.NET.Core; + +public class ExcelHelper +{ + /// + /// 数据导入 + /// + /// + /// + /// + public static IActionResult ImportData(IFormFile file, Action, Action, List, List>> action) where IN : BaseImportInput, new() where T : EntityBaseId, new() + { + try + { + var result = CommonUtil.ImportExcelDataAsync(file).Result ?? throw Oops.Oh("有效数据为空"); + result.ForEach(u => u.Id = YitIdHelper.NextId()); + + var tasks = new List(); + action.Invoke(result, (storageable, pageItems, rows) => + { + // 标记校验信息 + tasks.Add(Task.Run(() => + { + if (!storageable.TotalList.Any()) return; + + // 通过Id标记校验信息 + var itemMap = pageItems.ToDictionary(u => u.Id, u => u); + foreach (var item in storageable.TotalList) + { + var temp = itemMap.GetValueOrDefault(item.Item.Id); + if (temp != null) temp.Error ??= item.StorageMessage; + } + })); + }); + + // 等待所有标记验证信息任务完成 + Task.WhenAll(tasks).GetAwaiter().GetResult(); + + // 仅导出错误记录 + var errorList = result.Where(u => !string.IsNullOrWhiteSpace(u.Error)).ToList(); + if (!errorList.Any()) + return new JsonResult(AdminResultProvider.Ok("导入成功")); + return ExportData(errorList); + } + catch (Exception ex) + { + return new JsonResult(AdminResultProvider.Error(ex.Message)); + } + } + + /// + /// 导出Xlsx数据 + /// + /// + /// + /// + public static IActionResult ExportData(dynamic list, string fileName = "导入记录") + { + var exporter = new ExcelExporter(); + var fs = new MemoryStream(exporter.ExportAsByteArray(list).GetAwaiter().GetResult()); + return new XlsxFileResult(stream: fs, fileDownloadName: $"{fileName}-{DateTime.Now:yyyy-MM-dd_HHmmss}"); + } + + /// + /// 根据类型导出Xlsx模板 + /// + /// + /// + /// + /// + public static IActionResult ExportTemplate(IEnumerable list, string filename = "导入模板", Func> addListValidationFun = null) + { + using var package = new ExcelPackage((ExportData(list, filename) as XlsxFileResult)!.Stream); + var worksheet = package.Workbook.Worksheets[0]; + + // 创建一个隐藏的sheet,用于添加下拉列表 + var dropdownSheet = package.Workbook.Worksheets.Add("下拉数据"); + dropdownSheet.Hidden = eWorkSheetHidden.Hidden; + + var sysDictTypeService = App.GetService(); + foreach (var prop in typeof(T).GetProperties()) + { + var propType = prop.PropertyType; + + var headerAttr = prop.GetCustomAttribute(); + var isNullableEnum = propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(Nullable<>) && Nullable.GetUnderlyingType(propType).IsEnum(); + if (isNullableEnum) propType = Nullable.GetUnderlyingType(propType); + if (headerAttr == null) continue; + + // 获取列序号 + var columnIndex = 0; + foreach (var item in worksheet.Cells[1, 1, 1, worksheet.Dimension.End.Column]) + if (++columnIndex > 0 && item.Text.Equals(headerAttr.DisplayName)) break; + if (columnIndex <= 0) continue; + + // 优先从代理函数中获取下列列表,若为空且字段为枚举型,则填充枚举项为下列列表,若为字典字段,则填充字典值value列表为下列列表 + var dataList = addListValidationFun?.Invoke(worksheet, prop)?.ToList(); + if (dataList == null) + { + // 填充枚举项为下列列表 + if (propType.IsEnum()) + { + dataList = propType.EnumToList()?.Select(it => it.Describe).ToList(); + } + else + { + // 获取字段上的字典特性 + var dict = prop.GetCustomAttribute(); + if (dict != null) + { + // 填充字典值value为下列列表 + dataList = sysDictTypeService.GetDataList(new GetDataDictTypeInput { Code = dict.DictTypeCode }) + .Result?.Select(x => x.Label).ToList(); + } + } + } + + if (dataList != null) + { + // 添加下拉列表 + AddListValidation(dropdownSheet, columnIndex, dataList); + dropdownSheet.Cells[1, columnIndex, dataList.Count, columnIndex].LoadFromCollection(dataList); + } + } + + package.Save(); + package.Stream.Position = 0; + return new XlsxFileResult(stream: package.Stream, fileDownloadName: $"{filename}-{DateTime.Now:yyyy-MM-dd_HHmmss}"); + + void AddListValidation(ExcelWorksheet dropdownSheet, int columnIndex, List dataList) + { + var validation = worksheet.DataValidations.AddListValidation(worksheet.Cells[2, columnIndex, ExcelPackage.MaxRows, columnIndex].Address); + validation!.Formula.ExcelFormula = "=" + dropdownSheet.Cells[1, columnIndex, dataList.Count, columnIndex].FullAddressAbsolute; + validation.ShowErrorMessage = true; + validation.ErrorTitle = "无效输入"; + validation.Error = "请从列表中选择一个有效的选项"; + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/FileHelper.cs b/Admin.NET.Core/Utils/FileHelper.cs new file mode 100644 index 0000000..2f7e921 --- /dev/null +++ b/Admin.NET.Core/Utils/FileHelper.cs @@ -0,0 +1,125 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 文件帮助类 +/// +public static class FileHelper +{ + /// + /// 尝试删除文件/目录 + /// + /// + /// + public static bool TryDelete(string path) + { + try + { + if (string.IsNullOrEmpty(path)) return false; + if (Directory.Exists(path)) Directory.Delete(path, recursive: true); + else File.Delete(path); + return true; + } + catch (Exception) + { + // ignored + return false; + } + } + + /// + /// 复制目录 + /// + /// + /// + /// + public static void CopyDirectory(string sourceDir, string destinationDir, bool overwrite = false) + { + // 检查源目录是否存在 + if (!Directory.Exists(sourceDir)) throw new DirectoryNotFoundException("Source directory not found: " + sourceDir); + + // 如果目标目录不存在,则创建它 + if (!Directory.Exists(destinationDir)) Directory.CreateDirectory(destinationDir!); + + // 获取源目录下的所有文件并复制它们 + foreach (string file in Directory.GetFiles(sourceDir)) + { + string name = Path.GetFileName(file); + string dest = Path.Combine(destinationDir, name); + File.Copy(file, dest, overwrite); + } + + // 递归复制所有子目录 + foreach (string directory in Directory.GetDirectories(sourceDir)) + { + string name = Path.GetFileName(directory); + string dest = Path.Combine(destinationDir, name); + CopyDirectory(directory, dest, overwrite); + } + } + + /// + /// 在文件倒数第lastIndex个identifier前插入内容(备份原文件) + /// + /// 文件路径 + /// 要插入的内容 + /// 标识符号 + /// 倒数第几个标识符 + /// 是否创建备份文件 + public static async Task InsertsStringAtSpecifiedLocationInFile(string filePath, string insertContent, char identifier, int lastIndex, bool createBackup = false) + { + // 参数校验 + if (lastIndex < 1) throw new ArgumentOutOfRangeException(nameof(lastIndex)); + if (identifier == 0) throw new ArgumentException("标识符不能为空字符"); + + if (!File.Exists(filePath)) + throw new FileNotFoundException("目标文件不存在", filePath); + + // 创建备份文件 + if (createBackup) + { + string backupPath = $"{filePath}.bak_{DateTime.Now:yyyyMMddHHmmss}"; + File.Copy(filePath, backupPath, true); + } + + using var reader = new StreamReader(filePath, Encoding.UTF8); + var content = await reader.ReadToEndAsync(); + reader.Close(); + // 逆向查找算法 + int index = content.LastIndexOf(identifier); + if (index == -1) + { + throw new ArgumentException($"文件中未包含{identifier}"); + } + + int resIndex = content.LastIndexOf(identifier, index - lastIndex); + if (resIndex == -1) + { + throw new ArgumentException($"文件中{identifier}不足{lastIndex}个"); + } + + StringBuilder sb = new StringBuilder(content); + sb = sb.Insert(resIndex, insertContent); + await WriteToFileAsync(filePath, sb); + } + + /// + /// 写入文件内容 + /// + /// + /// + public static async Task WriteToFileAsync(string filePath, StringBuilder sb) + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + await using var writer = new StreamWriter(filePath, false, new UTF8Encoding(false)); // 无BOM + await writer.WriteAsync(sb.ToString()); + writer.Close(); + Console.WriteLine($"文件【{filePath}】写入完成"); + Console.ResetColor(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/GM/GM.cs b/Admin.NET.Core/Utils/GM/GM.cs new file mode 100644 index 0000000..69ea734 --- /dev/null +++ b/Admin.NET.Core/Utils/GM/GM.cs @@ -0,0 +1,471 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.GM; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.X509; + +namespace Admin.NET.Core; + +/** + * + * 用BC的注意点: + * 这个版本的BC对SM3withSM2的结果为asn1格式的r和s,如果需要直接拼接的r||s需要自己转换。下面rsAsn1ToPlainByteArray、rsPlainByteArrayToAsn1就在干这事。 + * 这个版本的BC对SM2的结果为C1||C2||C3,据说为旧标准,新标准为C1||C3||C2,用新标准的需要自己转换。下面(被注释掉的)changeC1C2C3ToC1C3C2、changeC1C3C2ToC1C2C3就在干这事。java版的高版本有加上C1C3C2,csharp版没准以后也会加,但目前还没有,java版的目前可以初始化时“ SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);”。 + * + */ + +public class GM +{ + private static X9ECParameters x9ECParameters = GMNamedCurves.GetByName("sm2p256v1"); + private static ECDomainParameters ecDomainParameters = new(x9ECParameters.Curve, x9ECParameters.G, x9ECParameters.N); + + /** + * + * @param msg + * @param userId + * @param privateKey + * @return r||s,直接拼接byte数组的rs + */ + + public static byte[] SignSm3WithSm2(byte[] msg, byte[] userId, AsymmetricKeyParameter privateKey) + { + return RsAsn1ToPlainByteArray(SignSm3WithSm2Asn1Rs(msg, userId, privateKey)); + } + + /** + * @param msg + * @param userId + * @param privateKey + * @return rs in asn1 format + */ + + public static byte[] SignSm3WithSm2Asn1Rs(byte[] msg, byte[] userId, AsymmetricKeyParameter privateKey) + { + ISigner signer = SignerUtilities.GetSigner("SM3withSM2"); + signer.Init(true, new ParametersWithID(privateKey, userId)); + signer.BlockUpdate(msg, 0, msg.Length); + byte[] sig = signer.GenerateSignature(); + return sig; + } + + /** + * + * @param msg + * @param userId + * @param rs r||s,直接拼接byte数组的rs + * @param publicKey + * @return + */ + + public static bool VerifySm3WithSm2(byte[] msg, byte[] userId, byte[] rs, AsymmetricKeyParameter publicKey) + { + if (rs == null || msg == null || userId == null) return false; + if (rs.Length != RS_LEN * 2) return false; + return VerifySm3WithSm2Asn1Rs(msg, userId, RsPlainByteArrayToAsn1(rs), publicKey); + } + + /** + * + * @param msg + * @param userId + * @param rs in asn1 format + * @param publicKey + * @return + */ + + public static bool VerifySm3WithSm2Asn1Rs(byte[] msg, byte[] userId, byte[] sign, AsymmetricKeyParameter publicKey) + { + ISigner signer = SignerUtilities.GetSigner("SM3withSM2"); + signer.Init(false, new ParametersWithID(publicKey, userId)); + signer.BlockUpdate(msg, 0, msg.Length); + return signer.VerifySignature(sign); + } + + /** + * bc加解密使用旧标c1||c2||c3,此方法在加密后调用,将结果转化为c1||c3||c2 + * @param c1c2c3 + * @return + */ + + private static byte[] ChangeC1C2C3ToC1C3C2(byte[] c1c2c3) + { + int c1Len = (x9ECParameters.Curve.FieldSize + 7) / 8 * 2 + 1; //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + const int c3Len = 32; //new SM3Digest().getDigestSize(); + byte[] result = new byte[c1c2c3.Length]; + Buffer.BlockCopy(c1c2c3, 0, result, 0, c1Len); //c1 + Buffer.BlockCopy(c1c2c3, c1c2c3.Length - c3Len, result, c1Len, c3Len); //c3 + Buffer.BlockCopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.Length - c1Len - c3Len); //c2 + return result; + } + + /** + * bc加解密使用旧标c1||c3||c2,此方法在解密前调用,将密文转化为c1||c2||c3再去解密 + * @param c1c3c2 + * @return + */ + + private static byte[] ChangeC1C3C2ToC1C2C3(byte[] c1c3c2) + { + int c1Len = (x9ECParameters.Curve.FieldSize + 7) / 8 * 2 + 1; //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + const int c3Len = 32; //new SM3Digest().GetDigestSize(); + byte[] result = new byte[c1c3c2.Length]; + Buffer.BlockCopy(c1c3c2, 0, result, 0, c1Len); //c1: 0->65 + Buffer.BlockCopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.Length - c1Len - c3Len); //c2 + Buffer.BlockCopy(c1c3c2, c1Len, result, c1c3c2.Length - c3Len, c3Len); //c3 + return result; + } + + /** + * c1||c3||c2 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2Decrypt(byte[] data, AsymmetricKeyParameter key) + { + return Sm2DecryptOld(ChangeC1C3C2ToC1C2C3(data), key); + } + + /** + * c1||c3||c2 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2Encrypt(byte[] data, AsymmetricKeyParameter key) + { + return ChangeC1C2C3ToC1C3C2(Sm2EncryptOld(data, key)); + } + + /** + * c1||c2||c3 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2EncryptOld(byte[] data, AsymmetricKeyParameter pubkey) + { + SM2Engine sm2Engine = new SM2Engine(); + sm2Engine.Init(true, new ParametersWithRandom(pubkey, new SecureRandom())); + return sm2Engine.ProcessBlock(data, 0, data.Length); + } + + /** + * c1||c2||c3 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2DecryptOld(byte[] data, AsymmetricKeyParameter key) + { + SM2Engine sm2Engine = new SM2Engine(); + sm2Engine.Init(false, key); + return sm2Engine.ProcessBlock(data, 0, data.Length); + } + + /** + * @param bytes + * @return + */ + + public static byte[] Sm3(byte[] bytes) + { + SM3Digest digest = new(); + digest.BlockUpdate(bytes, 0, bytes.Length); + byte[] result = DigestUtilities.DoFinal(digest); + return result; + } + + private const int RS_LEN = 32; + + private static byte[] BigIntToFixexLengthBytes(BigInteger rOrS) + { + // for sm2p256v1, n is 00fffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123, + // r and s are the result of mod n, so they should be less than n and have length<=32 + byte[] rs = rOrS.ToByteArray(); + if (rs.Length == RS_LEN) return rs; + else if (rs.Length == RS_LEN + 1 && rs[0] == 0) return Arrays.CopyOfRange(rs, 1, RS_LEN + 1); + else if (rs.Length < RS_LEN) + { + byte[] result = new byte[RS_LEN]; + Arrays.Fill(result, (byte)0); + Buffer.BlockCopy(rs, 0, result, RS_LEN - rs.Length, rs.Length); + return result; + } + else + { + throw new ArgumentException("err rs: " + Hex.ToHexString(rs)); + } + } + + /** + * BC的SM3withSM2签名得到的结果的rs是asn1格式的,这个方法转化成直接拼接r||s + * @param rsDer rs in asn1 format + * @return sign result in plain byte array + */ + + private static byte[] RsAsn1ToPlainByteArray(byte[] rsDer) + { + Asn1Sequence seq = Asn1Sequence.GetInstance(rsDer); + byte[] r = BigIntToFixexLengthBytes(DerInteger.GetInstance(seq[0]).Value); + byte[] s = BigIntToFixexLengthBytes(DerInteger.GetInstance(seq[1]).Value); + byte[] result = new byte[RS_LEN * 2]; + Buffer.BlockCopy(r, 0, result, 0, r.Length); + Buffer.BlockCopy(s, 0, result, RS_LEN, s.Length); + return result; + } + + /** + * BC的SM3withSM2验签需要的rs是asn1格式的,这个方法将直接拼接r||s的字节数组转化成asn1格式 + * @param sign in plain byte array + * @return rs result in asn1 format + */ + + private static byte[] RsPlainByteArrayToAsn1(byte[] sign) + { + if (sign.Length != RS_LEN * 2) throw new ArgumentException("err rs. "); + BigInteger r = new BigInteger(1, Arrays.CopyOfRange(sign, 0, RS_LEN)); + BigInteger s = new BigInteger(1, Arrays.CopyOfRange(sign, RS_LEN, RS_LEN * 2)); + Asn1EncodableVector v = new Asn1EncodableVector + { + new DerInteger(r), + new DerInteger(s) + }; + + return new DerSequence(v).GetEncoded("DER"); + } + + // 生成公私匙对 + public static AsymmetricCipherKeyPair GenerateKeyPair() + { + ECKeyPairGenerator kpGen = new(); + kpGen.Init(new ECKeyGenerationParameters(ecDomainParameters, new SecureRandom())); + return kpGen.GenerateKeyPair(); + } + + public static ECPrivateKeyParameters GetPrivatekeyFromD(BigInteger d) + { + return new ECPrivateKeyParameters(d, ecDomainParameters); + } + + public static ECPublicKeyParameters GetPublickeyFromXY(BigInteger x, BigInteger y) + { + return new ECPublicKeyParameters(x9ECParameters.Curve.CreatePoint(x, y), ecDomainParameters); + } + + public static AsymmetricKeyParameter GetPublickeyFromX509File(FileInfo file) + { + FileStream fileStream = null; + try + { + //file.DirectoryName + "\\" + file.Name + fileStream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read); + X509Certificate certificate = new X509CertificateParser().ReadCertificate(fileStream); + return certificate.GetPublicKey(); + } + catch (Exception) + { + //log.Error(file.Name + "读取失败,异常:" + e); + } + finally + { + if (fileStream != null) + fileStream.Close(); + } + return null; + } + + public class Sm2Cert + { + public AsymmetricKeyParameter privateKey; + public AsymmetricKeyParameter publicKey; + public string certId; + } + + private static byte[] ToByteArray(int i) + { + byte[] byteArray = new byte[4]; + byteArray[0] = (byte)(i >> 24); + byteArray[1] = (byte)((i & 0xFFFFFF) >> 16); + byteArray[2] = (byte)((i & 0xFFFF) >> 8); + byteArray[3] = (byte)(i & 0xFF); + return byteArray; + } + + /** + * 字节数组拼接 + * + * @param params + * @return + */ + + private static byte[] Join(params byte[][] byteArrays) + { + List byteSource = new(); + for (int i = 0; i < byteArrays.Length; i++) + { + byteSource.AddRange(byteArrays[i]); + } + byte[] data = byteSource.ToArray(); + return data; + } + + /** + * 密钥派生函数 + * + * @param Z + * @param klen + * 生成klen字节数长度的密钥 + * @return + */ + + private static byte[] KDF(byte[] Z, int klen) + { + int ct = 1; + int end = (int)Math.Ceiling(klen * 1.0 / 32); + List byteSource = new(); + + for (int i = 1; i < end; i++) + { + byteSource.AddRange(Sm3(Join(Z, ToByteArray(ct)))); + ct++; + } + byte[] last = Sm3(Join(Z, ToByteArray(ct))); + if (klen % 32 == 0) + { + byteSource.AddRange(last); + } + else + byteSource.AddRange(Arrays.CopyOfRange(last, 0, klen % 32)); + return byteSource.ToArray(); + } + + public static byte[] Sm4DecryptCBC(byte[] keyBytes, byte[] cipher, byte[] iv, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (cipher.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + if (iv == null) iv = ZeroIv(algo); + c.Init(false, new ParametersWithIV(key, iv)); + return c.DoFinal(cipher); + } + + public static byte[] Sm4EncryptCBC(byte[] keyBytes, byte[] plain, byte[] iv, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (plain.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + if (iv == null) iv = ZeroIv(algo); + c.Init(true, new ParametersWithIV(key, iv)); + return c.DoFinal(plain); + } + + public static byte[] Sm4EncryptECB(byte[] keyBytes, byte[] plain, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + //NoPadding 的情况下需要校验数据长度是16的倍数. + if (plain.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + c.Init(true, key); + return c.DoFinal(plain); + } + + public static byte[] Sm4DecryptECB(byte[] keyBytes, byte[] cipher, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (cipher.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + c.Init(false, key); + return c.DoFinal(cipher); + } + + public const string SM4_ECB_NOPADDING = "SM4/ECB/NoPadding"; + public const string SM4_CBC_NOPADDING = "SM4/CBC/NoPadding"; + public const string SM4_ECB_PKCS7PADDING = "SM4/ECB/PKCS7Padding"; + public const string SM4_CBC_PKCS7PADDING = "SM4/CBC/PKCS7Padding"; + + /** + * cfca官网CSP沙箱导出的sm2文件 + * @param pem 二进制原文 + * @param pwd 密码 + * @return + */ + + public static Sm2Cert ReadSm2File(byte[] pem, string pwd) + { + Sm2Cert sm2Cert = new(); + + Asn1Sequence asn1Sequence = (Asn1Sequence)Asn1Object.FromByteArray(pem); + // ASN1Integer asn1Integer = (ASN1Integer) asn1Sequence.getObjectAt(0); //version=1 + Asn1Sequence priSeq = (Asn1Sequence)asn1Sequence[1];//private key + Asn1Sequence pubSeq = (Asn1Sequence)asn1Sequence[2];//public key and x509 cert + + // ASN1ObjectIdentifier sm2DataOid = (ASN1ObjectIdentifier) priSeq.getObjectAt(0); + // ASN1ObjectIdentifier sm4AlgOid = (ASN1ObjectIdentifier) priSeq.getObjectAt(1); + Asn1OctetString priKeyAsn1 = (Asn1OctetString)priSeq[2]; + byte[] key = KDF(System.Text.Encoding.UTF8.GetBytes(pwd), 32); + byte[] priKeyD = Sm4DecryptCBC(Arrays.CopyOfRange(key, 16, 32), + priKeyAsn1.GetOctets(), + Arrays.CopyOfRange(key, 0, 16), SM4_CBC_PKCS7PADDING); + sm2Cert.privateKey = GetPrivatekeyFromD(new BigInteger(1, priKeyD)); + // log.Info(Hex.toHexString(priKeyD)); + + // ASN1ObjectIdentifier sm2DataOidPub = (ASN1ObjectIdentifier) pubSeq.getObjectAt(0); + Asn1OctetString pubKeyX509 = (Asn1OctetString)pubSeq[1]; + X509Certificate x509 = new X509CertificateParser().ReadCertificate(pubKeyX509.GetOctets()); + sm2Cert.publicKey = x509.GetPublicKey(); + sm2Cert.certId = x509.SerialNumber.ToString(10); //这里转10进制,有啥其他进制要求的自己改改 + return sm2Cert; + } + + /** + * + * @param cert + * @return + */ + + public static Sm2Cert ReadSm2X509Cert(byte[] cert) + { + Sm2Cert sm2Cert = new(); + + X509Certificate x509 = new X509CertificateParser().ReadCertificate(cert); + sm2Cert.publicKey = x509.GetPublicKey(); + sm2Cert.certId = x509.SerialNumber.ToString(10); //这里转10进制,有啥其他进制要求的自己改改 + return sm2Cert; + } + + public static byte[] ZeroIv(string algo) + { + IBufferedCipher cipher = CipherUtilities.GetCipher(algo); + int blockSize = cipher.GetBlockSize(); + byte[] iv = new byte[blockSize]; + Arrays.Fill(iv, (byte)0); + return iv; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/GM/GMUtil.cs b/Admin.NET.Core/Utils/GM/GMUtil.cs new file mode 100644 index 0000000..d92853a --- /dev/null +++ b/Admin.NET.Core/Utils/GM/GMUtil.cs @@ -0,0 +1,151 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Utilities.Encoders; + +namespace Admin.NET.Core; + +/// +/// GM工具类 +/// +public class GMUtil +{ + /// + /// SM2加密 + /// + /// + /// + /// + public static string SM2Encrypt(string publicKeyHex, string data_string) + { + // 如果是130位公钥,.NET使用的话,把开头的04截取掉 + if (publicKeyHex.Length == 130) + { + publicKeyHex = publicKeyHex.Substring(2, 128); + } + // 公钥X,前64位 + string x = publicKeyHex.Substring(0, 64); + // 公钥Y,后64位 + string y = publicKeyHex.Substring(64); + // 获取公钥对象 + AsymmetricKeyParameter publicKey1 = GM.GetPublickeyFromXY(new BigInteger(x, 16), new BigInteger(y, 16)); + // Sm2Encrypt: C1C3C2 + // Sm2EncryptOld: C1C2C3 + byte[] digestByte = GM.Sm2Encrypt(Encoding.UTF8.GetBytes(data_string), publicKey1); + string strSM2 = Hex.ToHexString(digestByte); + return strSM2; + } + + /// + /// SM2解密 + /// + /// + /// + /// + public static string SM2Decrypt(string privateKey_string, string encryptedData_string) + { + if (!encryptedData_string.StartsWith("04")) + encryptedData_string = "04" + encryptedData_string; + BigInteger d = new(privateKey_string, 16); + // 先拿到私钥对象,用ECPrivateKeyParameters 或 AsymmetricKeyParameter 都可以 + // ECPrivateKeyParameters bcecPrivateKey = GmUtil.GetPrivatekeyFromD(d); + AsymmetricKeyParameter bcecPrivateKey = GM.GetPrivatekeyFromD(d); + byte[] byToDecrypt = Hex.Decode(encryptedData_string); + byte[] byDecrypted = GM.Sm2Decrypt(byToDecrypt, bcecPrivateKey); + string strDecrypted = Encoding.UTF8.GetString(byDecrypted); + return strDecrypted; + } + + /// + /// SM4加密(ECB) + /// + /// + /// + /// + public static string SM4EncryptECB(string key_string, string plainText) + { + byte[] key = Hex.Decode(key_string); + byte[] bs = GM.Sm4EncryptECB(key, Encoding.UTF8.GetBytes(plainText), GM.SM4_ECB_PKCS7PADDING);//NoPadding 的情况下需要校验数据长度是16的倍数. 使用 HandleSm4Padding 处理 + return Hex.ToHexString(bs); + } + + /// + /// SM4解密(ECB) + /// + /// + /// + /// + public static string SM4DecryptECB(string key_string, string cipherText) + { + byte[] key = Hex.Decode(key_string); + byte[] bs = GM.Sm4DecryptECB(key, Hex.Decode(cipherText), GM.SM4_ECB_PKCS7PADDING); + return Encoding.UTF8.GetString(bs); + } + + /// + /// SM4加密(CBC) + /// + /// + /// + /// + /// + public static string SM4EncryptCBC(string key_string, string iv_string, string plainText) + { + byte[] key = Hex.Decode(key_string); + byte[] iv = Hex.Decode(iv_string); + byte[] bs = GM.Sm4EncryptCBC(key, Encoding.UTF8.GetBytes(plainText), iv, GM.SM4_CBC_PKCS7PADDING); + return Hex.ToHexString(bs); + } + + /// + /// SM4解密(CBC) + /// + /// + /// + /// + /// + public static string SM4DecryptCBC(string key_string, string iv_string, string cipherText) + { + byte[] key = Hex.Decode(key_string); + byte[] iv = Hex.Decode(iv_string); + byte[] bs = GM.Sm4DecryptCBC(key, Hex.Decode(cipherText), iv, GM.SM4_CBC_PKCS7PADDING); + return Encoding.UTF8.GetString(bs); + } + + /// + /// 补足 16 进制字符串的 0 字符,返回不带 0x 的16进制字符串 + /// + /// + /// 1表示加密,0表示解密 + /// + private static byte[] HandleSm4Padding(byte[] input, int mode) + { + if (input == null) + { + return null; + } + byte[] ret = (byte[])null; + if (mode == 1) + { + int p = 16 - input.Length % 16; + ret = new byte[input.Length + p]; + Array.Copy(input, 0, ret, 0, input.Length); + for (int i = 0; i < p; i++) + { + ret[input.Length + i] = (byte)p; + } + } + else + { + int p = input[input.Length - 1]; + ret = new byte[input.Length - p]; + Array.Copy(input, 0, ret, 0, input.Length - p); + } + return ret; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/GiteeHelper.cs b/Admin.NET.Core/Utils/GiteeHelper.cs new file mode 100644 index 0000000..a4b81a1 --- /dev/null +++ b/Admin.NET.Core/Utils/GiteeHelper.cs @@ -0,0 +1,59 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Gitee接口帮助类 +/// +public class GiteeHelper +{ + private const string BaseUrl = "https://gitee.com/api/v5/repos/"; + private static readonly HttpClient Client = new(); + + /// + /// 下载仓库 zip + /// + /// https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoZipball + /// + public static async Task DownloadRepoZip(string owner, string repo, string accessToken = null, string @ref = null) + { + if (string.IsNullOrWhiteSpace(owner)) throw Oops.Bah($"参数 {nameof(owner)} 不能为空"); + if (string.IsNullOrWhiteSpace(repo)) throw Oops.Bah($"参数 {nameof(repo)} 不能为空"); + var query = BuilderQueryString(new + { + access_token = accessToken, + @ref + }); + return await Client.GetStreamAsync($"{BaseUrl}{owner}/{repo}/zipball?{query}"); + } + + /// + /// 构建Query参数 + /// + /// + private static string BuilderQueryString([System.Diagnostics.CodeAnalysis.NotNull] object obj) + { + if (obj == null) return string.Empty; + var query = HttpUtility.ParseQueryString(string.Empty); + foreach (var prop in obj.GetType().GetProperties()) + { + var val = prop.GetValue(obj); + if (val == null) continue; + + // 以元组形式校验参数集 + var name = prop.Name.Trim('@'); + if (val is Tuple { Item1: not null } tuple) + { + if (!tuple.Item2.Split(",").Any(x => x.Trim().Equals(tuple.Item1))) throw Oops.Oh($"参数 {name} 的值只能为:{tuple.Item2}"); + query[name] = tuple.Item1.ToString(); + continue; + } + query[name] = val.ToString(); + } + return query.ToString(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/MiniExcelUtil.cs b/Admin.NET.Core/Utils/MiniExcelUtil.cs new file mode 100644 index 0000000..d6a9cd6 --- /dev/null +++ b/Admin.NET.Core/Utils/MiniExcelUtil.cs @@ -0,0 +1,70 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using MiniExcelLibs; + +namespace Admin.NET.Core; + +public static class MiniExcelUtil +{ + private const string SheetName = "ImportTemplate"; + private const string DirectoryName = "export"; + + /// + /// 导出模板Excel + /// + /// + public static async Task ExportExcelTemplate(string fileName = null) where T : class, new() + { + var values = Array.Empty(); + // 在内存中当开辟空间 + var memoryStream = new MemoryStream(); + // 将数据写到内存当中 + await memoryStream.SaveAsAsync(values, sheetName: SheetName); + // 从0的位置开始写入 + memoryStream.Seek(0, SeekOrigin.Begin); + return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = $"{(string.IsNullOrEmpty(fileName) ? typeof(T).Name : fileName)}.xlsx" + }; + } + + /// + /// 获取导入数据Excel + /// + /// + /// + public static async Task> GetImportExcelData([Required] IFormFile file) where T : class, new() + { + using MemoryStream stream = new MemoryStream(); + await file.CopyToAsync(stream); + var res = await stream.QueryAsync(sheetName: SheetName); + return res.ToArray(); + } + + /// + /// 获取导出数据excel地址 + /// + /// + public static async Task GetExportDataExcelUrl(IEnumerable exportData) where T : class, new() + { + var fileName = string.Format("{0}.xlsx", YitIdHelper.NextId()); + try + { + var path = Path.Combine(App.WebHostEnvironment.WebRootPath, DirectoryName); + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + var filePath = Path.Combine(path, fileName); + await MiniExcel.SaveAsAsync(filePath, exportData, overwriteFile: true); + } + catch (Exception error) + { + throw Oops.Oh("出现错误:" + error); + } + var host = CommonUtil.GetLocalhost(); + return $"{host}/{DirectoryName}/{fileName}"; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/NewtonsoftJsonSerializerProvider.cs b/Admin.NET.Core/Utils/NewtonsoftJsonSerializerProvider.cs new file mode 100644 index 0000000..568ffbd --- /dev/null +++ b/Admin.NET.Core/Utils/NewtonsoftJsonSerializerProvider.cs @@ -0,0 +1,59 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core; + +/// +/// 自定义序列化提供器 Newtonsoft.Json 实现 +/// +public class NewtonsoftJsonSerializerProvider : IJsonSerializerProvider, ISingleton +{ + /// + /// 序列化对象 + /// + /// + /// + /// + public string Serialize(object value, object jsonSerializerOptions = null) + { + return JsonConvert.SerializeObject(value, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + public T Deserialize(string json, object jsonSerializerOptions = null) + { + return JsonConvert.DeserializeObject(json, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + public object Deserialize(string json, Type returnType, object jsonSerializerOptions = null) + { + return JsonConvert.DeserializeObject(json, returnType, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 返回读取全局配置的 JSON 选项 + /// + /// + public object GetSerializerOptions() + { + return App.GetOptions()?.SerializerSettings; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/PathTreeBuilder.cs b/Admin.NET.Core/Utils/PathTreeBuilder.cs new file mode 100644 index 0000000..d93415a --- /dev/null +++ b/Admin.NET.Core/Utils/PathTreeBuilder.cs @@ -0,0 +1,57 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 树形节点 +/// +public class TreeNode +{ + public int Id { get; set; } + public int Pid { get; set; } + public string Name { get; set; } + public List Children { get; set; } = new(); +} + +/// +/// 根据路径数组生成树结构 +/// +public class PathTreeBuilder +{ + private int _nextId = 1; + + public TreeNode BuildTree(List paths) + { + var root = new TreeNode { Id = 1, Pid = 0, Name = "文件目录" }; // 根节点 + var dict = new Dictionary(); + + foreach (var path in paths) + { + var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + TreeNode currentNode = root; + + foreach (var part in parts) + { + var key = currentNode.Id + "_" + part; // 生成唯一键 + if (!dict.ContainsKey(key)) + { + var newNode = new TreeNode + { + Id = _nextId++, + Pid = currentNode.Id, + Name = part + }; + currentNode.Children.Add(newNode); + dict[key] = newNode; + } + currentNode = dict[key]; // 更新当前节点 + } + } + + return root; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/ReflectionUtil.cs b/Admin.NET.Core/Utils/ReflectionUtil.cs new file mode 100644 index 0000000..0797a1d --- /dev/null +++ b/Admin.NET.Core/Utils/ReflectionUtil.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 反射工具类 +/// +public static class ReflectionUtil +{ + /// + /// 获取字段特性 + /// + /// + /// + /// + public static T GetDescriptionValue(this FieldInfo field) where T : Attribute + { + // 获取字段的指定特性,不包含继承中的特性 + object[] customAttributes = field.GetCustomAttributes(typeof(T), false); + + // 如果没有数据返回null + return customAttributes.Length > 0 ? (T)customAttributes[0] : null; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/RegularValidate.cs b/Admin.NET.Core/Utils/RegularValidate.cs new file mode 100644 index 0000000..b7c8bfe --- /dev/null +++ b/Admin.NET.Core/Utils/RegularValidate.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 正则校验 +/// +public static class RegularValidate +{ + /// + /// 验证密码规则 + /// + /// + /// + public static bool ValidatePassword(string password) + { + var regex = new Regex(@" +(?=.*[0-9]) #必须包含数字 +(?=.*[a-z]) #必须包含小写 +(?=.*[A-Z]) #必须包含大写 +(?=([\x21-\x7e]+)[^a-zA-Z0-9]) #必须包含特殊符号 +.{8,30} #至少8个字符,最多30个字符 +", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace); + + //如果要求必须包含小写、大写字母,则上面的(?=.*[a-zA-Z]) 要改为: + /* + * (?=.*[a-z]) + * (?=.*[A-Z]) + */ + return regex.IsMatch(password); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/SSHHelper.cs b/Admin.NET.Core/Utils/SSHHelper.cs new file mode 100644 index 0000000..8205043 --- /dev/null +++ b/Admin.NET.Core/Utils/SSHHelper.cs @@ -0,0 +1,212 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Renci.SshNet; + +namespace Admin.NET.Core +{ + /// + /// SSH/Sftp 工具类 + /// + public class SSHHelper : IDisposable + { + private readonly SftpClient _sftp; + + public SSHHelper(string host, int port, string user, string password) + { + _sftp = new SftpClient(host, port, user, password); + } + + /// + /// 连接 + /// + private void Connect() + { + if (!_sftp.IsConnected) + _sftp.Connect(); + } + + /// + /// 是否存在同名文件 + /// + /// + /// + public bool Exists(string ftpFileName) + { + Connect(); + + return _sftp.Exists(ftpFileName); + } + + /// + /// 删除文件 + /// + /// + public void DeleteFile(string ftpFileName) + { + Connect(); + + _sftp.DeleteFile(ftpFileName); + } + + /// + /// 下载到指定目录 + /// + /// + /// + public void DownloadFile(string ftpFileName, string localFileName) + { + Connect(); + + using (Stream fileStream = File.OpenWrite(localFileName)) + { + _sftp.DownloadFile(ftpFileName, fileStream); + } + } + + /// + /// 读取字节 + /// + /// + /// + public byte[] ReadAllBytes(string ftpFileName) + { + Connect(); + + return _sftp.ReadAllBytes(ftpFileName); + } + + /// + /// 读取流 + /// + /// + /// + public Stream OpenRead(string path) + { + return _sftp.Open(path, FileMode.Open, FileAccess.Read); + } + + /// + /// 继续下载 + /// + /// + /// + public void DownloadFileWithResume(string ftpFileName, string localFileName) + { + DownloadFile(ftpFileName, localFileName); + } + + /// + /// 重命名 + /// + /// + /// + public void RenameFile(string oldPath, string newPath) + { + _sftp.RenameFile(oldPath, newPath); + } + + /// + /// 指定目录下文件 + /// + /// + /// + /// + public List GetFileList(string folder, IEnumerable filters) + { + Connect(); + + var files = new List(); + var sftpFiles = _sftp.ListDirectory(folder); + foreach (var file in sftpFiles) + { + if (file.IsRegularFile && filters.Any(f => file.Name.EndsWith(f))) + files.Add(file.Name); + } + return files; + } + + /// + /// 上传指定目录文件 + /// + /// + /// + public void UploadFile(string localFileName, string ftpFileName) + { + Connect(); + + var dir = Path.GetDirectoryName(ftpFileName); + CreateDir(_sftp, dir); + using (var fileStream = new FileStream(localFileName, FileMode.Open)) + { + _sftp.UploadFile(fileStream, ftpFileName); + } + } + + /// + /// 上传字节 + /// + /// + /// + public void UploadFile(byte[] bs, string ftpFileName) + { + Connect(); + + var dir = Path.GetDirectoryName(ftpFileName); + CreateDir(_sftp, dir); + _sftp.WriteAllBytes(ftpFileName, bs); + } + + /// + /// 上传流 + /// + /// + /// + public void UploadFile(Stream fileStream, string ftpFileName) + { + Connect(); + + var dir = Path.GetDirectoryName(ftpFileName); + CreateDir(_sftp, dir); + _sftp.UploadFile(fileStream, ftpFileName); + fileStream.Dispose(); + } + + /// + /// 创建目录 + /// + /// + /// + /// + private void CreateDir(SftpClient sftp, string dir) + { + ArgumentNullException.ThrowIfNull(dir); + + if (sftp.Exists(dir)) return; + + var index = dir.LastIndexOfAny(new char[] { '/', '\\' }); + if (index > 0) + { + var p = dir[..index]; + if (!sftp.Exists(p)) + CreateDir(sftp, p); + sftp.CreateDirectory(dir); + } + } + + /// + /// 释放对象 + /// + public void Dispose() + { + if (_sftp == null) return; + + if (_sftp.IsConnected) + _sftp.Disconnect(); + _sftp.Dispose(); + } + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/SafeMath.cs b/Admin.NET.Core/Utils/SafeMath.cs new file mode 100644 index 0000000..a0eb5f4 --- /dev/null +++ b/Admin.NET.Core/Utils/SafeMath.cs @@ -0,0 +1,133 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Globalization; + +namespace Admin.NET.Core; + +using System; + +/// +/// 安全的基本数学运算方法类 +/// +public static class SafeMath +{ + /// + /// 安全加法 + /// + /// 左操作数 + /// 右操作数 + /// 保留小数位数 + /// 默认值 + /// 是否抛出异常 + /// + public static T Add(object left, object right, int precision = 2, T defaultValue = default, bool throwOnError = true) where T : struct, IComparable, IConvertible, IFormattable + { + return PerformOperation(left, right, (a, b) => a + b, precision, defaultValue, throwOnError); + } + + /// + /// 安全减法 + /// + /// 左操作数 + /// 右操作数 + /// 保留小数位数 + /// 默认值 + /// 是否抛出异常 + public static T Sub(object left, object right, int precision = 2, T defaultValue = default, bool throwOnError = true) where T : struct, IComparable, IConvertible, IFormattable + { + return PerformOperation(left, right, (a, b) => a - b, precision, defaultValue, throwOnError); + } + + /// + /// 安全乘法 + /// + /// 左操作数 + /// 右操作数 + /// 保留小数位数 + /// 默认值 + /// 是否抛出异常 + public static T Mult(object left, object right, int precision = 2, T defaultValue = default, bool throwOnError = true) where T : struct, IComparable, IConvertible, IFormattable + { + return PerformOperation(left, right, (a, b) => a * b, precision, defaultValue, throwOnError); + } + + /// + /// 安全除法 + /// + /// 左操作数 + /// 右操作数 + /// 保留小数位数 + /// 默认值 + /// 是否抛出除以零异常 + public static T Div(object left, object right, int precision = 2, T defaultValue = default, bool throwOnDivideByZero = true) where T : struct, IComparable, IConvertible, IFormattable + { + return PerformOperation(left, right, (a, b) => + { + if (b != 0) return a / b; + if (throwOnDivideByZero) throw new DivideByZeroException("除数不能为0"); + return SafeConvert(defaultValue); + }, precision, defaultValue, throwOnDivideByZero); + } + + /// + /// 安全类型转换 + /// + /// 数据源 + /// 默认值 + public static T SafeConvert(object value, T defaultValue = default) where T : struct, IComparable, IConvertible, IFormattable + { + if (value == null) return defaultValue; + try + { + return (T)Convert.ChangeType(value, typeof(T)); + } + catch + { + return defaultValue; + } + } + + /// + /// 执行数学运算 + /// + private static T PerformOperation(object left, object right, Func operation, int precision, T defaultValue, bool throwOnError) where T : struct, IComparable, IConvertible, IFormattable + { + try + { + decimal leftValue = ConvertToDecimal(left); + decimal rightValue = ConvertToDecimal(right); + + decimal result = operation(leftValue, rightValue); + return SafeConvert(Math.Round(result, precision, MidpointRounding.AwayFromZero), defaultValue); + } + catch + { + if (throwOnError) throw; + return defaultValue; + } + } + + /// + /// 将输入值转换为 decimal + /// + public static decimal ConvertToDecimal(object value) + { + return value switch + { + null => 0m, + int intValue => intValue, + float floatValue => (decimal)floatValue, + double doubleValue => (decimal)doubleValue, + decimal decimalValue => decimalValue, + long longValue => longValue, + short shortValue => shortValue, + byte byteValue => byteValue, + string stringValue when decimal.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal parsedValue) => parsedValue, // 尝试解析字符串 + _ => throw new InvalidCastException($"不支持的类型: {value.GetType().Name}") + }; + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/TenantHeaderOperationFilter.cs b/Admin.NET.Core/Utils/TenantHeaderOperationFilter.cs new file mode 100644 index 0000000..53a5bab --- /dev/null +++ b/Admin.NET.Core/Utils/TenantHeaderOperationFilter.cs @@ -0,0 +1,28 @@ +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Admin.NET.Core; + +/// +/// 租户头部参数过滤器 +/// +public class TenantHeaderOperationFilter : IOperationFilter +{ + /// + /// 应用租户头部参数过滤器 + /// + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + operation.Parameters ??= []; + + operation.Parameters.Add(new OpenApiParameter + { + Name = ClaimConst.TenantId, + In = ParameterLocation.Header, + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + Required = false, + AllowEmptyValue = true, + Description = "租户ID(留空表示默认租户)" + }); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/TripleDES.cs b/Admin.NET.Core/Utils/TripleDES.cs new file mode 100644 index 0000000..fedec3c --- /dev/null +++ b/Admin.NET.Core/Utils/TripleDES.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Security.Cryptography; + +namespace Admin.NET.Core; + +/// +/// 3DES文件加解密 +/// +public static class TripleDES +{ + /// + /// 加密文件 + /// + /// 待加密文件路径 + /// 加密后的文件路径 + /// 密码 (24位长度) + [Obsolete] + public static void EncryptFile(string inputFile, string outputFile, string password) + { + using var ties = new TripleDESCryptoServiceProvider(); + ties.Mode = CipherMode.ECB; + ties.Padding = PaddingMode.PKCS7; + ties.Key = Encoding.UTF8.GetBytes(password); + using var inputFileStream = new FileStream(inputFile, FileMode.Open); + using var encryptedFileStream = new FileStream(outputFile, FileMode.Create); + using var cryptoStream = new CryptoStream(encryptedFileStream, ties.CreateEncryptor(), CryptoStreamMode.Write); + inputFileStream.CopyTo(cryptoStream); + } + + /// + /// 加密文件 + /// + /// 加密的文件路径 + /// 解密后的文件路径 + /// 密码 (24位长度) + [Obsolete] + public static void DecryptFile(string inputFile, string outputFile, string password) + { + using var ties = new TripleDESCryptoServiceProvider(); + ties.Mode = CipherMode.ECB; + ties.Padding = PaddingMode.PKCS7; + ties.Key = Encoding.UTF8.GetBytes(password); + using var encryptedFileStream = new FileStream(inputFile, FileMode.Open); + using var decryptedFileStream = new FileStream(outputFile, FileMode.Create); + using var cryptoStream = new CryptoStream(encryptedFileStream, ties.CreateDecryptor(), CryptoStreamMode.Read); + cryptoStream.CopyTo(decryptedFileStream); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/VerifyFileExtensionName.cs b/Admin.NET.Core/Utils/VerifyFileExtensionName.cs new file mode 100644 index 0000000..2c8e8be --- /dev/null +++ b/Admin.NET.Core/Utils/VerifyFileExtensionName.cs @@ -0,0 +1,163 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 验证文件类型 +/// +public static class VerifyFileExtensionName +{ + private static readonly IDictionary DicsExt = new Dictionary(); + private static readonly IDictionary> ExtDics = new Dictionary>(); + + static VerifyFileExtensionName() + { + DicsExt.Add("FFD8FFE0", ".jpg"); + DicsExt.Add("FFD8FFE1", ".jpg"); + DicsExt.Add("89504E47", ".png"); + DicsExt.Add("47494638", ".gif"); + DicsExt.Add("49492A00", ".tif"); + DicsExt.Add("424D", ".bmp"); + + // PS和CAD + DicsExt.Add("38425053", ".psd"); + DicsExt.Add("41433130", ".dwg"); // CAD + DicsExt.Add("252150532D41646F6265", ".ps"); + + // 办公文档类 + DicsExt.Add("D0CF11E0", ".ppt,.doc,.xls"); // ppt、doc、xls + DicsExt.Add("504B0304", ".pptx,.docx,.xlsx"); // pptx、docx、xlsx + + /* 注意由于文本文档录入内容过多,则读取文件头时较为多变-START */ + DicsExt.Add("0D0A0D0A", ".txt"); // txt + DicsExt.Add("0D0A2D2D", ".txt"); // txt + DicsExt.Add("0D0AB4B4", ".txt"); // txt + DicsExt.Add("B4B4BDA8", ".txt"); // 文件头部为汉字 + DicsExt.Add("73646673", ".txt"); // txt,文件头部为英文字母 + DicsExt.Add("32323232", ".txt"); // txt,文件头部内容为数字 + DicsExt.Add("0D0A09B4", ".txt"); // txt,文件头部内容为数字 + DicsExt.Add("3132330D", ".txt"); // txt,文件头部内容为数字 + /* 注意由于文本文档录入内容过多,则读取文件头时较为多变-END */ + + DicsExt.Add("7B5C727466", ".rtf"); // 日记本 + + DicsExt.Add("255044462D312E", ".pdf"); + + // 视频或音频类 + DicsExt.Add("3026B275", ".wma"); + DicsExt.Add("57415645", ".wav"); + DicsExt.Add("41564920", ".avi"); + DicsExt.Add("4D546864", ".mid"); + DicsExt.Add("2E524D46", ".rm"); + DicsExt.Add("000001BA", ".mpg"); + DicsExt.Add("000001B3", ".mpg"); + DicsExt.Add("6D6F6F76", ".mov"); + DicsExt.Add("3026B2758E66CF11", ".asf"); + + // 压缩包 + DicsExt.Add("52617221", ".rar"); + DicsExt.Add("504B03040A000000", ".zip"); + DicsExt.Add("504B030414000000", ".zip"); + DicsExt.Add("1F8B08", ".gz"); + + // 程序文件 + DicsExt.Add("3C3F786D6C", ".xml"); + DicsExt.Add("68746D6C3E", ".html"); + DicsExt.Add("04034b50", ".apk"); + //dics_ext.Add("7061636B", ".java"); + //dics_ext.Add("3C254020", ".jsp"); + //dics_ext.Add("4D5A9000", ".exe"); + + DicsExt.Add("44656C69766572792D646174653A", ".eml"); // 邮件 + DicsExt.Add("5374616E64617264204A", ".mdb"); // Access数据库文件 + + DicsExt.Add("46726F6D", ".mht"); + DicsExt.Add("4D494D45", ".mhtml"); + + foreach (var dics in DicsExt) + { + foreach (var ext in dics.Value.Split(",")) + { + if (!ExtDics.ContainsKey(ext)) + ExtDics.Add(ext, new HashSet { dics.Key.Length / 2 }); + else + ExtDics[ext].Add(dics.Key.Length / 2); + } + } + } + + /// + /// 文件格式和文件内容格式是否一致 + /// + /// + /// + /// + public static bool IsSameType(Stream stream, string suffix = ".jpg") + { + if (stream == null) + return false; + + suffix = suffix.ToLower(); + if (!ExtDics.TryGetValue(suffix, out HashSet dic)) return false; + + try + { + foreach (var len in dic) + { + byte[] b = new byte[len]; + stream.ReadExactly(b); + // string fileType = System.Text.Encoding.UTF8.GetString(b); + string fileKey = GetFileHeader(b); + if (DicsExt.ContainsKey(fileKey)) + return true; + } + } + catch (IOException) + { + } + return false; + } + + /** + * 根据文件转换成的字节数组获取文件头信息 + * @param 文件路径 + * @return 文件头信息 + */ + + private static string GetFileHeader(byte[] b) + { + string value = BytesToHexString(b); + return value; + } + + /** + * 将要读取文件头信息的文件的byte数组转换成string类型表示 + * 下面这段代码就是用来对文件类型作验证的方法, + * 将字节数组的前四位转换成16进制字符串,并且转换的时候,要先和0xFF做一次与运算。 + * 这是因为,整个文件流的字节数组中,有很多是负数,进行了与运算后,可以将前面的符号位都去掉, + * 这样转换成的16进制字符串最多保留两位,如果是正数又小于10,那么转换后只有一位, + * 需要在前面补0,这样做的目的是方便比较,取完前四位这个循环就可以终止了 + * @param src要读取文件头信息的文件的byte数组 + * @return 文件头信息 + */ + + private static string BytesToHexString(byte[] src) + { + var builder = new StringBuilder(); + if (src == null || src.Length <= 0) + return null; + + for (int i = 0; i < src.Length; i++) + { + // 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式,并转换为大写 + string hVal = Convert.ToString(src[i] & 0xFF, 16).ToUpper(); + if (hVal.Length < 2) builder.Append(0); + builder.Append(hVal); + } + return builder.ToString(); + } +} \ No newline at end of file diff --git a/Admin.NET.Core/Utils/XlsxFileResult.cs b/Admin.NET.Core/Utils/XlsxFileResult.cs new file mode 100644 index 0000000..75b5ef1 --- /dev/null +++ b/Admin.NET.Core/Utils/XlsxFileResult.cs @@ -0,0 +1,99 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Excel文件ActionResult +/// +/// +public class XlsxFileResult : XlsxFileResultBase where T : class, new() +{ + public string FileDownloadName { get; } + public ICollection Data { get; } + + /// + /// + /// + /// + /// + public XlsxFileResult(ICollection data, string fileDownloadName = null) + { + FileDownloadName = fileDownloadName; + Data = data; + } + + public override async Task ExecuteResultAsync(ActionContext context) + { + var exporter = new ExcelExporter(); + var bytes = await exporter.ExportAsByteArray(Data); + var fs = new MemoryStream(bytes); + await DownloadExcelFileAsync(context, fs, FileDownloadName); + } +} + +/// +/// +/// +public class XlsxFileResult : XlsxFileResultBase +{ + /// + /// + /// + /// + /// + public XlsxFileResult(Stream stream, string fileDownloadName = null) + { + Stream = stream; + FileDownloadName = fileDownloadName; + } + + /// + /// + /// + /// + /// + + public XlsxFileResult(byte[] bytes, string fileDownloadName = null) + { + Stream = new MemoryStream(bytes); + FileDownloadName = fileDownloadName; + } + + public Stream Stream { get; protected set; } + public string FileDownloadName { get; protected set; } + + public override async Task ExecuteResultAsync(ActionContext context) + { + await DownloadExcelFileAsync(context, Stream, FileDownloadName); + } +} + +/// +/// 基类 +/// +public class XlsxFileResultBase : ActionResult +{ + /// + /// 下载Excel文件 + /// + /// + /// + /// + /// + protected virtual async Task DownloadExcelFileAsync(ActionContext context, Stream stream, string downloadFileName) + { + var response = context.HttpContext.Response; + response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + downloadFileName ??= Guid.NewGuid().ToString("N") + ".xlsx"; + + if (string.IsNullOrEmpty(Path.GetExtension(downloadFileName))) downloadFileName += ".xlsx"; + + context.HttpContext.Response.Headers.Append("Content-Disposition", new[] { "attachment; filename=" + HttpUtility.UrlEncode(downloadFileName) }); + await stream.CopyToAsync(context.HttpContext.Response.Body); + } +} \ No newline at end of file diff --git a/Admin.NET.Test/Admin.NET.Test.csproj b/Admin.NET.Test/Admin.NET.Test.csproj new file mode 100644 index 0000000..819da23 --- /dev/null +++ b/Admin.NET.Test/Admin.NET.Test.csproj @@ -0,0 +1,26 @@ + + + net8.0 + 1701;1702;1591;8632 + + enable + true + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + + + + + + + + + + + diff --git a/Admin.NET.Test/BaseTest.cs b/Admin.NET.Test/BaseTest.cs new file mode 100644 index 0000000..8314822 --- /dev/null +++ b/Admin.NET.Test/BaseTest.cs @@ -0,0 +1,98 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OpenQA.Selenium; +using OpenQA.Selenium.Edge; +using OpenQA.Selenium.Support.UI; + +namespace Admin.NET.Test; + +/// +/// 测试基类 +/// +public class BaseTest : IDisposable +{ + private readonly string _baseUrl = "http://localhost:8888"; + protected readonly EdgeDriver Driver = new(); + + protected BaseTest(string token = null) + { + var url = _baseUrl; + if (!string.IsNullOrWhiteSpace(token)) url += $"/#/login?token={token}"; + Driver.Manage().Window.Maximize(); + Driver.Navigate().GoToUrl(url); + + // 隐式等待3秒(隐式等待是元素未被呈现出来,才会等待) + Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3); + } + + /// + /// 等待网页加载完成 + /// + protected async Task WaitExecutorCompleteAsync() + { + var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(30)); + wait.Until(driver => ((IJavaScriptExecutor)driver).ExecuteScript("return document.readyState").Equals("complete")); + await Task.Delay(1000); + } + + /// + /// 用户登录 + /// + /// + /// + protected async Task Login(string account = "superadmin", string password = "123456") + { + await GoToUrlAsync("/#/login"); + var inputList = Driver.FindElements(By.CssSelector("#pane-account input")); + + // 输入用户名 + var accountInput = inputList.First(); + accountInput.Clear(); + accountInput.SendKeys(account); + + // 输入密码 + var passwordInput = inputList.Skip(1).First(); + passwordInput.Clear(); + passwordInput.SendKeys(password); + + // 输入验证码 + var captchaInput = inputList.Skip(2).First(); + captchaInput.Clear(); + captchaInput.SendKeys("0"); + + // 提交 + var button = Driver.FindElement(By.CssSelector("#pane-account button")); + button.Click(); + } + + /// + /// 打开指定页面 + /// + /// + protected async Task GoToUrlAsync(string url) + { + if (url.StartsWith("http")) await Driver.Navigate().GoToUrlAsync(url); + else await Driver.Navigate().GoToUrlAsync(_baseUrl + "/" + url.TrimStart('/')); + await WaitExecutorCompleteAsync(); + } + + /// + /// 等待用户按回车键继续 + /// + /// 提示词 + protected void WaitEnter(string text = "等待用户按回车键继续...") + { + Console.WriteLine(text); + Console.ReadLine(); + } + + public void Dispose() + { + Driver.Quit(); + Driver.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET.Test/User/UserTest.cs b/Admin.NET.Test/User/UserTest.cs new file mode 100644 index 0000000..a1132fd --- /dev/null +++ b/Admin.NET.Test/User/UserTest.cs @@ -0,0 +1,65 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OpenQA.Selenium; +using Xunit; + +namespace Admin.NET.Test.User; + +public class UserTest : BaseTest +{ + // 用户登录 token + private static readonly string Token = "xxxxxxxxx"; + + public UserTest() : base(Token) + { + } + + [Fact] + public async Task Login() + { + await base.Login(); + WaitEnter(); + } + + [Fact] + public async Task AddUser() + { + await Task.Delay(1000); + + await GoToUrlAsync("/#/system/user"); + var addBut = Driver.FindElement(By.XPath("//*[@id=\"app\"]/section/section/div/div[1]/div/main/div/div[1]/div/div[1]/div[1]/div[1]/div[3]/div[1]/div/form/div[6]/div/button")); + addBut.Click(); + + //点击基础信息选项卡 + await Task.Delay(1000); + Driver.FindElement(By.Id("tab-0")).Click(); + + var tab = Driver.FindElement(By.Id("pane-0")); + var formItemList = tab.FindElements(By.CssSelector("input")); + + // 输入 账号名称 + var first = formItemList.First(); + first.Clear(); + first.SendKeys("test1"); + await Task.Delay(1000); + + // 输入 手机号码 + var second = formItemList.Skip(1).First(); + second.Clear(); + second.SendKeys("17396157893"); + await Task.Delay(1000); + + // 输入 姓名 + var third = formItemList.Skip(2).First(); + third.Clear(); + third.SendKeys("测试1"); + await Task.Delay(1000); + + // 阻塞 + WaitEnter(); + } +} \ No newline at end of file diff --git a/Admin.NET.Test/Utils/DateTimeUtilTests.cs b/Admin.NET.Test/Utils/DateTimeUtilTests.cs new file mode 100644 index 0000000..ceb88f2 --- /dev/null +++ b/Admin.NET.Test/Utils/DateTimeUtilTests.cs @@ -0,0 +1,287 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Xunit; + +namespace Admin.NET.Test.Utils; + +public class DateTimeUtilTests +{ + [Fact] + public void Init_WithTimeSpan_ReturnsCorrectDateTime() + { + // Arrange + var timeSpan = new TimeSpan(1, 0, 0, 0); // 1天 + + // Act + var dateTimeUtil = DateTimeUtil.Init(timeSpan); + + // Assert + Assert.Equal(DateTime.Now.AddDays(1).Date, dateTimeUtil.Date.Date); + } + + [Fact] + public void Init_WithDateTime_ReturnsCorrectDateTime() + { + // Arrange + var dateTime = new DateTime(2023, 10, 1); + + // Act + var dateTimeUtil = DateTimeUtil.Init(dateTime); + + // Assert + Assert.Equal(dateTime, dateTimeUtil.Date); + } + + [Fact] + public void GetTodayRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15, 12, 30, 0)); + + // Act + var (start, end) = dateTimeUtil.GetTodayRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 15), start); // 当天开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当天结束时间 + } + + [Fact] + public void GetMonthRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetMonthRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 1), start); // 本月第一天 + Assert.Equal(new DateTime(2023, 10, 31, 23, 59, 59), end); // 本月最后一天 + } + + [Fact] + public void GetFirstDayOfMonth_ReturnsCorrectDate() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var firstDay = dateTimeUtil.GetFirstDayOfMonth(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 1), firstDay); // 本月第一天 + } + + [Fact] + public void GetLastDayOfMonth_ReturnsCorrectDate() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var lastDay = dateTimeUtil.GetLastDayOfMonth(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 31, 23, 59, 59), lastDay); // 本月最后一天 + } + + [Fact] + public void GetYearRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetYearRange(); + + // Assert + Assert.Equal(new DateTime(2023, 1, 1), start); // 今年第一天 + Assert.Equal(new DateTime(2023, 12, 31, 23, 59, 59), end); // 今年最后一天 + } + + [Fact] + public void GetFirstDayOfYear_ReturnsCorrectDate() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var firstDay = dateTimeUtil.GetFirstDayOfYear(); + + // Assert + Assert.Equal(new DateTime(2023, 1, 1), firstDay); // 今年第一天 + } + + [Fact] + public void GetLastDayOfYear_ReturnsCorrectDate() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var lastDay = dateTimeUtil.GetLastDayOfYear(); + + // Assert + Assert.Equal(new DateTime(2023, 12, 31, 23, 59, 59), lastDay); // 今年最后一天 + } + + [Fact] + public void GetDayBeforeYesterdayRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetDayBeforeYesterdayRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 13), start); // 前天开始时间 + Assert.Equal(new DateTime(2023, 10, 13, 23, 59, 59), end); // 前天结束时间 + } + + [Fact] + public void GetYesterdayRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetYesterdayRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 14), start); // 昨天开始时间 + Assert.Equal(new DateTime(2023, 10, 14, 23, 59, 59), end); // 昨天结束时间 + } + + [Fact] + public void GetLastWeekRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); // 2023-10-15 是周日 + + // Act + var (start, end) = dateTimeUtil.GetLastWeekRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 8), start); // 上周第一天(周一) + Assert.Equal(new DateTime(2023, 10, 14, 23, 59, 59), end); // 上周最后一天(周日) + } + + [Fact] + public void GetThisWeekRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); // 2023-10-15 是周日 + + // Act + var (start, end) = dateTimeUtil.GetThisWeekRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 15), start); // 本周第一天(周一) + Assert.Equal(new DateTime(2023, 10, 21, 23, 59, 59), end); // 本周最后一天(周日) + } + + [Fact] + public void GetLastMonthRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLastMonthRange(); + + // Assert + Assert.Equal(new DateTime(2023, 9, 1), start); // 上月第一天 + Assert.Equal(new DateTime(2023, 9, 30, 23, 59, 59), end); // 上月最后一天 + } + + [Fact] + public void GetLast3DaysRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLast3DaysRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 13), start); // 3天前的开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当前日期的结束时间 + } + + [Fact] + public void GetLast7DaysRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLast7DaysRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 9), start); // 7天前的开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当前日期的结束时间 + } + + [Fact] + public void GetLast15DaysRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLast15DaysRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 1), start); // 15天前的开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当前日期的结束时间 + } + + [Fact] + public void GetLast3MonthsRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLast3MonthsRange(); + + // Assert + Assert.Equal(new DateTime(2023, 7, 15), start); // 3个月前的开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当前日期的结束时间 + } + + [Fact] + public void GetFirstHalfYearRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetFirstHalfYearRange(); + + // Assert + Assert.Equal(new DateTime(2023, 1, 1), start); // 上半年开始时间 + Assert.Equal(new DateTime(2023, 6, 30, 23, 59, 59), end); // 上半年结束时间 + } + + [Fact] + public void GetSecondHalfYearRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetSecondHalfYearRange(); + + // Assert + Assert.Equal(new DateTime(2023, 7, 1), start); // 下半年开始时间 + Assert.Equal(new DateTime(2023, 12, 31, 23, 59, 59), end); // 下半年结束时间 + } +} \ No newline at end of file diff --git a/Admin.NET.Test/Utils/SafeMathTests.cs b/Admin.NET.Test/Utils/SafeMathTests.cs new file mode 100644 index 0000000..b014855 --- /dev/null +++ b/Admin.NET.Test/Utils/SafeMathTests.cs @@ -0,0 +1,313 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Xunit; + +namespace Admin.NET.Test.Utils; + +public class SafeMathTests +{ + [Fact] + public void Add_IntAndDouble_ReturnsCorrectResult() + { + // Arrange + int left = 10; + double right = 20.5; + + // Act + var result = SafeMath.Add(left, right, precision: 2); + + // Assert + Assert.Equal(30, result); // 10 + 20.5 = 30.5,四舍五入后为 30 + } + + [Fact] + public void Add_StringAndDecimal_ReturnsCorrectResult() + { + // Arrange + string left = "15.75"; + decimal right = 4.25m; + + // Act + var result = SafeMath.Add(left, right, precision: 2); + + // Assert + Assert.Equal(20.00m, result); // 15.75 + 4.25 = 20.00 + } + + [Fact] + public void Sub_DoubleAndInt_ReturnsCorrectResult() + { + // Arrange + double left = 50.75; + int right = 25; + + // Act + var result = SafeMath.Sub(left, right, precision: 2); + + // Assert + Assert.Equal(25.75, result); // 50.75 - 25 = 25.75 + } + + [Fact] + public void Mult_DecimalAndFloat_ReturnsCorrectResult() + { + // Arrange + decimal left = 10.5m; + float right = 2.0f; + + // Act + var result = SafeMath.Mult(left, right, precision: 2); + + // Assert + Assert.Equal(21.00m, result); // 10.5 * 2.0 = 21.00 + } + + [Fact] + public void Div_IntAndInt_ReturnsCorrectResult() + { + // Arrange + int left = 10; + int right = 3; + + // Act + var result = SafeMath.Div(left, right, precision: 4); + + // Assert + Assert.Equal(3.3333, result); // 10 / 3 = 3.3333 + } + + [Fact] + public void Div_ByZero_ReturnsDefaultValue() + { + // Arrange + int left = 10; + int right = 0; + + // Act + int result = SafeMath.Div(left, right, defaultValue: -1, throwOnDivideByZero: false); + + // Assert + Assert.Equal(-1, result); // 除数为 0,返回默认值 -1 + } + + [Fact] + public void Div_ByZero_ThrowsException() + { + // Arrange + int left = 10; + int right = 0; + + // Act & Assert + Assert.Throws(() => + { + SafeMath.Div(left, right, throwOnDivideByZero: true); + }); + } + + [Fact] + public void SafeConvert_StringToInt_ReturnsCorrectResult() + { + // Arrange + string value = "42"; + + // Act + int result = SafeMath.SafeConvert(value, defaultValue: -1); + + // Assert + Assert.Equal(42, result); // 字符串 "42" 转换为 int 42 + } + + [Fact] + public void SafeConvert_InvalidString_ReturnsDefaultValue() + { + // Arrange + string value = "invalid"; + + // Act + int result = SafeMath.SafeConvert(value, defaultValue: -1); + + // Assert + Assert.Equal(-1, result); // 转换失败,返回默认值 -1 + } + + [Fact] + public void ConvertToDecimal_Int_ReturnsCorrectResult() + { + // Arrange + int value = 42; + + // Act + decimal result = SafeMath.ConvertToDecimal(value); + + // Assert + Assert.Equal(42m, result); // int 42 转换为 decimal 42m + } + + [Fact] + public void ConvertToDecimal_String_ReturnsCorrectResult() + { + // Arrange + string value = "42.75"; + + // Act + decimal result = SafeMath.ConvertToDecimal(value); + + // Assert + Assert.Equal(42.75m, result); // 字符串 "42.75" 转换为 decimal 42.75m + } + + [Fact] + public void ConvertToDecimal_InvalidString_ReturnsZero() + { + // Arrange + string value = "invalid"; + + // Act & Assert + Assert.Throws(() => SafeMath.ConvertToDecimal(value)); + } + + [Fact] + public void Add_LeftNull_ReturnsDefaultValue() + { + // Arrange + object left = null; + int right = 20; + + // Act + int result = SafeMath.Add(left, right); + + // Assert + Assert.Equal(20, result); // 左操作数为 null + } + + [Fact] + public void Add_RightNull_ReturnsDefaultValue() + { + // Arrange + int left = 10; + object right = null; + + // Act + var result = SafeMath.Add(left, right); + + // Assert + Assert.Equal(10, result); // 右操作数为 null + } + + [Fact] + public void Sub_LeftNull_ReturnsDefaultValue() + { + // Arrange + object left = null; + int right = 20; + + // Act + int result = SafeMath.Sub(left, right); + + // Assert + Assert.Equal(-20, result); // 左操作数为 null + } + + [Fact] + public void Sub_RightNull_ReturnsDefaultValue() + { + // Arrange + int left = 10; + object right = null; + + // Act + var result = SafeMath.Sub(left, right); + + // Assert + Assert.Equal(10, result); // 右操作数为 null + } + + [Fact] + public void Mult_LeftNull_ReturnsDefaultValue() + { + // Arrange + object left = null; + int right = 20; + + // Act + int result = SafeMath.Mult(left, right); + + // Assert + Assert.Equal(0, result); // 左操作数为 null + } + + [Fact] + public void Mult_RightNull_ReturnsDefaultValue() + { + // Arrange + int left = 10; + object right = null; + + // Act + int result = SafeMath.Mult(left, right); + + // Assert + Assert.Equal(0, result); // 右操作数为 null + } + + [Fact] + public void Div_LeftNull_ReturnsDefaultValue() + { + // Arrange + object left = null; + int right = 20; + + // Act + int result = SafeMath.Div(left, right); + + // Assert + Assert.Equal(0, result); // 左操作数为 null + } + + [Fact] + public void Div_RightNull_ReturnsDefaultValue() + { + // Arrange + int left = 10; + object right = null; + + // Act + Assert.Throws(() => + { + int result = SafeMath.Div(left, right); + // Assert + Assert.Equal(-1, result); // 右操作数为 null,返回默认值 -1 + }); + } + + [Fact] + public void SafeConvert_NullInput_ReturnsDefaultValue() + { + // Arrange + object value = null; + + // Act + int result = SafeMath.SafeConvert(value, defaultValue: -1); + + // Assert + Assert.Equal(-1, result); // 输入为 null,返回默认值 -1 + } + + [Fact] + public void ConvertToDecimal_NullInput_ReturnsZero() + { + // Arrange + object value = null; + + // Act + decimal result = SafeMath.ConvertToDecimal(value); + + // Assert + Assert.Equal(0m, result); // 输入为 null,返回默认值 0m + } +} \ No newline at end of file diff --git a/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj b/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj new file mode 100644 index 0000000..6193f32 --- /dev/null +++ b/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + 1701;1702;1591 + + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + + + + + + + + diff --git a/Admin.NET.Web.Core/Handlers/JwtHandler.cs b/Admin.NET.Web.Core/Handlers/JwtHandler.cs new file mode 100644 index 0000000..7899823 --- /dev/null +++ b/Admin.NET.Web.Core/Handlers/JwtHandler.cs @@ -0,0 +1,98 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Admin.NET.Core.Service; +using Furion; +using Furion.Authorization; +using Furion.DataEncryption; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using System; +using System.Threading.Tasks; + +namespace Admin.NET.Web.Core; + +public class JwtHandler : AppAuthorizeHandler +{ + private readonly SysCacheService _sysCacheService = App.GetRequiredService(); + private readonly SysConfigService _sysConfigService = App.GetRequiredService(); + private static readonly SysMenuService SysMenuService = App.GetRequiredService(); + + /// + /// 自动刷新Token + /// + /// + /// + /// + public override async Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext) + { + var userId = context.User.FindFirst(ClaimConst.UserId)?.Value; + var token = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); + + // 🛡️ 黑名单校验(包括用户和token) + if (_sysCacheService.ExistKey($"{CacheConst.KeyBlacklist}{userId}") || + _sysCacheService.ExistKey($"blacklist:token:{token}")) + { + context.Fail(); + context.GetCurrentHttpContext().SignoutToSwagger(); + return; + } + + var tokenExpire = await _sysConfigService.GetTokenExpire(); + var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire(); + if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext(), tokenExpire, refreshTokenExpire)) + { + await AuthorizeHandleAsync(context); + } + else + { + context.Fail(); // 授权失败 + var currentHttpContext = context.GetCurrentHttpContext(); + if (currentHttpContext == null) return; + + // 跳过由于 SignatureAuthentication 引发的失败 + if (currentHttpContext.Items.ContainsKey(SignatureAuthenticationDefaults.AuthenticateFailMsgKey)) return; + currentHttpContext.SignoutToSwagger(); + } + } + + public override async Task PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext) + { + // 已自动验证 Jwt Token 有效性 + return await CheckAuthorizeAsync(httpContext); + } + + /// + /// 权限校验核心逻辑 + /// + /// + /// + private static async Task CheckAuthorizeAsync(DefaultHttpContext httpContext) + { + // 登录模式判断PC、APP + if (App.User.FindFirst(ClaimConst.LoginMode)?.Value == ((int)LoginModeEnum.APP).ToString()) + return true; + + // 排除超管 + if (App.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString()) + return true; + + // 路由名称 + var routeName = httpContext.Request.Path.StartsWithSegments("/api") + ? httpContext.Request.Path.Value![5..].Replace("/", ":") + : httpContext.Request.Path.Value![1..].Replace("/", ":"); + + // 获取用户拥有按钮权限集合 + var ownBtnPermList = await SysMenuService.GetOwnBtnPermList(); + if (ownBtnPermList.Exists(u => routeName.Equals(u, StringComparison.CurrentCultureIgnoreCase))) + return true; + + // 获取系统所有按钮权限集合 + var allBtnPermList = await SysMenuService.GetAllBtnPermList(); + return allBtnPermList.TrueForAll(u => !routeName.Equals(u, StringComparison.CurrentCultureIgnoreCase)); + } +} \ No newline at end of file diff --git a/Admin.NET.Web.Core/ProjectOptions.cs b/Admin.NET.Web.Core/ProjectOptions.cs new file mode 100644 index 0000000..7564181 --- /dev/null +++ b/Admin.NET.Web.Core/ProjectOptions.cs @@ -0,0 +1,51 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using AspNetCoreRateLimit; +using Furion; +using Microsoft.Extensions.DependencyInjection; + +namespace Admin.NET.Web.Core; + +public static class ProjectOptions +{ + /// + /// 注册项目配置选项 + /// + /// + /// + public static IServiceCollection AddProjectOptions(this IServiceCollection services) + { + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.Configure(App.Configuration.GetSection("IpRateLimiting")); + services.Configure(App.Configuration.GetSection("IpRateLimitPolicies")); + services.Configure(App.Configuration.GetSection("ClientRateLimiting")); + services.Configure(App.Configuration.GetSection("ClientRateLimitPolicies")); + + return services; + } +} \ No newline at end of file diff --git a/Admin.NET.Web.Core/Startup.cs b/Admin.NET.Web.Core/Startup.cs new file mode 100644 index 0000000..4929a68 --- /dev/null +++ b/Admin.NET.Web.Core/Startup.cs @@ -0,0 +1,406 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Admin.NET.Core.ElasticSearch; +using Admin.NET.Core.Service; +using AspNetCoreRateLimit; +using Furion; +using Furion.Logging; +using Furion.SpecificationDocument; +using Furion.VirtualFileServer; +using IPTools.Core; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; +using OnceMi.AspNetCore.OSS; +using Scalar.AspNetCore; +using SixLabors.ImageSharp.Web.DependencyInjection; +using System; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; +using System.Threading.Tasks; + +#if NET10_0_OR_GREATER +using Admin.NET.Core.Update; +#endif + +namespace Admin.NET.Web.Core; + +[AppStartup(int.MaxValue)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + // 配置选项 + services.AddProjectOptions(); + + // 缓存注册 + services.AddCache(); + // SqlSugar + services.AddSqlSugar(); + // JWT + services.AddJwt(enableGlobalAuthorize: true, jwtBearerConfigure: options => + { + // 实现 JWT 身份验证过程控制 + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var httpContext = context.HttpContext; + // 若请求 Url 包含 token 参数,则设置 Token 值 + if (httpContext.Request.Query.ContainsKey("token")) + context.Token = httpContext.Request.Query["token"]; + return Task.CompletedTask; + } + }; + }).AddSignatureAuthentication(options => // 添加 Signature 身份验证 + { + options.Events = SysOpenAccessService.GetSignatureAuthenticationEventImpl(); + }); + + // 允许跨域 + services.AddCorsAccessor(); + // 远程请求 + services.AddHttpRemote(); + // 任务队列 + services.AddTaskQueue(); + // 任务调度 + services.AddSchedule(options => + { + options.AddPersistence(); // 添加作业持久化器 + options.AddMonitor(); // 添加作业执行监视器 + }); + // 脱敏检测 + services.AddSensitiveDetection(); + + // Json序列化设置 + static void SetNewtonsoftJsonSetting(JsonSerializerSettings setting) + { + setting.DateFormatHandling = DateFormatHandling.IsoDateFormat; + setting.DateTimeZoneHandling = DateTimeZoneHandling.Local; + //setting.Converters.AddDateTimeTypeConverters(localized: false); // 时间本地化 + setting.DateFormatString = "yyyy-MM-dd HH:mm:ss"; // 时间格式化 + setting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // 忽略循环引用 + // setting.ContractResolver = new CamelCasePropertyNamesContractResolver(); // 解决动态对象属性名大写 + // setting.NullValueHandling = NullValueHandling.Ignore; // 忽略空值 + setting.Converters.AddLongTypeConverters(); // long转string(防止js精度溢出) 超过17位开启 + // setting.MetadataPropertyHandling = MetadataPropertyHandling.Ignore; // 解决DateTimeOffset异常 + // setting.DateParseHandling = DateParseHandling.None; // 解决DateTimeOffset异常 + // setting.Converters.Add(new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }); // 解决DateTimeOffset异常 + } + ; + + services.AddControllersWithViews() + .AddAppLocalization() + .AddNewtonsoftJson(options => SetNewtonsoftJsonSetting(options.SerializerSettings)) + //.AddXmlSerializerFormatters() + //.AddXmlDataContractSerializerFormatters() + .AddInjectWithUnifyResult() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); // 禁止Unicode转码 + options.JsonSerializerOptions.Converters.AddDateTimeTypeConverters("yyyy-MM-dd HH:mm:ss"); // 时间格式化 + }); + + // 三方授权登录OAuth + services.AddOAuth(); + + // ElasticSearch + services.AddElasticSearchClients(); + + // 配置Nginx转发获取客户端真实IP + // 注1:如果负载均衡不是在本机通过 Loopback 地址转发请求的,一定要加上options.KnownNetworks.Clear()和options.KnownProxies.Clear() + // 注2:如果设置环境变量 ASPNETCORE_FORWARDEDHEADERS_ENABLED 为 True,则不需要下面的配置代码 + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.All; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + }); + + // 限流服务 + services.AddInMemoryRateLimiting(); + services.AddSingleton(); + + // 事件总线 + services.AddEventBus(options => + { + options.UseUtcTimestamp = false; + // 不启用事件日志 + options.LogEnabled = false; + // 事件执行器(失败重试) + options.AddExecutor(); + // 事件执行器(重试后依然处理未处理异常的处理器) + options.UnobservedTaskExceptionHandler = (obj, args) => + { + if (args.Exception?.Message != null) + Log.Error($"EeventBus 有未处理异常 :{args.Exception?.Message} ", args.Exception); + }; + // 事件执行器-监视器(每一次处理都会进入) + options.AddMonitor(); + + #region Redis消息队列 + + // 替换事件源存储器为Redis + var cacheOptions = App.GetConfig("Cache", true); + if (cacheOptions.CacheType == CacheTypeEnum.Redis.ToString()) + { + options.ReplaceStorer(serviceProvider => + { + var cacheProvider = serviceProvider.GetRequiredService(); + // 创建默认内存通道事件源对象,可自定义队列路由key,如:adminnet_eventsource_queue + return new RedisEventSourceStorer(cacheProvider, "adminnet_eventsource_queue", 3000); + }); + } + + #endregion Redis消息队列 + + #region RabbitMQ消息队列 + + //// 创建默认内存通道事件源对象,可自定义队列路由key,如:adminnet + //var eventBusOpt = App.GetConfig("EventBus", true); + //var rbmqEventSourceStorer = new RabbitMQEventSourceStore(new ConnectionFactory + //{ + // UserName = eventBusOpt.RabbitMQ.UserName, + // Password = eventBusOpt.RabbitMQ.Password, + // HostName = eventBusOpt.RabbitMQ.HostName, + // Port = eventBusOpt.RabbitMQ.Port + //}, "adminnet", 3000); + + //// 替换默认事件总线存储器 + //options.ReplaceStorer(serviceProvider => + //{ + // return rbmqEventSourceStorer; + //}); + + #endregion RabbitMQ消息队列 + }); + + // 图像处理 + services.AddImageSharp(); + + // OSS对象存储 + var ossOpt = App.GetConfig("OSSProvider", true); + services.AddOSSService(Enum.GetName(ossOpt.Provider), "OSSProvider"); + + // 文件存储服务 + services.AddTransient(); + services.AddSingleton(); // 改为单例以保持缓存 + services.AddTransient(); + + // 模板引擎 + services.AddViewEngine(); + + // 即时通讯 + services.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.KeepAliveInterval = TimeSpan.FromSeconds(15); // 服务器端向客户端ping的间隔 + options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); // 客户端向服务器端ping的间隔 + options.MaximumReceiveMessageSize = 1024 * 1014 * 10; // 数据包大小10M,默认最大为32K + }).AddNewtonsoftJsonProtocol(options => SetNewtonsoftJsonSetting(options.PayloadSerializerSettings)); + + // 系统日志 + services.AddLoggingSetup(); + + // 验证码 + services.AddCaptcha(); + + // 控制台logo + services.AddConsoleLogo(); + + //// Swagger 时间格式化 + //services.AddSwaggerGen(c => + //{ + // c.MapType(() => new Microsoft.OpenApi.Models.OpenApiSchema + // { + // Type = "string", + // Format = "date-time", + // Example = new Microsoft.OpenApi.Any.OpenApiString(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")) // 示例值 + // }); + + // // 确保生成的文档包含 OpenAPI 版本字段 + // c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + // { + // Version = "v1", + // Title = "Admin.NET API", + // Description = "Admin.NET 通用权限开发平台" + // }); + // c.OperationFilter(); + //}); + + // 将IP地址数据库文件完全加载到内存,提升查询速度(以空间换时间,内存将会增加60-70M) + IpToolSettings.LoadInternationalDbToMemory = true; + // 设置默认查询器China和International + //IpToolSettings.DefalutSearcherType = IpSearcherType.China; + IpToolSettings.DefalutSearcherType = IpSearcherType.International; + + // 配置gzip与br的压缩等级为最优 + //services.Configure(options => + //{ + // options.Level = CompressionLevel.Optimal; + //}); + //services.Configure(options => + //{ + // options.Level = CompressionLevel.Optimal; + //}); + // 注册压缩响应 + services.AddResponseCompression((options) => + { + options.EnableForHttps = true; + options.Providers.Add(); + options.Providers.Add(); + options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( + [ + "text/html; charset=utf-8", + "application/xhtml+xml", + "application/atom+xml", + "image/svg+xml" + ]); + }); + + // 注册虚拟文件系统服务 + services.AddVirtualFileServer(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // 响应压缩 + app.UseResponseCompression(); + + app.UseForwardedHeaders(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + app.UseHsts(); + } + + app.Use(async (context, next) => + { + context.Response.Headers.Append("Admin.NET", "Admin.NET"); + await next(); + }); + + // 图像处理 + app.UseImageSharp(); + + // 特定文件类型(文件后缀)处理 + var contentTypeProvider = FS.GetFileExtensionContentTypeProvider(); + // contentTypeProvider.Mappings[".文件后缀"] = "MIME 类型"; + app.UseStaticFiles(new StaticFileOptions + { + ContentTypeProvider = contentTypeProvider + }); + // 二级目录文件路径解析 + if (!string.IsNullOrEmpty(App.Settings.VirtualPath)) + app.UseStaticFiles(new StaticFileOptions + { + RequestPath = App.Settings.VirtualPath, + FileProvider = App.WebHostEnvironment.WebRootFileProvider + }); + //// 启用HTTPS + //app.UseHttpsRedirection(); + + // 启用OAuth + app.UseOAuth(); + + // 添加状态码拦截中间件 + app.UseUnifyResultStatusCodes(); + + // 启用多语言,必须在 UseRouting 之前 + app.UseAppLocalization(); + + // 路由注册 + app.UseRouting(); + + // 启用跨域,必须在 UseRouting 和 UseAuthentication 之间注册 + app.UseCorsAccessor(); + + // 启用鉴权授权 + app.UseAuthentication(); + app.UseAuthorization(); + + // 限流组件(在跨域之后) + app.UseIpRateLimiting(); + app.UseClientRateLimiting(); + app.UsePolicyRateLimit(); + + // 任务调度看板 + app.UseScheduleUI(options => + { + options.RequestPath = "/schedule"; // 必须以 / 开头且不以 / 结尾 + options.DisableOnProduction = false; // 是否在生产环境中关闭 + options.DisplayEmptyTriggerJobs = true; // 是否显示空作业触发器的作业 + options.DisplayHead = false; // 是否显示页头 + options.DefaultExpandAllJobs = false; // 是否默认展开所有作业 + options.EnableDirectoryBrowsing = false; // 是否启用目录浏览 + options.Title = "定时任务看板"; // 自定义看板标题 + + options.LoginConfig.OnLoging = async (username, password, httpContext) => + { + var res = await httpContext.RequestServices.GetRequiredService().SwaggerSubmitUrl(new SpecificationAuth { UserName = username, Password = password }); + return res == 200; + }; + options.LoginConfig.DefaultUsername = ""; + options.LoginConfig.DefaultPassword = ""; + options.LoginConfig.SessionKey = "schedule_session_key"; // 登录客户端存储的 Session 键 + }); + + app.UseInject(string.Empty, options => + { + foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) + { + groupInfo.Description += "
👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!"; + } + options.ConfigureSwagger(m => + { + m.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0; + }); + }); + +#if NET10_0_OR_GREATER + app.UseAutoVersionUpdate(); +#endif + + app.UseEndpoints(endpoints => + { + // 配置 Scalar 第三方 UI 集成(路由前缀一致代表独立,不同则代表共存) + if (App.GetConfig("AppSettings:InjectSpecificationDocument", true)) + { + endpoints.MapScalarApiReference("sapi", options => + { + options.WithTitle("Admin.NET"); + + // 配置 OpenAPI 文档 + foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) + { + options.AddDocument(groupInfo.Group, groupInfo.Title, groupInfo.RouteTemplate); + } + }); + } + // 注册集线器 + endpoints.MapHubs(); + + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } +} \ No newline at end of file diff --git a/Evergreen.Health.Api/AlipayCrt/alipayPublicCert.crt b/Evergreen.Health.Api/AlipayCrt/alipayPublicCert.crt new file mode 100644 index 0000000..e69de29 diff --git a/Evergreen.Health.Api/AlipayCrt/alipayRootCert.crt b/Evergreen.Health.Api/AlipayCrt/alipayRootCert.crt new file mode 100644 index 0000000..e69de29 diff --git a/Evergreen.Health.Api/AlipayCrt/appPublicCert.crt b/Evergreen.Health.Api/AlipayCrt/appPublicCert.crt new file mode 100644 index 0000000..e69de29 diff --git a/Evergreen.Health.Api/Configuration/Database.local.example.json b/Evergreen.Health.Api/Configuration/Database.local.example.json new file mode 100644 index 0000000..c80c2ca --- /dev/null +++ b/Evergreen.Health.Api/Configuration/Database.local.example.json @@ -0,0 +1,10 @@ +{ + "DbConnection": { + "ConnectionConfigs": [ + { + "DbType": "MySqlConnector", + "ConnectionString": "Server=127.0.0.1;Port=3306;Database=evergreen_health;User ID=root;Password=CHANGE_ME;CharSet=utf8mb4;SslMode=None;Pooling=true;Connection Timeout=30" + } + ] + } +} diff --git a/Evergreen.Health.Api/Controllers/HomeController.cs b/Evergreen.Health.Api/Controllers/HomeController.cs new file mode 100644 index 0000000..66aad03 --- /dev/null +++ b/Evergreen.Health.Api/Controllers/HomeController.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Evergreen.Health.Api.Controllers +{ + [AllowAnonymous] + public class HomeController : Controller + { + //private readonly ISystemService _systemService; + + //public HomeController(ISystemService systemService) + //{ + // _systemService = systemService; + //} + + public IActionResult Index() + { + //ViewBag.Description = _systemService.GetDescription(); + + return View(); + } + } +} \ No newline at end of file diff --git a/Evergreen.Health.Api/Dockerfile b/Evergreen.Health.Api/Dockerfile new file mode 100644 index 0000000..7f10a38 --- /dev/null +++ b/Evergreen.Health.Api/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 5005 + +COPY . . + +# 设置语言/区域设置环境变量 +ENV LANG zh-Hans + +# 使用阿里云的镜像源进行更新 +# .NET6使用 +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/' /etc/apt/sources.list +# .NET8使用 +# RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/' /etc/apt/sources.list.d/debian.sources +# 更新包管理器并安装free命令 +RUN apt-get update && apt-get install -y procps + +ENTRYPOINT ["dotnet", "Evergreen.Health.Api.dll"] \ No newline at end of file diff --git a/Evergreen.Health.Api/Evergreen.Health.Api.csproj b/Evergreen.Health.Api/Evergreen.Health.Api.csproj new file mode 100644 index 0000000..b2b98ec --- /dev/null +++ b/Evergreen.Health.Api/Evergreen.Health.Api.csproj @@ -0,0 +1,67 @@ + + + + net8.0 + enable + zh-Hans + true + false + ad9369d1-f29b-4f8f-a7df-8b4d7aa0726b + Linux + true + Admin.NET + 常青树健康管理系统 + 1.0.0 + 1.0.0 + 1.0.0 + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + PreserveNewest + + + PreserveNewest + + + Always + + + + + + Never + + + + + + PreserveNewest + + + + + + + + diff --git a/Evergreen.Health.Api/FakeStartup.cs b/Evergreen.Health.Api/FakeStartup.cs new file mode 100644 index 0000000..f1c4957 --- /dev/null +++ b/Evergreen.Health.Api/FakeStartup.cs @@ -0,0 +1,14 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Evergreen.Health.Api; + +/// +/// 供集成测试使用 +/// +public class FakeStartup +{ +} \ No newline at end of file diff --git a/Evergreen.Health.Api/GeoLite2-City.mmdb b/Evergreen.Health.Api/GeoLite2-City.mmdb new file mode 100644 index 0000000..f5e23dc Binary files /dev/null and b/Evergreen.Health.Api/GeoLite2-City.mmdb differ diff --git a/Evergreen.Health.Api/Program.cs b/Evergreen.Health.Api/Program.cs new file mode 100644 index 0000000..549fdd4 --- /dev/null +++ b/Evergreen.Health.Api/Program.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +Serve.Run(RunOptions.Default.AddWebComponent()); + +public class WebComponent : IWebComponent +{ + public void Load(WebApplicationBuilder builder, ComponentContext componentContext) + { + // 本机数据库配置。该文件已加入 .gitignore,仅用于保存真实连接信息。 + builder.Configuration.AddJsonFile( + "Configuration/Database.local.json", + optional: true, + reloadOnChange: true); + + // 设置日志过滤 + builder.Logging.AddFilter((provider, category, logLevel) => + { + return !new[] { "Microsoft.Hosting", "Microsoft.AspNetCore" }.Any(u => category.StartsWith(u)) && logLevel >= LogLevel.Information; + }); + + // 设置接口超时时间和上传大小-Kestrel + builder.WebHost.ConfigureKestrel(u => + { + u.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(30); + u.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(30); + u.Limits.MaxRequestBodySize = null; + }); + } +} diff --git a/Evergreen.Health.Api/Properties/launchSettings.json b/Evergreen.Health.Api/Properties/launchSettings.json new file mode 100644 index 0000000..90b28e4 --- /dev/null +++ b/Evergreen.Health.Api/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5005" + // "sslPort": 44325 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Evergreen.Health.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "http://localhost:5005", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Evergreen.Health.Api/Resources/Lang.en.resx b/Evergreen.Health.Api/Resources/Lang.en.resx new file mode 100644 index 0000000..d555440 --- /dev/null +++ b/Evergreen.Health.Api/Resources/Lang.en.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Admin NET General Permission Development Platform + + + API Interfaces + + + Source Address + + \ No newline at end of file diff --git a/Evergreen.Health.Api/Resources/Lang.zh-CN.resx b/Evergreen.Health.Api/Resources/Lang.zh-CN.resx new file mode 100644 index 0000000..4c25852 --- /dev/null +++ b/Evergreen.Health.Api/Resources/Lang.zh-CN.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Admin.NET 通用权限开发平台 + + + API 接口 + + + 源码地址 + + \ No newline at end of file diff --git a/Evergreen.Health.Api/SingleFilePublish.cs b/Evergreen.Health.Api/SingleFilePublish.cs new file mode 100644 index 0000000..cefe9e9 --- /dev/null +++ b/Evergreen.Health.Api/SingleFilePublish.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion; +using System.Reflection; + +namespace Evergreen.Health.Api; + +/// +/// 解决单文件发布问题 +/// +public class SingleFilePublish : ISingleFilePublish +{ + /// + /// 解决单文件不能扫描的程序集 + /// + /// 和 可同时配置 + /// + public Assembly[] IncludeAssemblies() + { + // 需要 Furion 框架扫描哪些程序集就写上去即可 + return Array.Empty(); + } + + /// + /// 解决单文件不能扫描的程序集名称 + /// + /// 和 可同时配置 + /// + public string[] IncludeAssemblyNames() + { + // 需要 Furion 框架扫描哪些程序集就写上去即可 + return new[] + { + "Evergreen.Health.Application", + "Admin.NET.Core", + "Admin.NET.Web.Core", + }; + } +} \ No newline at end of file diff --git a/Evergreen.Health.Api/UpdateScripts/1.0.0.sql b/Evergreen.Health.Api/UpdateScripts/1.0.0.sql new file mode 100644 index 0000000..d1f760f --- /dev/null +++ b/Evergreen.Health.Api/UpdateScripts/1.0.0.sql @@ -0,0 +1,3 @@ +-- 1.0.0.sql +-- update +-- 2025-05-24 11:00:00 \ No newline at end of file diff --git a/Evergreen.Health.Api/Views/Home/Index.cshtml b/Evergreen.Health.Api/Views/Home/Index.cshtml new file mode 100644 index 0000000..0abb7d0 --- /dev/null +++ b/Evergreen.Health.Api/Views/Home/Index.cshtml @@ -0,0 +1,7 @@ +@{ + ViewData["Title"] = ViewBag.Description; +} + +
+ +
\ No newline at end of file diff --git a/Evergreen.Health.Api/Views/Shared/_Layout.cshtml b/Evergreen.Health.Api/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..b56f063 --- /dev/null +++ b/Evergreen.Health.Api/Views/Shared/_Layout.cshtml @@ -0,0 +1,11 @@ + + + + + + @ViewData["Title"] - Furion + + + @RenderBody() + + \ No newline at end of file diff --git a/Evergreen.Health.Api/Views/_ViewImports.cshtml b/Evergreen.Health.Api/Views/_ViewImports.cshtml new file mode 100644 index 0000000..a55228d --- /dev/null +++ b/Evergreen.Health.Api/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using Evergreen.Health.Api +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/Evergreen.Health.Api/Views/_ViewStart.cshtml b/Evergreen.Health.Api/Views/_ViewStart.cshtml new file mode 100644 index 0000000..d641c67 --- /dev/null +++ b/Evergreen.Health.Api/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} \ No newline at end of file diff --git a/Evergreen.Health.Api/appsettings.Development.json b/Evergreen.Health.Api/appsettings.Development.json new file mode 100644 index 0000000..c072d2b --- /dev/null +++ b/Evergreen.Health.Api/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "ConfigurationScanDirectories": [ "Configuration", "" ] +} \ No newline at end of file diff --git a/Evergreen.Health.Api/appsettings.json b/Evergreen.Health.Api/appsettings.json new file mode 100644 index 0000000..cd1c674 --- /dev/null +++ b/Evergreen.Health.Api/appsettings.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "ConfigurationScanDirectories": [ "Configuration", "" ] // 扫描配置文件json文件夹(自动合并该文件夹里面所有json文件) +} \ No newline at end of file diff --git a/Evergreen.Health.Api/sensitive-words.txt b/Evergreen.Health.Api/sensitive-words.txt new file mode 100644 index 0000000..901a560 --- /dev/null +++ b/Evergreen.Health.Api/sensitive-words.txt @@ -0,0 +1,4 @@ +装X|特么的|SB|屌爆了|你妹|马勒戈壁|蛋疼|买了个表|妈蛋|日了狗 +吃翔 +装13 +屁民 diff --git a/Evergreen.Health.Api/web.config b/Evergreen.Health.Api/web.config new file mode 100644 index 0000000..002dc98 --- /dev/null +++ b/Evergreen.Health.Api/web.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Evergreen.Health.Api/wwwroot/images/logo.png b/Evergreen.Health.Api/wwwroot/images/logo.png new file mode 100644 index 0000000..da79429 Binary files /dev/null and b/Evergreen.Health.Api/wwwroot/images/logo.png differ diff --git a/Evergreen.Health.Api/wwwroot/template/Dto.cs.vm b/Evergreen.Health.Api/wwwroot/template/Dto.cs.vm new file mode 100644 index 0000000..69a9459 --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/Dto.cs.vm @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace @(Model.NameSpace); + +/// +/// @(Model.BusName)输出参数 +/// +public class @(Model.ClassName)Dto +{ +@foreach (var column in Model.TableField){ +if(column.EffectType == "ForeignKey"){ + @:/// + @:/// @column.ColumnComment + @:/// + @:public string @(column.PropertyName)FkColumn { get; set; } + @: +} +} +@foreach (var column in Model.TableField){ + @:/// + @:/// @column.ColumnComment + @:/// + @:public @column.NetType @column.PropertyName { get; set; } + @: +} +} diff --git a/Evergreen.Health.Api/wwwroot/template/Entity.cs.vm b/Evergreen.Health.Api/wwwroot/template/Entity.cs.vm new file mode 100644 index 0000000..9e03652 --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/Entity.cs.vm @@ -0,0 +1,46 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +@if(Model.BaseClassName!=""){ +@:using Admin.NET.Core; +@:using SqlSugar; +} +namespace @(Model.NameSpace); + +/// +/// @(Model.Description) +/// +[Tenant("@(Model.ConfigId)")] +[SugarTable("@(Model.TableName)", "@(Model.Description)")] +public partial class @(Model.EntityName) @Model.BaseClassName +{ +@foreach (var column in Model.TableField) { + var propSuffix = ""; + if (column.IsPrimarykey && (Model.BaseClassName == "" || Model.BaseClassName != "" && column.DbColumnName.ToLower() != "id")) { + propSuffix = $", IsPrimaryKey = true, IsIdentity = {column.IsIdentity.ToString().ToLower()}"; + } + + if (column.DataType.TrimEnd('?') == "string") { + propSuffix += $", Length = {column.Length}"; + } else if (column.DataType.TrimEnd('?') == "decimal") { + propSuffix += $", Length = {column.Length}, DecimalDigits={column.DecimalDigits}"; + } + + if(!string.IsNullOrWhiteSpace(column.DefaultValue)){ + propSuffix +=$", DefaultValue = \"{column.DefaultValue}\""; + } + + @:/// + @:/// @column.ColumnDescription + @:/// + if(!column.IsNullable){ + @:[Required] + } + @:[SugarColumn(ColumnName = "@column.DbColumnName", ColumnDescription = "@column.ColumnDescription"@propSuffix)] + @:public virtual @column.DataType @column.PropertyName { get; set; } + @: +} +} diff --git a/Evergreen.Health.Api/wwwroot/template/Input.cs.vm b/Evergreen.Health.Api/wwwroot/template/Input.cs.vm new file mode 100644 index 0000000..b972c9f --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/Input.cs.vm @@ -0,0 +1,200 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using System.ComponentModel.DataAnnotations; +using Magicodes.ExporterAndImporter.Core; +using Magicodes.ExporterAndImporter.Excel; + +namespace @(Model.NameSpace); + +/// +/// @(Model.BusName)基础输入参数 +/// +public class @(Model.ClassName)BaseInput +{ +@foreach (var column in Model.PrimaryKeyFieldList.Concat(Model.AddUpdateFieldList)){ + @:/// + @:/// @column.ColumnComment + @:/// + if (column.EffectType is "EnumSelector" or "DictSelector") { + @:[Dict(@(column.EffectType == "EnumSelector" ? $"nameof({column.DictTypeCode})" : $"\"{column.DictTypeCode}\""), AllowNullValue=true)] + } + if (column.WhetherRequired == "Y") { + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + } + @:public virtual @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: +} +} + +/// +/// @(Model.BusName)分页查询输入参数 +/// +public class Page@(Model.ClassName)Input : BasePageInput +{ +@foreach (var column in Model.TableField.Where(u => u.WhetherQuery == "Y")){ + if(column.NetType?.TrimEnd('?') == "DateTime" && column.QueryType == "~"){ + @:/// + @:/// @(column.ColumnComment)范围 + @:/// + @: public DateTime?[] @(column.PropertyName)Range { get; set; } + } else { + @:/// + @:/// @column.ColumnComment + @:/// + if (column.EffectType is "EnumSelector" or "DictSelector") { + @:[Dict(@(column.EffectType == "EnumSelector" ? $"nameof({column.DictTypeCode})" : $"\"{column.DictTypeCode}\""), AllowNullValue=true)] + } + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + } + @: +} +@if (Model.ImportFieldList.Count > 0){ + var primaryKey = Model.PrimaryKeyFieldList.First(); + @:/// + @:/// 选中主键列表 + @:/// + @: public List<@(primaryKey.NetType)> SelectKeyList { get; set; } +} +} + +/// +/// @(Model.BusName)增加输入参数 +/// +public class Add@(Model.ClassName)Input +{ +@foreach (var column in Model.AddUpdateFieldList){ + @:/// + @:/// @column.ColumnComment + @:/// + if (column.EffectType is "EnumSelector" or "DictSelector") { + @:[Dict(@(column.EffectType == "EnumSelector" ? $"nameof({column.DictTypeCode})" : $"\"{column.DictTypeCode}\""), AllowNullValue=true)] + } + if (column.WhetherRequired == "Y") { + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + } + if (column.NetType.TrimEnd('?').EndsWith("string") && column.ColumnLength > 0){ + @:[MaxLength(@column.ColumnLength, ErrorMessage = "@(column.ColumnComment)字符长度不能超过@(column.ColumnLength)")] + } + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: +} +} + +/// +/// @(Model.BusName)删除输入参数 +/// +public class Delete@(Model.ClassName)Input +{ +@foreach (var column in Model.PrimaryKeyFieldList) { + @:/// + @:/// @column.ColumnComment + @:/// + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: +} +} + +/// +/// @(Model.BusName)更新输入参数 +/// +public class Update@(Model.ClassName)Input +{ + @foreach (var column in Model.PrimaryKeyFieldList.Concat(Model.AddUpdateFieldList)){ + @:/// + @:/// @column.ColumnComment + @:/// + if (column.EffectType is "EnumSelector" or "DictSelector") { + @:[Dict(@(column.EffectType == "EnumSelector" ? $"nameof({column.DictTypeCode})" : $"\"{column.DictTypeCode}\""), AllowNullValue=true)] + } + if (column.WhetherRequired == "Y" || column.ColumnKey == "True") { + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + } + if (column.NetType.TrimEnd('?').EndsWith("string") && column.ColumnLength > 0){ + @:[MaxLength(@column.ColumnLength, ErrorMessage = "@(column.ColumnComment)字符长度不能超过@(column.ColumnLength)")] + } + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: + } +} + +/// +/// @(Model.BusName)主键查询输入参数 +/// +public class QueryById@(Model.ClassName)Input : Delete@(Model.ClassName)Input +{ +} + +@if (Model.DropdownFieldList.Count > 0) { +@:/// +@:/// 下拉数据输入参数 +@:/// +@:public class DropdownData@(Model.ClassName)Input +@:{ + @:/// + @:/// 是否用于分页查询 + @:/// + @:public bool FromPage { get; set; } +@:} +@: +} +@if (Model.HasSetStatus) { +@:/// +@:/// 设置状态输入参数 +@:/// +@:public class Set@(Model.ClassName)StatusInput : BaseStatusInput +@:{ + @foreach (var column in Model.PrimaryKeyFieldList.Where(u => u.PropertyName != "Id")) { + @:/// + @:/// @column.ColumnComment + @:/// + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: + } +@:} +@: +} +@if (Model.ImportFieldList.Count > 0){ +@:/// +@:/// @(Model.BusName)数据导入实体 +@:/// +@:[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)] +@:public class Import@(Model.ClassName)Input : BaseImportInput +@:{ + foreach (var column in Model.ImportFieldList){ + var headerName = (column.WhetherRequired == "Y" ? "*" : "") + column.ColumnComment; + if(column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector" || column.EffectType == "DictSelector") { + @:/// + @:/// @column.ColumnComment 关联值 + @:/// + @:[ImporterHeader(IsIgnore = true)] + @:[ExporterHeader(IsIgnore = true)] + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: + @:/// + @:/// @column.ColumnComment 文本 + @:/// + if (column.EffectType == "DictSelector") { + @:[Dict(@($"\"{column.DictTypeCode}\""))] + } + @:[ImporterHeader(Name = "@(headerName)")] + @:[ExporterHeader("@(headerName)", Format = "@", Width = 25, IsBold = true)] + @:public string @column.ExtendedPropertyName { get; set; } + } else { + @:/// + @:/// @column.ColumnComment + @:/// + @:[ImporterHeader(Name = "@(headerName)")] + @:[ExporterHeader("@(headerName)", Format = "@", Width = 25, IsBold = true)] + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + } + @: + } +@:} +} \ No newline at end of file diff --git a/Evergreen.Health.Api/wwwroot/template/Output.cs.vm b/Evergreen.Health.Api/wwwroot/template/Output.cs.vm new file mode 100644 index 0000000..e1f2c11 --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/Output.cs.vm @@ -0,0 +1,71 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! +using Magicodes.ExporterAndImporter.Core; +namespace @(Model.NameSpace); + +/// +/// @(Model.BusName)输出参数 +/// +public class @(Model.ClassName)Output +{ + @foreach (var column in Model.TableField){ + @:/// + @:/// @column.ColumnComment + @:/// + @:public @column.NetType @(column.PropertyName) { get; set; } + if(column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector") { + @: + @:/// + @:/// @(column.ColumnComment) 描述 + @:/// + @:public string @(column.ExtendedPropertyName) { get; set; } + }else if(column.EffectType == "Upload" || column.EffectType == "Upload_SingleFile"){ + @: + @:/// + @:/// @(column.ColumnComment) 附件信息 + @:/// + @:public SysFile @(column.ExtendedPropertyName) { get; set; } + } + @: + } +} +@if (Model.ApiTreeFieldList.Count > 0) { +foreach(var column in Model.ApiTreeFieldList) { +@:/// +@:/// @column.ColumnComment 树选择器输出参数 +@:/// +@:public class Tree@(column.PropertyNameTrimEndId)Output : @(Model.ClassName) +@:{ + @:/// + @:/// 节点值 + @:/// + @:public @column.NetType Value { get; set; } + @: + @:/// + @:/// 节点文本 + @:/// + @:public string Label { get; set; } + @: + @:/// + @:/// 子集数据 + @:/// + @:public List Children { get; set; } +@:} +@: +} +} +@if (Model.ImportFieldList.Count > 0) { +@: +@:/// +@:/// @(Model.BusName)数据导入模板实体 +@:/// +@:public class Export@(Model.ClassName)Output : Import@(Model.ClassName)Input +@:{ +@: [ImporterHeader(IsIgnore = true)] +@: [ExporterHeader(IsIgnore = true)] +@: public override string Error { get; set; } +@:} +} \ No newline at end of file diff --git a/Evergreen.Health.Api/wwwroot/template/SeedData.cs.vm b/Evergreen.Health.Api/wwwroot/template/SeedData.cs.vm new file mode 100644 index 0000000..2101c5b --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/SeedData.cs.vm @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using @Model.EntityNameSpace; + +namespace @(Model.NameSpace); + +/// +/// @(Model.Description) 表种子数据 +/// +public class @(Model.SeedDataName) : ISqlSugarEntitySeedData<@(Model.EntityName)> +{ + /// + /// 种子数据 + /// + /// + public IEnumerable<@(Model.EntityName)> HasData() + { + return new List<@(Model.EntityName)> { + @foreach (var record in Model.RecordList) { + @:new() { @record }, + } + }; + } +} \ No newline at end of file diff --git a/Evergreen.Health.Api/wwwroot/template/Service.cs.vm b/Evergreen.Health.Api/wwwroot/template/Service.cs.vm new file mode 100644 index 0000000..7005846 --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/Service.cs.vm @@ -0,0 +1,421 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core.Service; +using Microsoft.AspNetCore.Http; +using Furion.DatabaseAccessor; +using Furion.FriendlyException; +using Mapster; +using SqlSugar; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using @(Model.NameSpace).Entity; +namespace @(Model.NameSpace); + +/// +/// @(Model.BusName)服务 🧩 +/// +[ApiDescriptionSettings(@(Model.ProjectLastName)Const.GroupName, Order = 100)] +public partial class @(Model.ClassName)Service : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository<@(Model.ClassName)> _@(Model.LowerClassName)Rep; + @foreach(var kv in Model.InjectServiceMap) { + @:private readonly @(kv.Key) _@(kv.Value); + } + + public @(Model.ClassName)Service(SqlSugarRepository<@(Model.ClassName)> @(Model.LowerClassName)Rep@(Model.InjectServiceArgs)) + { + _@(Model.LowerClassName)Rep = @(Model.LowerClassName)Rep; + @foreach(var kv in Model.InjectServiceMap) { + @:_@(kv.Value) = @(kv.Value); + } + } + + /// + /// 分页查询@(Model.BusName) 🔖 + /// + /// + /// + [DisplayName("分页查询@(Model.BusName)")] + [ApiDescriptionSettings(Name = "Page"), HttpPost] + public async Task> Page(Page@(Model.ClassName)Input input) + { + input.Keyword = input.Keyword?.Trim(); + var query = _@(Model.LowerClassName)Rep.AsQueryable() + @{ + string joinTableName = "u"; + var queryFields = Model.TableField.Where(u => u.WhetherQuery == "Y"); + // 关键字模糊查询 + if (queryFields.Any(u => u.QueryType == "like")) { + @:.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => @string.Join(" || ", queryFields.Where(u => u.QueryType == "like").Select(col => $"u.{col.PropertyName}.Contains(input.Keyword)"))) + } + + // 单字段模糊查询 + foreach(var column in queryFields.Where(u => u.QueryType == "like")) { + @:.WhereIF(!string.IsNullOrWhiteSpace(input.@(column.PropertyName)), u => u.@(column.PropertyName).Contains(input.@(column.PropertyName).Trim())) + } + + // 字段组合查询 + foreach(var column in queryFields.Where(u => u.QueryType != "like")) { + if (column.NetType.TrimEnd('?') == "string") { + @:.WhereIF(!string.IsNullOrWhiteSpace(input.@(column.PropertyName)), u => u.@(column.PropertyName) == input.@(column.PropertyName)) + } else if (column.NetType.TrimEnd('?') == "int" || column.NetType.TrimEnd('?') == "long") { + @:.WhereIF(input.@(column.PropertyName) != null, u => u.@(column.PropertyName) @(column.QueryType) input.@(column.PropertyName)) + } else if (column.NetType.TrimEnd('?').EndsWith("Enum")) { + @:.WhereIF(input.@(column.PropertyName).HasValue, u => u.@(column.PropertyName) == input.@(column.PropertyName)) + } else if (column.NetType.TrimEnd('?') == "DateTime" && column.QueryType == "~") { + @:.WhereIF(input.@(column.PropertyName)Range?.Length == 2, u => u.@(column.PropertyName) >= input.@(column.PropertyName)Range[0] && u.@(column.PropertyName) <= input.@(column.PropertyName)Range[1]) + } else if (column.NetType.TrimEnd('?').EndsWith("bool")) { + @:.WhereIF(input.@(column.PropertyName).HasValue, u => u.@(column.PropertyName) == input.@(column.PropertyName)) + } + + } + // 联表 + if (Model.HasJoinTable) { + foreach (var column in Model.TableField.Where(u => u.EffectType == "ForeignKey" || u.EffectType == "ApiTreeSelector")){ + joinTableName += ", " + column.LowerPropertyNameTrimEndId; + @:.LeftJoin<@column.FkEntityName>((@joinTableName) => u.@(column.PropertyName) == @(column.LowerPropertyNameTrimEndId).@(column.FkLinkColumnName)) + } + // 查询列表 + @:.Select((@joinTableName) => new @(Model.ClassName)Output + @:{ + foreach (var column in Model.TableField) { + if(column.NetType.TrimEnd('?').EndsWith("Enum")) { + @:@(column.PropertyName) = (@(column.NetType))u.@(column.PropertyName), + }else{ + @:@(column.PropertyName) = u.@(column.PropertyName), + } + if (column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector") { + @:@(column.ExtendedPropertyName) = @column.GetDisplayColumn(column.LowerPropertyNameTrimEndId), + } + } + @:}); + } else { + // 无联表 + @:.Select<@(Model.ClassName)Output>(); + } + } + return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取@(Model.BusName)详情 ℹ️ + /// + /// + /// + [DisplayName("获取@(Model.BusName)详情")] + [ApiDescriptionSettings(Name = "Detail"), HttpGet] + public async Task<@(Model.ClassName)> Detail([FromQuery] QueryById@(Model.ClassName)Input input) + { + return await _@(Model.LowerClassName)Rep.GetFirstAsync(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}")); + } + + /// + /// 增加@(Model.BusName) ➕ + /// + /// + /// + [DisplayName("增加@(Model.BusName)")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(Add@(Model.ClassName)Input input) + { + var entity = input.Adapt<@(Model.ClassName)>(); + @foreach (var config in Model.TableUniqueConfigList) { + @:if (await _@(Model.LowerClassName)Rep.IsAnyAsync(u => @(string.Join(" && ", @config.Columns.Select(x => $"u.{x} != null && u.{x} == input.{x}"))))) throw Oops.Oh("@(config.Message)已存在"); + } + return await _@(Model.LowerClassName)Rep.InsertAsync(entity) ? entity.Id : 0; + } + + /// + /// 更新@(Model.BusName) ✏️ + /// + /// + /// + [DisplayName("更新@(Model.BusName)")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(Update@(Model.ClassName)Input input) + { + @{ + var primaryKeyWhere = Model.PrimaryKeysFormat(" && ", "u.{0} != input.{0}"); + foreach (var config in Model.TableUniqueConfigList) { + @:if (await _@(Model.LowerClassName)Rep.IsAnyAsync(u => @primaryKeyWhere && @config.Format(" && ", "u.{0} != null && u.{0} == input.{0}"))) throw Oops.Oh("@(config.Message)已存在"); + } + } + var entity = input.Adapt<@(Model.ClassName)>(); + await _@(Model.LowerClassName)Rep.AsUpdateable(entity) + @if (Model.IgnoreUpdateFieldList.Count > 0) { + @:.IgnoreColumns(u => new { + foreach (var column in Model.IgnoreUpdateFieldList) { + @:u.@(column.PropertyName), + } + @:}) + } + .ExecuteCommandAsync(); + } + + /// + /// 删除@(Model.BusName) ❌ + /// + /// + /// + [DisplayName("删除@(Model.BusName)")] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task Delete(Delete@(Model.ClassName)Input input) + { + var entity = await _@(Model.LowerClassName)Rep.GetFirstAsync(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}")) ?? throw Oops.Oh(ErrorCodeEnum.D1002); +@{ + @if(Model.TableField.Where(x => x.ColumnName == "IsDelete").Any()) + { + @:await _@(Model.LowerClassName)Rep.FakeDeleteAsync(entity); //假删除 + } + else{ + @:await _@(Model.LowerClassName)Rep.DeleteAsync(entity); //真删除 + } +} + } + + /// + /// 批量删除@(Model.BusName) ❌ + /// + /// + /// + [DisplayName("批量删除@(Model.BusName)")] + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + public async Task BatchDelete([Required(ErrorMessage = "主键列表不能为空")]List input) + { + var exp = Expressionable.Create<@(Model.ClassName)>(); + foreach (var row in input) exp = exp.Or(it => @Model.PrimaryKeysFormat(" && ", "it.{0} == row.{0}")); + var list = await _@(Model.LowerClassName)Rep.AsQueryable().Where(exp.ToExpression()).ToListAsync(); +@{ + @if(Model.TableField.Where(x => x.ColumnName == "IsDelete").Any()) + { + @:return await _@(Model.LowerClassName)Rep.FakeDeleteAsync(list); //假删除 + } + else{ + @:return await _@(Model.LowerClassName)Rep.Context.Deleteable(list).ExecuteCommandAsync(); //真删除--返回受影响的行数 + } +} + } + @if (Model.HasSetStatus) { + @: + @:/// + @:/// 设置@(Model.BusName)状态 🚫 + @:/// + @:/// + @:/// + @:[DisplayName("设置@(Model.BusName)状态")] + @:[ApiDescriptionSettings(Name = "SetStatus"), HttpPost] + @:public async Task Set@(Model.ClassName)Status(Set@(Model.ClassName)StatusInput input) + @:{ + @:await _@(Model.LowerClassName)Rep.AsUpdateable().SetColumns(u => u.Status, input.Status).Where(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}")).ExecuteCommandAsync(); + @:} + } + @foreach (var column in Model.UploadFieldList) { + @: + @:/// + @:/// 上传@(column.ColumnComment) ⬆️ + @:/// + @:/// + @:/// + @:[DisplayName("上传@(column.ColumnComment)")] + @:[ApiDescriptionSettings(Name = "Upload@(column.PropertyName)"), HttpPost] + @:public async Task Upload@(column.PropertyName)([Required] IFormFile file) + @:{ + @:return await _sysFileService.UploadFile(new UploadFileInput { File = file },"upload/@(Model.ClassName)/@(column.PropertyName)"); + @:} + } + @if (Model.DropdownFieldList.Count > 0) { + @: + @:/// + @:/// 获取下拉列表数据 🔖 + @:/// + @:/// + @:[DisplayName("获取下拉列表数据")] + @:[ApiDescriptionSettings(Name = "DropdownData"), HttpPost] + @:public async Task> DropdownData(DropdownData@(Model.ClassName)Input input) + @:{ + foreach (var column in Model.DropdownFieldList) { + @:var @(column.LowerPropertyName)Data = await _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>() + if (column.EffectType != "ApiTreeSelector") { + @:.InnerJoinIF<@Model.ClassName>(input.FromPage, (u, r) => u.@(column.FkLinkColumnName) == r.@(column.PropertyName)) + } + if (column.EffectType != "ApiTreeSelector") { + @:.Select(u => new { + @:Value = u.@(column.FkLinkColumnName), + @:Label = @column.GetDisplayColumn("u") + @:}).ToListAsync(); + } else { + @:.Select(u => new Tree@(column.PropertyNameTrimEndId)Output { + @:Value = u.@(column.FkLinkColumnName), + @:Label = @column.GetDisplayColumn("u") + @:}, true).ToTreeAsync(u => u.Children, u => u.@(column.PidColumn), @(column.WhetherRequired == "Y" ? "0" : "null")); + } + } + @:return new Dictionary + @:{ + foreach (var column in Model.DropdownFieldList) { + @:{ "@(column.LowerPropertyName)", @(column.LowerPropertyName)Data }, + } + @:}; + @:} + } + @if (Model.ImportFieldList.Count > 0) { + @: + @:/// + @:/// 导出@(Model.BusName)记录 🔖 + @:/// + @:/// + @:/// + @:[DisplayName("导出@(Model.BusName)记录")] + @:[ApiDescriptionSettings(Name = "Export"), HttpPost, NonUnify] + @:public async Task Export(Page@(Model.ClassName)Input input) + @:{ + @:var list = (await Page(input)).Items?.Adapt>() ?? new(); + @:if (input.SelectKeyList?.Count > 0) list = list.Where(x => input.SelectKeyList.Contains(x.@(Model.PrimaryKeyFieldList.First().PropertyName))).ToList(); + var dictFields = Model.TableField.Where(x => x.WhetherImport == "Y" && x.EffectType == "DictSelector") ?? default; + foreach (var column in dictFields) { + @:var @(column.LowerPropertyName)DictMap = _sysDictTypeService.GetDataList(new GetDataDictTypeInput { Code = "@(column.DictTypeCode)" }).Result.ToDictionary(x => x.Value, x => x.Label); + } + if (dictFields.Count() > 0) { + @:list.ForEach(e => { + foreach (var column in dictFields) { + @:e.@(column.ExtendedPropertyName) = @(column.LowerPropertyName)DictMap.GetValueOrDefault(e.@(column.PropertyName) ?? "", e.@(column.PropertyName)); + } + @:}); + } + @:return ExcelHelper.ExportTemplate(list, "@(Model.BusName)导出记录"); + @:} + @: + @:/// + @:/// 下载@(Model.BusName)数据导入模板 ⬇️ + @:/// + @:/// + @:[DisplayName("下载@(Model.BusName)数据导入模板")] + @:[ApiDescriptionSettings(Name = "Import"), HttpGet, NonUnify] + @:public IActionResult DownloadTemplate() + @:{ + var fieldsList = Model.ImportFieldList.Where(u => u.EffectType == "ForeignKey" || u.EffectType == "ApiTreeSelector").ToList(); + if (fieldsList.Any()) { + @:return ExcelHelper.ExportTemplate(new List(), "@(Model.BusName)导入模板", (_, info) => + @:{ + foreach (var column in fieldsList) { + var columnList = column.FkDisplayColumnList.Select(n => $"{{u.{n}}}").ToList(); + @:if (nameof(Export@(Model.ClassName)Output.@column.ExtendedPropertyName) == info.Name) return _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>().Select(u => $"@(string.Join("-", columnList))").Distinct().ToList(); + } + @:return null; + @:}); + } else { + @:return ExcelHelper.ExportTemplate(new List(), "@(Model.BusName)导入模板"); + } + @:} + @: + @:private static readonly object _@(Model.LowerClassName)ImportLock = new object(); + @:/// + @:/// 导入@(Model.BusName)记录 💾 + @:/// + @:/// + @:[DisplayName("导入@(Model.BusName)记录")] + @:[ApiDescriptionSettings(Name = "Import"), HttpPost, NonUnify, UnitOfWork] + @:public IActionResult ImportData([Required] IFormFile file) + @:{ + @:lock (_@(Model.LowerClassName)ImportLock) + @:{ + var dictTableField = Model.TableField.Where(x => x.WhetherImport == "Y" && x.EffectType == "DictSelector") ?? default; + foreach (var column in dictTableField){ + @:var @(column.LowerPropertyName)DictMap = _sysDictTypeService.GetDataList(new GetDataDictTypeInput { Code = "@(column.DictTypeCode)" }).Result.ToDictionary(x => x.Label!, x => x.Value); + } + + @:var stream = ExcelHelper.ImportData(file, (list, markerErrorAction) => + @:{ + @:_sqlSugarClient.Utilities.PageEach(list, 2048, pageItems => + @:{ + foreach (var column in Model.ImportFieldList.Where(u => u.EffectType == "ForeignKey" || u.EffectType == "ApiTreeSelector")) { + @:// 链接 @(column.ColumnComment) + @:var @(column.LowerPropertyName)LabelList = pageItems.Where(x => x.@column.ExtendedPropertyName != null).Select(x => x.@column.ExtendedPropertyName).Distinct().ToList(); + @:if (@(column.LowerPropertyName)LabelList.Any()) { + var columnList = column.FkDisplayColumnList.Select(n => $"{{u.{n}}}").ToList(); + @:var @(column.LowerPropertyName)LinkMap = _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>().Where(u => @(column.LowerPropertyName)LabelList.Contains($"@(string.Join("-", columnList))")).ToList().ToDictionary(u => $"@(string.Join("-", columnList))", u => u.@(column.FkLinkColumnName) as @(column.NetType.TrimEnd('?') == "long" ? "long?": column.NetType)); + @:pageItems.ForEach(e => { + @:e.@(column.PropertyName) = @(column.LowerPropertyName)LinkMap.GetValueOrDefault(e.@column.ExtendedPropertyName ?? ""); + @:if (e.@(column.PropertyName) == null) e.Error = "@(column.ColumnComment)链接失败"; + @:}); + @:} + } + + if (dictTableField.Any()) { + @: + @:// 映射字典值 + @:foreach(var item in pageItems) { + @:System.Text.StringBuilder sbError = new System.Text.StringBuilder(); + foreach (var column in dictTableField) { + @:if (!string.IsNullOrWhiteSpace(item.@(column.ExtendedPropertyName))) { + @:item.@(column.PropertyName) = @(column.LowerPropertyName)DictMap.GetValueOrDefault(item.@(column.ExtendedPropertyName)); + @:if (item.@(column.PropertyName) == null) sbError.AppendLine("@(column.ColumnComment)字典映射失败"); + @:} + } + @:item.Error = sbError.ToString(); + @:} + } + + @: + @:// 校验并过滤必填基本类型为null的字段 + @:var rows = pageItems.Where(x => { + @:if (!string.IsNullOrWhiteSpace(x.Error)) return false; + foreach (var column in Model.ImportFieldList.Where(x => x.WhetherRequired == "Y" && Regex.IsMatch(x.NetType, "(int|long|double|float|bool|Enum[?]?)"))){ + @:if (x.@(column.PropertyName) == null){ + @:x.Error = "@(column.ColumnComment)不能为空"; + @:return false; + @:} + } + @:return true; + @:}).Adapt>(); + + @{var updateFields = new List();} + @: + @:var storageable = _@(Model.LowerClassName)Rep.Context.Storageable(rows) + foreach (var column in Model.ImportFieldList){ + if (column.WhetherImport == "Y"){ + updateFields.Add(column.PropertyName); + } + if (column.WhetherRequired == "Y"){ + if(column.NetType.TrimEnd('?') == "string"){ + @:.SplitError(it => string.IsNullOrWhiteSpace(it.Item.@(column.PropertyName)), "@(column.ColumnComment)不能为空") + } else if(column.NetType.EndsWith('?') == true){ + @:.SplitError(it => it.Item.@(column.PropertyName) == null, "@(column.ColumnComment)不能为空") + }} + if (column.NetType?.TrimEnd('?') == "string" && column.ColumnLength > 0){ + @:.SplitError(it => it.Item.@(column.PropertyName)?.Length > @(column.ColumnLength), "@(column.ColumnComment)长度不能超过@(column.ColumnLength)个字符") + }} + if(Model.TableUniqueConfigList.Count>0){ + foreach (var config in Model.TableUniqueConfigList) { + @:.WhereColumns(it => new { @config.Format(", ", "it.{0}") }) + } + @:.SplitInsert(it=> !it.Any()) + @:.SplitUpdate(it=> it.Any()) + }else{ + @:.SplitInsert(_=> true) // 没有设置唯一键代表插入所有数据 + } + @:.ToStorage(); + @: + @:storageable.AsInsertable.ExecuteCommand();// 不存在插入 + @:storageable.AsUpdateable.UpdateColumns(it => new + @:{ + @foreach (var field in updateFields) + { + @: it.@(field), + } + @:}).ExecuteCommand();// 存在更新 + @: + @:// 标记错误信息 + @:markerErrorAction.Invoke(storageable, pageItems, rows); + @:}); + @:}); + @: + @:return stream; + @:} + @:} + } +} diff --git a/Evergreen.Health.Api/wwwroot/template/api.ts.vm b/Evergreen.Health.Api/wwwroot/template/api.ts.vm new file mode 100644 index 0000000..3419568 --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/api.ts.vm @@ -0,0 +1,59 @@ +import {useBaseApi} from '/@@/api/base'; + +// @(Model.BusName)接口服务 +export const use@(Model.ClassName)Api = () => { + const baseApi = useBaseApi("@(Model.LowerClassName)"); + return { + // 分页查询@(Model.BusName) + page: baseApi.page, + // 查看@(Model.BusName)详细 + detail: baseApi.detail, + // 新增@(Model.BusName) + add: baseApi.add, + // 更新@(Model.BusName) + update: baseApi.update, + @if (Model.HasSetStatus) { + @:// 设置@(Model.BusName)状态 + @:setStatus: baseApi.setStatus, + } + // 删除@(Model.BusName) + delete: baseApi.delete, + // 批量删除@(Model.BusName) + batchDelete: baseApi.batchDelete, + @if (Model.ImportFieldList.Count > 0) { + @:// 导出@(Model.BusName)数据 + @:exportData: baseApi.exportData, + @:// 导入@(Model.BusName)数据 + @:importData: baseApi.importData, + @:// 下载@(Model.BusName)数据导入模板 + @:downloadTemplate: baseApi.downloadTemplate, + } + @if (Model.DropdownFieldList.Count > 0) { + @:// 获取下拉列表数据 + @:getDropdownData: (fromPage: Boolean = false, cancel: boolean = false) => baseApi.dropdownData({ fromPage }, cancel), + } + @foreach (var column in Model.UploadFieldList) { + @:// 上传@(column.ColumnComment) + @:upload@(column.PropertyName): (params: any, cancel: boolean = false) => baseApi.uploadFile(params, 'upload@(column.PropertyName)', cancel), + } + } +} + +// @(Model.BusName)实体 +export interface @(Model.ClassName) { +@{ +var typeMap = new Dictionary() { + { "bool", "boolean" }, + { "int", "number" }, + { "long", "number" }, + { "double", "number" }, + { "float", "number" }, + { "decimal", "number" }, + { "byte", "number" } +}; +foreach (var column in Model.TableField) { + @:// @(column.ColumnComment) + @:@(column.LowerPropertyName)@(column.WhetherRequired == "Y" ? "?" : ""): @(Regex.IsMatch("@(column.DataType.Trim('?'))", ".*?Enum") ? "number" : typeMap.GetValueOrDefault(column.DataType.Trim('?').ToLower(), "string")); +} +} +} \ No newline at end of file diff --git a/Evergreen.Health.Api/wwwroot/template/data.data.ts.vm b/Evergreen.Health.Api/wwwroot/template/data.data.ts.vm new file mode 100644 index 0000000..3308776 --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/data.data.ts.vm @@ -0,0 +1,159 @@ +import { h } from 'vue'; +import { BasicColumn, FormSchema } from '/@@/components/Table'; +@foreach (var column in Model.TableField){ +if(column.EffectType == "Upload" || column.EffectType == "Upload_SingleFile"){ +@:import { uploadFile } from '/@@/api/sys/admin'; +}else if(column.EffectType == "ForeignKey"){ +@:import { get@(column.FkEntityName)Dropdown } from '/@@/api/main/@(Model.ClassName)'; +}else if(column.EffectType == "DictSelector"){ +@:import { getDataList } from '/@@/api/sys/admin'; +}else if(column.EffectType == "ApiTreeSelector"){ +@:import { get@(column.FkEntityName)Tree } from '/@@/api/main/@(Model.ClassName)'; +}else if(column.EffectType == "ConstSelector"){ +@:import { codeToName, getSelector } from '/@@/utils/helper/constSelectorHelper'; +}else if(column.EffectType == "Switch"){ +@:import { Switch } from 'ant-design-vue'; +} +} +export const columns: BasicColumn[] = [ + @foreach (var column in Model.TableField){ + if(column.WhetherTable == "Y"){ + @:{ + @:title: '@column.ColumnComment', + @:dataIndex: '@column.LowerPropertyName', + @:sorter: true, +if(column.EffectType == "Upload" || column.EffectType == "Upload_SingleFile"){ + @:slots: { customRender: '@(column.LowerPropertyName)' }, +}else if(column.EffectType == "ForeignKey"){ + @:customRender: ({ record }) => { + @:return record.fk@(column.PropertyName).@(column.LowerFkDisplayColumnsList?.First()); + @:}, +}else if(column.EffectType == "Switch"){ + @:customRender: ({ record }) => { + @:return h(@(column.EffectType), { checked: record.@(column.LowerPropertyName) }); + @:}, +}else if(column.EffectType == "ConstSelector"){ + @:customRender: ({ record }) => { + @:return codeToName(record.@(column.LowerPropertyName), '@(column.DictTypeCode)'); + @:}, +} + @:}, + } + + } +]; + +export const searchFormSchema: FormSchema[] = [ +@foreach (var column in Model.WhetherQueryList){ + @:{ + @:field: '@column.LowerPropertyName', + @:label: '@column.ColumnComment', + @:colProps: { span: 8 }, +if(column.EffectType == "ForeignKey"){ + @:component: 'ApiSelect', + @:componentProps: { + @:api: get@(column.FkEntityName)Dropdown, + @:labelField: 'label', + @:valueField: 'value', + @:}, +}else if(column.EffectType == "DictSelector"){ + @:component: 'ApiSelect', + @:componentProps: { + @:api: getDataList, + @:params: '@(column.DictTypeCode)', + @:fieldNames: { + @:label: 'label', + @:value: 'value', + @:}, + @:}, +}else if(column.EffectType == "ConstSelector"){ + @:component: 'Select', + @:componentProps: { + @:options: getSelector('@(column.DictTypeCode)'), + @:fieldNames: { + @:label: 'name', + @:value: 'code', + @:}, + @:}, +}else if(column.EffectType == "ApiTreeSelector"){ + @:component: '@(column.EffectType)', + @:componentProps: { + @:api: get@(column.FkEntityName)Tree, + @:}, +} +else if(column.NetType?.TrimEnd('?') == "DateTime" && column.QueryType == "~"){ + @:component: 'RangePicker', + @:componentProps: { + @: valueFormat:"YYYY-MM-DD" + @:}, +} else { + @:component: 'Input', +} + + @:}, +} +]; + +export const formSchema: FormSchema[] = [ + @foreach (var column in Model.TableField){ + @:{ + @:label: '@column.ColumnComment', + @:field: '@column.LowerPropertyName', +if(column.EffectType == "ForeignKey"){ + @:component: 'ApiSelect', + @:componentProps: { + @:api: get@(column.FkEntityName)Dropdown, + @:labelField: 'label', + @:valueField: 'value', + @:}, +}else if(column.EffectType == "DictSelector"){ + @:component: 'ApiSelect', + @:componentProps: { + @:api: getDataList, + @:params: '@(column.DictTypeCode)', + @:fieldNames: { + @:label: 'label', + @:value: 'value', + @:}, + @:}, +}else if(column.EffectType == "ConstSelector"){ + @:component: 'Select', + @:componentProps: { + @:options: getSelector('@(column.DictTypeCode)'), + @:fieldNames: { + @:label: 'name', + @:value: 'code', + @:}, + @:}, +}else if(column.EffectType == "ApiTreeSelector"){ + @:component: '@(column.EffectType)', + @:componentProps: { + @:api: get@(column.FkEntityName)Tree, + @:}, +}else if(column.EffectType == "Switch"){ + @:component: '@(column.EffectType)', + @:componentProps: { + @:checkedChildren: '是', + @:unCheckedChildren: '否', + @:}, +}else{ + @:component: '@column.EffectType', +} + if(column.WhetherRequired == "Y"){ + @:required: true, + }else{ + @:required: false, + } + if(column.EffectType == "Upload" || column.EffectType == "Upload_SingleFile"){ + @:componentProps: { + @:maxNumber: 1, + @:api: uploadFile, + @:}, + } + if(column.LowerPropertyName == "id"){ + @:show: false, + } + @:colProps: { span: 12 }, + @:}, + } +]; diff --git a/Evergreen.Health.Api/wwwroot/template/dataModal.vue.vm b/Evergreen.Health.Api/wwwroot/template/dataModal.vue.vm new file mode 100644 index 0000000..734b41d --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/dataModal.vue.vm @@ -0,0 +1,71 @@ + + diff --git a/Evergreen.Health.Api/wwwroot/template/editDialog.vue.vm b/Evergreen.Health.Api/wwwroot/template/editDialog.vue.vm new file mode 100644 index 0000000..8eddc15 --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/editDialog.vue.vm @@ -0,0 +1,198 @@ + + + \ No newline at end of file diff --git a/Evergreen.Health.Api/wwwroot/template/index.vue.vm b/Evergreen.Health.Api/wwwroot/template/index.vue.vm new file mode 100644 index 0000000..e5da3c0 --- /dev/null +++ b/Evergreen.Health.Api/wwwroot/template/index.vue.vm @@ -0,0 +1,326 @@ + + + \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/APIJSON.json b/Evergreen.Health.Application/Configuration/APIJSON.json new file mode 100644 index 0000000..5baf231 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/APIJSON.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "APIJSON": { + "Roles": [ + { + "RoleName": "Role1", // 权限名称 唯一 + "Select": { // 查询 + "Table": [ "*" ], // 可操作的表 + "Column": [ "*" ], // 可操作的字段 + "Filter": [] + }, + "Insert": { // 添加 + "Table": [ "table1", "table2", "table3" ], + "Column": [ "*", "*", "tb.*" ] + }, + "Update": { // 修改 + "Table": [ "table1", "table2", "table3" ], + "Column": [ "*", "tb.*", "tb.*" ] + }, + "Delete": { // 删除 + "Table": [ "table1", "table2", "table3" ] + } + }, + { + "RoleName": "Role2", + "Select": { + "Table": [ "table1" ], + "Column": [ "tb.*" ] + } + } + ] + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Alipay.json b/Evergreen.Health.Application/Configuration/Alipay.json new file mode 100644 index 0000000..5aa74e1 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Alipay.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 支付宝支付配置,文档地址:https://openhome.alipay.com/develop/sandbox/app + "Alipay": { + "ServerUrl": "https://openapi-sandbox.dl.alipaydev.com/gateway.do", // 支付宝网关地址 + "WebsocketUrl": "openchannel-sandbox.dl.alipaydev.com", // websocket服务地址 + //"AuthUrl": "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm", // 正式环境授权回调地址 + "AuthUrl": "https://openauth-sandbox.dl.alipaydev.com/oauth2/publicAppAuthorize.htm", // 沙箱环境授权回调地址 + "AppAuthUrl": "http://xxxxxxxxxx", // 应用授权回调地址 + "NotifyUrl": "http://xxxxxxxxx/api/sysAlipay/Notify", // 应用网关地址 + "RootCertPath": "AlipayCrt/alipayRootCert.crt", // 支付宝根证书存放路径 + "AccountList": [ + { + "Name": "sandbox 默认应用", + "AppId": "xxxxxxxxxxxxxx", + "SignType": "RSA2", + "PrivateKey": "xxxxxxxxxxxxxxxxx", + "EncryptKey": "xxxxxxxxxxxxxxxxxxxx", + "AppCertPath": "AlipayCrt/appPublicCert.crt", // 应用公钥证书存放路径 + "AlipayPublicCertPath": "AlipayCrt/alipayPublicCert.crt" // 支付宝公钥证书存放路径 + }, + { + "Name": "sandbox 默认应用2", + "AppId": "xxxxxxxxxxxxxx", + "SignType": "RSA2", + "PrivateKey": "xxxxxxxxxxxxxxxxx", + "EncryptKey": "xxxxxxxxxxxxxxxxxxxx", + "AppCertPath": "AlipayCrt/appPublicCert.crt", // 应用公钥证书存放路径 + "AlipayPublicCertPath": "AlipayCrt/alipayPublicCert.crt" // 支付宝公钥证书存放路径 + } + ] + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/App.json b/Evergreen.Health.Application/Configuration/App.json new file mode 100644 index 0000000..814355a --- /dev/null +++ b/Evergreen.Health.Application/Configuration/App.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Urls": "http://*:5005", // 默认端口 + "AllowedHosts": "*", // 允许所有地址 + + "AppSettings": { + "InjectSpecificationDocument": true, // 生产环境是否开启Swagger + "ExternalAssemblies": [ "plugins" ], // 插件目录 + "VirtualPath": "" // 二级虚拟目录 + }, + "DynamicApiControllerSettings": { + //"DefaultRoutePrefix": "api", // 默认路由前缀 + "CamelCaseSeparator": "", // 驼峰命名分隔符 + "SplitCamelCase": false, // 切割骆驼(驼峰)/帕斯卡命名 + "LowercaseRoute": false, // 小写路由格式 + "AsLowerCamelCase": true, // 小驼峰命名(首字母小写) + "KeepVerb": false, // 保留动作方法请求谓词 + "KeepName": false // 保持原有名称不处理 + }, + "FriendlyExceptionSettings": { + "DefaultErrorMessage": "系统异常,请联系管理员", + "ThrowBah": true, // 是否将 Oops.Oh 默认抛出为业务异常 + "LogError": false // 是否输出异常日志 + }, + // 静态资源处理方式(允许这些文件被访问),包含".*": "application/octet-stream"允许访问所有静态资源 + "StaticContentTypeMappings": { + ".dll": "application/octet-stream", + ".exe": "application/octet-stream", + ".pdb": "application/octet-stream", + ".so": "application/octet-stream" + }, + "LocalizationSettings": { + "SupportedCultures": [ "zh-CN", "en" ], // 语言列表 + "DefaultCulture": "zh-CN", // 默认语言 + "DateTimeFormatCulture": "zh-CN" // 固定时间区域为特定时区(多语言) + }, + "CorsAccessorSettings": { + //"PolicyName": "App.Cors.Policy", // 跨域策略名称 + //"WithOrigins": [ "http://localhost:5005", "https://gitee.com" ], // 允许的跨域地址 + "WithExposedHeaders": [ "Content-Disposition", "X-Pagination", "access-token", "x-access-token", "Access-Control-Expose-Headersx-access-token" ], // 如果前端不代理且是axios请求 + "SignalRSupport": true // 启用 SignalR 跨域支持 + }, + // 定时任务/作业调度 + "JobSchedule": { + "Enabled": true // 是否开启 + }, + // 雪花Id + "SnowId": { + "WorkerId": 1, // 雪花Id机器码,多服务器时全局唯一 + "WorkerIdBitLength": 6, // 机器码位长 默认值6,取值范围 [1, 19] + "SeqBitLength": 6, // 序列数位长 默认值6,取值范围 [3, 21](建议不小于4,值越大性能越高、Id位数也更长) + "WorkerPrefix": "adminnet_" // 缓存前缀 + }, + // 密码策略 + "Cryptogram": { + "StrongPassword": false, // 是否开启密码强度验证 + "PasswordStrengthValidation": "(?=^.{6,16}$)(?=.*\\d)(?=.*\\W+)(?=.*[A-Z])(?=.*[a-z])(?!.*\\n).*$", // 密码强度验证正则表达式,必须须包含大小写字母、数字和特殊字符的组合,长度在6-16之间 + "PasswordStrengthValidationMsg": "密码必须包含大小写字母、数字和特殊字符的组合,长度在6-16之间", // 密码强度验证消息提示 + "CryptoType": "SM2", // 密码加密算法:MD5、SM2、SM4 + // 新业务系统记得改密匙,通过接口(http://localhost:5005/api/sysCommon/smKeyPair)获取。记得同步修改前端公钥配置:VITE_SM_PUBLIC_KEY + "PublicKey": "0484C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312", // 公钥 + "PrivateKey": "8EDB615B1D48B8BE188FC0F18EC08A41DF50EA731FA28BF409E6552809E3A111" // 私钥 + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/CDConfig.json b/Evergreen.Health.Application/Configuration/CDConfig.json new file mode 100644 index 0000000..940cec8 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/CDConfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "CDConfig": { + "Enabled": true, // 启用持续部署 + "Owner": "zuohuaijun", // gitee用户名 + "Repo": "Admin.NET", // 仓库名 + "Branch": "v2", // 分支名 + "AccessToken": "xxxxxxxxxxxxxxxxxxxxxxxxx", // gitee用户授权码 + "UpdateInterval": 0, // 最小更新间隔(分钟),0不限制 + "BackupCount": 10, // 备份文件保留数量,0不限制 + "BackendOutput": "D:\\Admin.NET", // 后端输出目录 + "Publish": { // 后端发布选项 + "Configuration": "Release", // 发布运行版本 + "TargetFramework": "net8.0", // 发布.NET版本 + "RuntimeIdentifier": "linux-x64" // 运行平台 + }, + "ExcludeFiles": [ "Configuration\\*.json" ] // 排除文件 + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Cache.json b/Evergreen.Health.Application/Configuration/Cache.json new file mode 100644 index 0000000..aeaa2e0 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Cache.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 缓存配置 + "Cache": { + "Prefix": "adminnet_", // 全局缓存前缀 + "CacheType": "Memory", // Memory、Redis + "Redis": { + "Configuration": "server=127.0.0.1:6379;password=;db=5;", // Redis连接字符串 + "Prefix": "adminnet_", // Redis前缀(目前没用) + "MaxMessageSize": "1048576", // 最大消息大小 默认1024 * 1024 + "AutoDetect": false // 自动检测集群节点 阿里云的Redis分布式集群使用代理模式,需要设置为false关闭自动检测;如果不用代理地址,就配置多个节点地址并打开自动检测 + } + }, + // 集群配置 + "Cluster": { + "Enabled": false, // 启用集群:前提开启Redis缓存模式 + "ServerId": "adminnet", // 服务器标识 + "ServerIp": "", // 服务器IP + "SignalR": { + "RedisConfiguration": "127.0.0.1:6379,ssl=false,password=,defaultDatabase=5", + "ChannelPrefix": "signalrPrefix_" + }, + "DataProtecteKey": "AdminNet:DataProtection-Keys", + "IsSentinel": false, // 是否哨兵模式 + "SentinelConfig": { + "DefaultDb": "4", + "EndPoints": [ // 哨兵端口 + // "10.10.0.124:26380" + ], + "MainPrefix": "adminNet:", + "Password": "123456", + "SentinelPassword": "adminNet", + "ServiceName": "adminNet", + "SignalRChannelPrefix": "signalR:" + } + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Captcha.json b/Evergreen.Health.Application/Configuration/Captcha.json new file mode 100644 index 0000000..fcb1058 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Captcha.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // Lazy.Captcha.Core 组件详细文档(https://api.gitee.com/pojianbing/lazy-captcha/) + "CaptchaOptions": { + "CaptchaType": 10, // 验证码类型0、1、2、3、4、5、6、7、8、9、10、11 + "CodeLength": 1, // 验证码长度, 要放在CaptchaType设置后 当类型为算术表达式时,长度代表操作的个数, 例如2 + "ExpirySeconds": 60, // 验证码过期秒数 + "IgnoreCase": true, // 比较时是否忽略大小写 + "StoreageKeyPrefix": "", // 存储键前缀 + "ImageOption": { + "Animation": true, // 是否启用动画 + "FontSize": 36, // 字体大小 + "Width": 150, // 验证码宽度 + "Height": 50, // 验证码高度 + "BubbleMinRadius": 5, // 气泡最小半径 + "BubbleMaxRadius": 10, // 气泡最大半径 + "BubbleCount": 3, // 气泡数量 + "BubbleThickness": 1.0, // 气泡边沿厚度 + "InterferenceLineCount": 3, // 干扰线数量 + "FontFamily": "kaiti", // 包含actionj,epilog,fresnel,headache,lexo,prefix,progbot,ransom,robot,scandal,kaiti + "FrameDelay": 300, // 每帧延迟,Animation=true时有效, 默认300 + "BackgroundColor": "#ffffff", // 格式: rgb, rgba, rrggbb, or rrggbbaa format to match web syntax, 默认#fff + "ForegroundColors": "", // 颜色格式同BackgroundColor,多个颜色逗号分割,随机选取。不填,空值,则使用默认颜色集 + "Quality": 100, // 图片质量(质量越高图片越大,gif调整无效可能会更大) + "TextBold": true // 粗体 + } + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/CodeGen.json b/Evergreen.Health.Application/Configuration/CodeGen.json new file mode 100644 index 0000000..9f02228 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/CodeGen.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 代码生成配置项-程序集名称集合 + "CodeGen": { + "EntityAssemblyNames": [ "Admin.NET.Core", "Evergreen.Health.Application" ], // 实体所在程序集(自行添加新建程序集名称) + "BaseEntityNames": [ "EntityBaseId", "EntityBase", "EntityBaseDel", "EntityBaseOrg", "EntityBaseOrgDel", "EntityBaseTenant", "EntityBaseTenantDel", "EntityBaseTenantId", "EntityBaseTenantOrg", "EntityBaseTenantOrgDel" ], // 实体基类名称 + "FrontRootPath": "Web", // 前端项目根目录 + "BackendApplicationNamespaces": [ "Evergreen.Health.Application" ] // 后端生成到的项目(自行添加新建命名空间) + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Database.json b/Evergreen.Health.Application/Configuration/Database.json new file mode 100644 index 0000000..82254f8 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Database.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 详细数据库配置见SqlSugar官网(第一个为默认库),极力推荐 PostgreSQL 数据库 + // 数据库连接字符串参考地址:https://www.connectionstrings.com/ + "DbConnection": { + "EnableConsoleSql": false, // 启用控制台打印SQL + "ConnectionConfigs": [ + { + //"ConfigId": "1300000000001", // 默认库标识-禁止修改 + "DbType": "MySqlConnector", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access、OpenGauss、QuestDB、HG、ClickHouse、GBase、Odbc、Custom + "DbNickName": "系统库", + "ConnectionString": "Server=127.0.0.1;Port=3306;Database=evergreen_health;Uid=root;Pwd=CHANGE_ME;SslMode=None;AllowLoadLocalInfile=true;AllowUserVariables=true;CharSet=utf8mb4;", // 本地开发MySQL:请替换Uid和Pwd + //"ConnectionString": "PORT=5432;DATABASE=xxx;HOST=localhost;PASSWORD=xxx;USER ID=xxx", // PostgreSQL + //"ConnectionString": "Server=localhost;Database=xxx;Uid=xxx;Pwd=xxx;SslMode=None;AllowLoadLocalInfile=true;AllowUserVariables=true;", // MySql, + //"ConnectionString": "User Id=xxx; Password=xxx; Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))", // Oracle + //"ConnectionString": "Server=localhost;Database=xxx;User Id=xxx;Password=xxx;Encrypt=True;TrustServerCertificate=True;", // SqlServer + + //"SlaveConnectionConfigs": [ // 读写分离/主从 + // { + // "HitRate": 10, + // "ConnectionString": "DataSource=./Admin.NET1.db" + // }, + // { + // "HitRate": 10, + // "ConnectionString": "DataSource=./Admin.NET2.db" + // } + //], + "DbSettings": { + "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + "EnableInitView": true, // 启用视图初始化(若实体和视图没有变化建议关闭) + "EnableDiffLog": false, // 启用库表差异日志 + "EnableUnderLine": false, // 启用驼峰转下划线 + "EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密) + }, + "TableSettings": { + "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + }, + "SeedSettings": { + "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭) + "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + } + } + //// 日志独立数据库配置 + //{ + // "ConfigId": "1300000000002", // 日志库标识-禁止修改 + // "DbNickName": "日志库", + // "DbType": "Sqlite", + // "ConnectionString": "DataSource=./Admin.NET.Log.db", // 库连接字符串 + // "DbSettings": { + // "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + // "EnableDiffLog": false, // 启用库表差异日志 + // "EnableUnderLine": false // 启用驼峰转下划线 + // }, + // "TableSettings": { + // "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + // "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + // }, + // "SeedSettings": { + // "EnableInitSeed": false, // 启用种子初始化(若种子没有变化建议关闭) + // "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + // } + //}, + //// 其他数据库配置(可以配置多个) + //{ + // "ConfigId": "test", // 库标识 + // "DbType": "Sqlite", // 库类型 + // "ConnectionString": "DataSource=./Admin.NET.Test.db", // 库连接字符串 + // "DbSettings": { + // "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + // "EnableDiffLog": false, // 启用库表差异日志 + // "EnableUnderLine": false // 启用驼峰转下划线 + // }, + // "TableSettings": { + // "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + // "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + // }, + // "SeedSettings": { + // "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭) + // "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + // } + //} + ] + } +} diff --git a/Evergreen.Health.Application/Configuration/DeepSeek.example b/Evergreen.Health.Application/Configuration/DeepSeek.example new file mode 100644 index 0000000..d9192df --- /dev/null +++ b/Evergreen.Health.Application/Configuration/DeepSeek.example @@ -0,0 +1,9 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "DeepSeekSettings": { + "SourceLang": "zh-cn", + "ApiUrl": "https://api.deepseek.com/v1/chat/completions", + "ApiKey": "你的 API KEY" + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/ElasticSearch.json b/Evergreen.Health.Application/Configuration/ElasticSearch.json new file mode 100644 index 0000000..78f34de --- /dev/null +++ b/Evergreen.Health.Application/Configuration/ElasticSearch.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "ElasticSearch:Logging": { + "Enabled": true, // 启用ES日志 + "AuthType": "None", // ES认证类型,可选 Basic、ApiKey、Base64ApiKey、None + "User": "elastic", // Basic认证的用户名,使用Basic认证类型时必填 + "Password": "123456", // Basic认证的密码,使用Basic认证类型时必填 + "ApiId": "", // 使用ApiKey认证类型时必填 + "ApiKey": "", // 使用ApiKey认证类型时必填 + "Base64ApiKey": "TmtrOEszNEJuQ0NyaWlydGtROFk6SG1RZ0w3YzBTc2lCanJTYlV3aXNzZw==", // 使用Base64ApiKey认证类型时必填 + "Fingerprint": "37:08:6A:C6:06:CC:9A:43:CF:ED:25:A2:1C:A4:69:57:90:31:2C:06:CA:61:56:39:6A:9C:46:11:BD:22:51:DA", // ES使用Https时的证书指纹 + "ServerUris": [ "http://192.168.3.90:9200" ], // 地址 + "DefaultIndex": "adminnet" // 索引 + }, + "ElasticSearch:Business": { + "Enabled": false, // 启用ES日志 + "AuthType": "Basic", // ES认证类型,可选 Basic、ApiKey、Base64ApiKey、None + "User": "admin", // Basic认证的用户名,使用Basic认证类型时必填 + "Password": "123456", // Basic认证的密码,使用Basic认证类型时必填 + "ApiId": "", // 使用ApiKey认证类型时必填 + "ApiKey": "", // 使用ApiKey认证类型时必填 + "Base64ApiKey": "TmtrOEszNEJuQ0NyaWlydGtROFk6SG1RZ0w3YzBTc2lCanJTYlV3aXNzZw==", // 使用Base64ApiKey认证类型时必填 + "Fingerprint": "37:08:6A:C6:06:CC:9A:43:CF:ED:25:A2:1C:A4:69:57:90:31:2C:06:CA:61:56:39:6A:9C:46:11:BD:22:51:DA", // ES使用Https时的证书指纹 + "ServerUris": [ "http://192.168.1.100:9200" ], // 地址 + "DefaultIndex": "adminnet" // 索引 + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Email.json b/Evergreen.Health.Application/Configuration/Email.json new file mode 100644 index 0000000..b06759e --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Email.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 邮箱配置 + "Email": { + "Host": "smtp.163.com", // 主机 + "Port": 465, // 端口 465、994、25 + "EnableSsl": true, // 启用SSL + "DefaultFromEmail": "xxx@163.com", // 默认发件者邮箱 + "DefaultToEmail": "xxx@qq.com", // 默认接收人邮箱 + "UserName": "xxx@163.com", // 邮箱账号 + "Password": "", // 邮箱授权码 + "DefaultFromName": "常青树健康管理系统" // 默认邮件标题 + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Enum.json b/Evergreen.Health.Application/Configuration/Enum.json new file mode 100644 index 0000000..e15deae --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Enum.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 枚举配置(将所有枚举转成字典) + "Enum": { + "EntityAssemblyNames": [ "Admin." ] // 枚举所在程序集(自行添加新建程序集名称) + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/EventBus.json b/Evergreen.Health.Application/Configuration/EventBus.json new file mode 100644 index 0000000..02b5ab2 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/EventBus.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "EventBus": { + // 事件源存储器类型,默认内存存储(Redis则需要配合缓存相关配置) + "EventSourceType": "Memory", // Memory、Redis、RabbitMQ、Kafka + "RabbitMQ": { + "UserName": "adminnet", + "Password": "adminnet++123456", + "HostName": "127.0.0.1", + "Port": 5672 + }, + "Kafka": { + } + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/JWT.json b/Evergreen.Health.Application/Configuration/JWT.json new file mode 100644 index 0000000..72c7dc5 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/JWT.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "JWTSettings": { + "ValidateIssuerSigningKey": true, // 是否验证密钥,bool 类型,默认true + "IssuerSigningKey": "3c1cbc3f546eda35168c3aa3cb91780fbe703f0996c6d123ea96dc85c70bbc0a", // 密钥,string 类型,必须是复杂密钥,长度大于16 + "ValidateIssuer": true, // 是否验证签发方,bool 类型,默认true + "ValidIssuer": "Admin.NET", // 签发方,string 类型 + "ValidateAudience": true, // 是否验证签收方,bool 类型,默认true + "ValidAudience": "Admin.NET", // 签收方,string 类型 + "ValidateLifetime": true, // 是否验证过期时间,bool 类型,默认true,建议true + //"ExpiredTime": 20, // 过期时间,long 类型,单位分钟,默认20分钟,最大支持 13 年 + "ClockSkew": 5, // 过期时间容错值,long 类型,单位秒,默认5秒 + "Algorithm": "HS256", // 加密算法,string 类型,默认 HS256 + "RequireExpirationTime": true // 验证过期时间,设置 false 将永不过期 + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Limit.json b/Evergreen.Health.Application/Configuration/Limit.json new file mode 100644 index 0000000..d23bbca --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Limit.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // IP限流配置 + "IpRateLimiting": { + // 例如:设置每分钟5次访问限流 + // 当False时:每个接口都加入计数,不管你访问哪个接口,只要在一分钟内累计够5次,将禁止访问。 + // 当True 时:当一分钟请求了5次GetData接口,则该接口将在时间段内禁止访问,但是还可以访问PostData()5次,总得来说是每个接口都有5次在这一分钟,互不干扰。 + "EnableEndpointRateLimiting": true, + // 如果StackBlockedRequests设置为false,拒绝的API调用不会添加到调用次数计数器上。比如:如果客户端每秒发出3个请求并且您设置了每秒一个调用的限制, + // 则每分钟或每天计数器等其他限制将仅记录第一个调用,即成功的API调用。如果您希望被拒绝的API调用计入其他时间的显示(分钟,小时等),则必须设置 + "StackBlockedRequests": false, + // 在RealIpHeader使用时,你的Kestrel服务器背后是一个反向代理,如果你的代理服务器使用不同的页眉然后提取客户端IP X-Real-IP使用此选项来设置它。 + "RealIpHeader": "X-Real-IP", + // 将ClientIdHeader被用于提取白名单的客户端ID。如果此标头中存在客户端ID并且与ClientWhitelist中指定的值匹配,则不应用速率限制。 + "ClientIdHeader": "X-ClientId", + // IP白名单:支持Ipv4和Ipv6 + "IpWhitelist": [], + // 端点白名单 + "EndpointWhitelist": [], + // 客户端白名单 + "ClientWhitelist": [], + "QuotaExceededResponse": { + "Content": "{{\"code\":429,\"type\":\"error\",\"message\":\"访问过于频繁,请稍后重试!禁止违法行为否则110 👮\",\"result\":null,\"extras\":null}}", + "ContentType": "application/json", + "StatusCode": 429 + }, + // 返回状态码 + "HttpStatusCode": 429, + // API规则,结尾一定要带* + "GeneralRules": [ + // 1秒钟只能调用1000次 + { + "Endpoint": "*", + "Period": "1s", + "Limit": 1000 + }, + // 1分钟只能调用60000次 + { + "Endpoint": "*", + "Period": "1m", + "Limit": 60000 + } + //// 1小时只能调用3600000次 + //{ + // "Endpoint": "*", + // "Period": "1h", + // "Limit": 3600000 + //}, + //// 1天只能调用86400000次 + //{ + // "Endpoint": "*", + // "Period": "1d", + // "Limit": 86400000 + //} + ] + }, + // IP 黑名单 + "IpRateLimitPolicies": { + "IpRules": [ + { + "Ip": "0.0.0.0", // IP可用:"::1/10" + "Rules": [ + { + "Endpoint": "*", + "Period": "1s", + "Limit": 0 // 设置为0就是1次都不能请求,完全屏蔽 + } + ] + } + ] + }, + // 客户端限流配置 + "ClientRateLimiting": { + "EnableEndpointRateLimiting": true, + "ClientIdHeader": "X-ClientId", + "EndpointWhitelist": [], + "ClientWhitelist": [], + "QuotaExceededResponse": { + "Content": "{{\"code\":429,\"type\":\"error\",\"message\":\"访问人数过多,请稍后重试!\",\"result\":null,\"extras\":null}}", + "ContentType": "application/json", + "StatusCode": 429 + }, + "HttpStatusCode": 429, + "GeneralRules": [ + { + "Endpoint": "*", + "Period": "1s", + "Limit": 2000 + } + ] + }, + "ClientRateLimitPolicies": { + "ClientRules": [ + { + "ClientId": "", + "Rules": [ + { + "Endpoint": "*", + "Period": "1s", + "Limit": 2000 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Logging.json b/Evergreen.Health.Application/Configuration/Logging.json new file mode 100644 index 0000000..4d83d40 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Logging.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Information", + "AspNetCoreRateLimit": "None", + "System.Net.Http.HttpClient": "Warning" + }, + "File": { + "Enabled": true, // 启用文件日志 + "FileName": "logs/{0:yyyyMMdd}_{1}.log", // 日志文件 + "Append": true, // 追加覆盖 + "MinimumLevel": "Error", // 日志级别 + "FileSizeLimitBytes": 10485760, // 10M=10*1024*1024 + "MaxRollingFiles": 30 // 只保留30个文件 + }, + "Database": { + "Enabled": true, // 启用数据库日志 + "MinimumLevel": "Information" // 日志级别 + }, + "Monitor": { + "GlobalEnabled": true, // 启用全局拦截日志(建议生产环境关闭,否则对性能有影响) + "IncludeOfMethods": [], // 拦截特定方法,当GlobalEnabled=false有效 + "ExcludeOfMethods": [], // 排除特定方法,当GlobalEnabled=true有效 + "BahLogLevel": "Information", // Oops.Oh 和 Oops.Bah 业务日志输出级别 + "WithReturnValue": true, // 是否包含返回值,默认true + "ReturnValueThreshold": 0, // 返回值字符串阈值,默认0全量输出 + "JsonBehavior": "None", // 是否输出Json,默认None(OnlyJson、All) + "JsonIndented": false, // 是否格式化Json + "UseUtcTimestamp": false, // 时间格式UTC、LOCAL + "ConsoleLog": true // 是否显示控制台日志 + } + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/OAuth.json b/Evergreen.Health.Application/Configuration/OAuth.json new file mode 100644 index 0000000..f2f28aa --- /dev/null +++ b/Evergreen.Health.Application/Configuration/OAuth.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "OAuth": { + "Weixin": { + "ClientId": "xxx", + "ClientSecret": "xxx" + }, + "Gitee": { + "ClientId": "xxx", + "ClientSecret": "xxx" + } + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/SMS.json b/Evergreen.Health.Application/Configuration/SMS.json new file mode 100644 index 0000000..bc5c546 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/SMS.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 短信配置 + "SMS": { + "VerifyCodeExpireSeconds": 60, // 验证码缓存过期时间(秒),默认60秒 + // 阿里云短信 + "Aliyun": { + "AccessKeyId": "", + "AccessKeySecret": "", + "Templates": [ + { + "Id": "0", + "SignName": "AdminNET 平台", + "TemplateCode": "SMS_xxx", + "Content": "您的验证码为:${code},请勿泄露于他人!" + }, + { + "Id": "1", + "SignName": "AdminNET 平台", + "TemplateCode": "SMS_xxx", + "Content": "注册成功,感谢您的注册,请妥善保管您的账户信息" + } + ] + }, + // 腾讯云短信 + "Tencentyun": { + "SdkAppId": "", + "AccessKeyId": "", + "AccessKeySecret": "", + "Templates": [ + { + "Id": "0", + "SignName": "AdminNET 平台", + "TemplateCode": "", + "Content": "" + } + ] + }, + // 自定义短信接口 + "Custom": { + "Enabled": false, // 是否启用自定义短信接口 + "Method": "GET", // 请求方法: GET 或 POST + "ApiUrl": "https://api.xxxx.com/sms?u=xxxx&key=59e03f49c3dbb5033&m={mobile}&c={content}", // API接口地址,支持占位符: {mobile}, {content}, {code} + "ContentType": "application/x-www-form-urlencoded", // POST请求的Content-Type: application/json 或 application/x-www-form-urlencoded + "PostData": "", // POST请求的数据模板,支持占位符,JSON 格式示例: {"mobile":"{mobile}","content":"{content}","apikey":"your_key"};Form 格式示例: mobile={mobile}&content={content}&apikey=your_key + "SuccessFlag": "0", // 成功响应标识,响应内容包含此字符串则认为发送成功 + "Templates": [ + { + "Id": "0", + "Content": "您的验证码为:{code},请勿泄露于他人!" + }, + { + "Id": "1", + "Content": "注册成功,感谢您的注册,请妥善保管您的账户信息" + } + ] + } + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Swagger.json b/Evergreen.Health.Application/Configuration/Swagger.json new file mode 100644 index 0000000..b3926fb --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Swagger.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "SpecificationDocumentSettings": { + "DocumentTitle": "常青树健康管理系统", + "GroupOpenApiInfos": [ + { + "Group": "Default", + "Title": "常青树健康管理系统", + "Description": "让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。", + "Version": "1.0.0", + "Order": 1000 + }, + { + "Group": "All Groups", + "Title": "所有接口", + "Description": "让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。", + "Version": "1.0.0", + "Order": 0 + } + ], + "DefaultGroupName": "Default", // 默认分组名 + "DocExpansionState": "List", // List、Full、None + "EnableAllGroups": true, + //"ServerDir": "xxx", // 二级虚拟目录并且开启 Servers 选择列表 + "HideServers": true, + "Servers": [ + { + "Url": "http://ip/xxx", + "Description": "二级目录应用程序名" + } + ], + "LoginInfo": { + "Enabled": true, // 是否开启Swagger登录 + "CheckUrl": "/api/swagger/checkUrl", + "SubmitUrl": "/api/swagger/submitUrl", + "EnableOnProduction": false // 是否在生产环境中自动开启 + }, + "EnumToNumber": true // 枚举类型生成值类型 + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Upload.json b/Evergreen.Health.Application/Configuration/Upload.json new file mode 100644 index 0000000..d504fe7 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Upload.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Upload": { + "Path": "upload/{yyyy}/{MM}/{dd}", // 文件上传目录 + "MaxSize": 51200, // 文件最大限制KB:1024*50 + "ContentType": [ "image/jpg", "image/png", "image/jpeg", "image/gif", "image/bmp", "text/plain", "text/xml", "application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "video/mp4", "application/wps-office.docx", "application/wps-office.xlsx", "application/wps-office.pptx", "application/vnd.android.package-archive" ], + "EnableMd5": false // 启用文件MDF5验证-防止重复上传 + }, + "OSSProvider": { + "Enabled": false, + "Provider": "Minio", // OSS提供者 Invalid/Minio/Aliyun/QCloud/Qiniu/HuaweiCloud + "Endpoint": "xxx.xxx.xxx.xxx:8090", // 节点/API地址(在腾讯云OSS中表示AppId) + "Region": "xxx.xxx.xxx.xxx", // 地域 + "AccessKey": "", + "SecretKey": "", + "IsEnableHttps": false, // 是否启用HTTPS + "IsEnableCache": true, // 是否启用缓存 + "Bucket": "admin.net", + "CustomHost": "" // 自定义Host:拼接外链的Host,若空则使用Endpoint拼接 + }, + "SSHProvider": { + "Enabled": false, + "Host": "127.0.0.1", + "Port": 8222, + "Username": "sshuser", + "Password": "Password.1" + }, + "MultiOSS": { + "Enabled": true, // 是否启用多OSS功能 + "Description": "启用多OSS功能后,系统将支持多个存储提供者,可以通过BucketName或ProviderId指定使用哪个存储提供者" + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Configuration/Wechat.json b/Evergreen.Health.Application/Configuration/Wechat.json new file mode 100644 index 0000000..25ec0d2 --- /dev/null +++ b/Evergreen.Health.Application/Configuration/Wechat.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Wechat": { + // 公众号 + "WechatAppId": "", + "WechatAppSecret": "", + "WechatToken": "", // 微信公众号服务器配置中的令牌(Token) + "WechatEncodingAESKey": "", // 微信公众号服务器配置中的消息加解密密钥(EncodingAESKey) + // 小程序 + "WxOpenAppId": "", + "WxOpenAppSecret": "", + "WxToken": "", // 小程序消息推送中的令牌(Token) + "WxEncodingAESKey": "", // 小程序消息推送中的消息加解密密钥(EncodingAESKey) + "QRImagePath": "" //小程序生成带参数二维码保存位置(绝对路径 eg: D:\\Web\\wwwroot\\upload\\QRImage),如果不配置,则默认保存在项目根目录下wwwroot\\upload\\QRImage + }, + // 微信支付 + "WechatPay": { + "AppId": "", // 微信公众平台AppId、开放平台AppId、小程序AppId、企业微信CorpId + "MerchantId": "", // 商户平台的商户号 + "MerchantV3Secret": "", // 商户平台的APIv3密钥 + "MerchantCertificateSerialNumber": "", // 商户平台的证书序列号 + "MerchantCertificatePrivateKey": "WxPayCert/apiclient_key.pem" // 商户平台的API证书私钥(apiclient_key.pem文件内容) + }, + // 支付回调 + "PayCallBack": { + "WechatPayUrl": "https://xxx/api/sysWechatPay/payCallBack", // 微信支付回调 + "WechatRefundUrl": "", // 微信退款回调 + "AlipayUrl": "", // 支付宝支付回调 + "AlipayRefundUrl": "" // 支付宝退款回调 + } +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Const/ApplicationConst.cs b/Evergreen.Health.Application/Const/ApplicationConst.cs new file mode 100644 index 0000000..4aab93b --- /dev/null +++ b/Evergreen.Health.Application/Const/ApplicationConst.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Evergreen.Health.Application; + +/// +/// 业务应用相关常量 +/// +public class ApplicationConst +{ + /// + /// API分组名称 + /// + public const string GroupName = "xxx业务应用"; +} \ No newline at end of file diff --git a/Evergreen.Health.Application/Evergreen.Health.Application.csproj b/Evergreen.Health.Application/Evergreen.Health.Application.csproj new file mode 100644 index 0000000..a5ed9a6 --- /dev/null +++ b/Evergreen.Health.Application/Evergreen.Health.Application.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + 1701;1702;1591;8632 + + enable + True + disable + Admin.NET + 常青树健康管理系统 + + + + + true + PreserveNewest + PreserveNewest + + + true + PreserveNewest + PreserveNewest + + + + + + + + diff --git a/Evergreen.Health.Application/GlobalUsings.cs b/Evergreen.Health.Application/GlobalUsings.cs new file mode 100644 index 0000000..c35c4ea --- /dev/null +++ b/Evergreen.Health.Application/GlobalUsings.cs @@ -0,0 +1,15 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Admin.NET.Core; +global using Furion; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Extensions.DependencyInjection; +global using System; +global using System.Threading.Tasks; \ No newline at end of file diff --git a/Evergreen.Health.Application/Health/Entities/HealthEntities.cs b/Evergreen.Health.Application/Health/Entities/HealthEntities.cs new file mode 100644 index 0000000..97f64be --- /dev/null +++ b/Evergreen.Health.Application/Health/Entities/HealthEntities.cs @@ -0,0 +1,142 @@ +using SqlSugar; +using System.Text.Json.Serialization; + +namespace Evergreen.Health.Application.Health.Entities; + +public enum HealthUserStatus { Disabled = 0, Enabled = 1 } + +[SugarTable("health_user", "健康端用户")] +[SugarIndex("idx_health_user_phone", nameof(Phone), OrderByType.Asc, IsUnique = true)] +public sealed class HealthUser : EntityBaseDel +{ + [SugarColumn(ColumnDescription = "手机号", Length = 20)] + public string Phone { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "密码摘要", Length = 256)] + [JsonIgnore, Newtonsoft.Json.JsonIgnore] + public string PasswordHash { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "姓名或昵称", Length = 50)] + public string DisplayName { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "头像", Length = 500, IsNullable = true)] + public string? AvatarUrl { get; set; } + + [SugarColumn(ColumnDescription = "状态")] + public HealthUserStatus Status { get; set; } = HealthUserStatus.Enabled; + + [SugarColumn(ColumnDescription = "最后登录时间", IsNullable = true)] + public DateTime? LastLoginTime { get; set; } + + [SugarColumn(ColumnDescription = "最后登录IP", Length = 64, IsNullable = true)] + public string? LastLoginIp { get; set; } +} + +[SugarTable("health_profile", "用户健康档案")] +[SugarIndex("idx_health_profile_user", nameof(UserId), OrderByType.Asc, IsUnique = true)] +public sealed class HealthProfile : EntityBase +{ + [SugarColumn(ColumnDescription = "健康用户Id")] + public long UserId { get; set; } + + [SugarColumn(ColumnDescription = "出生日期", IsNullable = true)] + public DateTime? Birthday { get; set; } + + [SugarColumn(ColumnDescription = "性别", Length = 20, IsNullable = true)] + public string? Gender { get; set; } + + [SugarColumn(ColumnDescription = "身高厘米", DecimalDigits = 1, IsNullable = true)] + public decimal? HeightCm { get; set; } + + [SugarColumn(ColumnDescription = "体重千克", DecimalDigits = 1, IsNullable = true)] + public decimal? WeightKg { get; set; } + + [SugarColumn(ColumnDescription = "平均睡眠小时", DecimalDigits = 1, IsNullable = true)] + public decimal? AverageSleepHours { get; set; } + + [SugarColumn(ColumnDescription = "每日运动分钟", IsNullable = true)] + public int? DailyExerciseMinutes { get; set; } + + [SugarColumn(ColumnDescription = "饮食偏好", Length = 500, IsNullable = true)] + public string? DietPreference { get; set; } + + [SugarColumn(ColumnDescription = "健康目标", Length = 500, IsNullable = true)] + public string? HealthGoal { get; set; } +} + +[SugarTable("health_module_selection", "用户健康板块选择")] +[SugarIndex("idx_health_module_user_code", nameof(UserId), OrderByType.Asc, nameof(ModuleCode), OrderByType.Asc, IsUnique = true)] +public sealed class HealthModuleSelection : EntityBase +{ + [SugarColumn(ColumnDescription = "健康用户Id")] + public long UserId { get; set; } + + [SugarColumn(ColumnDescription = "板块编码", Length = 32)] + public string ModuleCode { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "是否启用")] + public bool IsEnabled { get; set; } = true; +} + +[SugarTable("health_plan", "个性化健康方案")] +[SugarIndex("idx_health_plan_user", nameof(UserId), OrderByType.Asc)] +public sealed class HealthPlan : EntityBase +{ + [SugarColumn(ColumnDescription = "健康用户Id")] + public long UserId { get; set; } + + [SugarColumn(ColumnDescription = "方案版本")] + public int Version { get; set; } = 1; + + [SugarColumn(ColumnDescription = "方案标题", Length = 100)] + public string Title { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "方案摘要", Length = 1000)] + public string Summary { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "参数JSON", ColumnDataType = "text")] + public string ParametersJson { get; set; } = "[]"; + + [SugarColumn(ColumnDescription = "当前方案")] + public bool IsCurrent { get; set; } = true; +} + +[SugarTable("health_device_binding", "健康用户设备绑定")] +[SugarIndex("idx_health_device_user_entity", nameof(UserId), OrderByType.Asc, nameof(EntityId), OrderByType.Asc, IsUnique = true)] +public sealed class HealthDeviceBinding : EntityBase +{ + [SugarColumn(ColumnDescription = "健康用户Id")] + public long UserId { get; set; } + + [SugarColumn(ColumnDescription = "Home Assistant实体Id", Length = 200)] + public string EntityId { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "显示名称", Length = 100)] + public string DisplayName { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "设备类型", Length = 50, IsNullable = true)] + public string? DeviceType { get; set; } + + [SugarColumn(ColumnDescription = "是否启用")] + public bool IsEnabled { get; set; } = true; +} + +[SugarTable("health_scene_execution", "健康场景执行记录")] +[SugarIndex("idx_health_scene_user", nameof(UserId), OrderByType.Asc)] +public sealed class HealthSceneExecution : EntityBase +{ + [SugarColumn(ColumnDescription = "健康用户Id")] + public long UserId { get; set; } + + [SugarColumn(ColumnDescription = "场景编码", Length = 50)] + public string SceneCode { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "场景名称", Length = 100)] + public string SceneName { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "执行状态", Length = 32)] + public string Status { get; set; } = string.Empty; + + [SugarColumn(ColumnDescription = "执行详情", ColumnDataType = "text", IsNullable = true)] + public string? Detail { get; set; } +} diff --git a/Evergreen.Health.Application/Health/Models/HealthModels.cs b/Evergreen.Health.Application/Health/Models/HealthModels.cs new file mode 100644 index 0000000..03b32ae --- /dev/null +++ b/Evergreen.Health.Application/Health/Models/HealthModels.cs @@ -0,0 +1,97 @@ +using System.ComponentModel.DataAnnotations; + +namespace Evergreen.Health.Application.Health.Models; + +public sealed class HealthRegisterInput +{ + [Required, RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "请输入正确的手机号码")] + public string Phone { get; set; } = string.Empty; + + [Required, MinLength(8), MaxLength(32)] + public string Password { get; set; } = string.Empty; + + [Required, MaxLength(50)] + public string DisplayName { get; set; } = string.Empty; +} + +public sealed class HealthLoginInput +{ + [Required] + public string Phone { get; set; } = string.Empty; + + [Required] + public string Password { get; set; } = string.Empty; +} + +public sealed class HealthAuthOutput +{ + public string AccessToken { get; set; } = string.Empty; + public string RefreshToken { get; set; } = string.Empty; + public HealthUserOutput User { get; set; } = new(); +} + +public sealed class HealthUserOutput +{ + public long Id { get; set; } + public string Phone { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string? AvatarUrl { get; set; } +} + +public sealed class SaveHealthProfileInput +{ + public DateTime? Birthday { get; set; } + + [MaxLength(20)] + public string? Gender { get; set; } + + [Range(80, 250)] + public decimal? HeightCm { get; set; } + + [Range(20, 300)] + public decimal? WeightKg { get; set; } + + [Range(0, 24)] + public decimal? AverageSleepHours { get; set; } + + [Range(0, 1440)] + public int? DailyExerciseMinutes { get; set; } + + [MaxLength(500)] + public string? DietPreference { get; set; } + + [MaxLength(500)] + public string? HealthGoal { get; set; } +} + +public sealed class SaveHealthModulesInput +{ + [Required, MinLength(1)] + public List ModuleCodes { get; set; } = []; +} + +public sealed class HealthPlanItemOutput +{ + public string ModuleCode { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Target { get; set; } = string.Empty; + public string Advice { get; set; } = string.Empty; +} + +public sealed class HealthPlanOutput +{ + public long Id { get; set; } + public int Version { get; set; } + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + public List Items { get; set; } = []; +} + +public sealed class HealthDashboardOutput +{ + public HealthUserOutput User { get; set; } = new(); + public SaveHealthProfileInput? Profile { get; set; } + public List SelectedModules { get; set; } = []; + public HealthPlanOutput? CurrentPlan { get; set; } +} diff --git a/Evergreen.Health.Application/Health/Security/HealthPasswordHasher.cs b/Evergreen.Health.Application/Health/Security/HealthPasswordHasher.cs new file mode 100644 index 0000000..1c8c86e --- /dev/null +++ b/Evergreen.Health.Application/Health/Security/HealthPasswordHasher.cs @@ -0,0 +1,37 @@ +using System.Security.Cryptography; + +namespace Evergreen.Health.Application.Health.Security; + +internal static class HealthPasswordHasher +{ + private const int Iterations = 120_000; + private const int SaltSize = 16; + private const int KeySize = 32; + private const string Algorithm = "pbkdf2-sha256"; + + public static string Hash(string password) + { + var salt = RandomNumberGenerator.GetBytes(SaltSize); + var key = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, KeySize); + return "$" + Algorithm + "$" + Iterations + "$" + Convert.ToBase64String(salt) + "$" + Convert.ToBase64String(key); + } + + public static bool Verify(string password, string encodedHash) + { + try + { + var parts = encodedHash.Split('$', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 4 || parts[0] != Algorithm || !int.TryParse(parts[1], out var iterations)) + return false; + + var salt = Convert.FromBase64String(parts[2]); + var expected = Convert.FromBase64String(parts[3]); + var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expected.Length); + return CryptographicOperations.FixedTimeEquals(actual, expected); + } + catch (FormatException) + { + return false; + } + } +} diff --git a/Evergreen.Health.Application/Health/Services/HealthAppService.cs b/Evergreen.Health.Application/Health/Services/HealthAppService.cs new file mode 100644 index 0000000..66aadd3 --- /dev/null +++ b/Evergreen.Health.Application/Health/Services/HealthAppService.cs @@ -0,0 +1,331 @@ +using Evergreen.Health.Application.Health.Entities; +using Evergreen.Health.Application.Health.Models; +using Evergreen.Health.Application.Health.Security; +using Furion.DataEncryption; +using Furion.DatabaseAccessor; +using Furion.FriendlyException; +using Microsoft.AspNetCore.Http; +using SqlSugar; +using System.Security.Claims; +using System.Text.Json; + +namespace Evergreen.Health.Application.Health.Services; + +[ApiDescriptionSettings("健康管理", Name = "Health", Order = 50)] +public sealed class HealthAppService : IDynamicApiController, ITransient +{ + private const string HealthUserIdClaim = "health_user_id"; + private static readonly HashSet SupportedModules = new(StringComparer.OrdinalIgnoreCase) + { + "sleep", "exercise", "diet" + }; + + private readonly SqlSugarRepository _userRepository; + private readonly SqlSugarRepository _profileRepository; + private readonly SqlSugarRepository _moduleRepository; + private readonly SqlSugarRepository _planRepository; + private readonly IHttpContextAccessor _httpContextAccessor; + + public HealthAppService( + SqlSugarRepository userRepository, + SqlSugarRepository profileRepository, + SqlSugarRepository moduleRepository, + SqlSugarRepository planRepository, + IHttpContextAccessor httpContextAccessor) + { + _userRepository = userRepository; + _profileRepository = profileRepository; + _moduleRepository = moduleRepository; + _planRepository = planRepository; + _httpContextAccessor = httpContextAccessor; + } + + [AllowAnonymous] + [HttpPost("/api/health/auth/register")] + [UnitOfWork] + public async Task Register(HealthRegisterInput input) + { + var phone = input.Phone.Trim(); + if (await _userRepository.IsAnyAsync(user => user.Phone == phone && !user.IsDelete)) + throw Oops.Oh("该手机号已经注册"); + + var user = await _userRepository.AsInsertable(new HealthUser + { + Phone = phone, + DisplayName = input.DisplayName.Trim(), + PasswordHash = HealthPasswordHasher.Hash(input.Password), + Status = HealthUserStatus.Enabled + }).ExecuteReturnEntityAsync(); + + await _profileRepository.InsertAsync(new HealthProfile { UserId = user.Id }); + await _moduleRepository.InsertRangeAsync(new List + { + new HealthModuleSelection { UserId = user.Id, ModuleCode = "sleep", IsEnabled = true }, + new HealthModuleSelection { UserId = user.Id, ModuleCode = "exercise", IsEnabled = true }, + new HealthModuleSelection { UserId = user.Id, ModuleCode = "diet", IsEnabled = true } + }); + + return CreateAuthOutput(user); + } + + [AllowAnonymous] + [HttpPost("/api/health/auth/login")] + public async Task Login(HealthLoginInput input) + { + var phone = input.Phone.Trim(); + var user = await _userRepository.GetFirstAsync(item => item.Phone == phone && !item.IsDelete); + if (user is null || !HealthPasswordHasher.Verify(input.Password, user.PasswordHash)) + throw Oops.Oh("手机号或密码错误").StatusCode(401); + if (user.Status != HealthUserStatus.Enabled) + throw Oops.Oh("账号已停用").StatusCode(403); + + user.LastLoginTime = DateTime.Now; + user.LastLoginIp = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(); + await _userRepository.AsUpdateable(user) + .UpdateColumns(item => new { item.LastLoginTime, item.LastLoginIp }) + .ExecuteCommandAsync(); + + return CreateAuthOutput(user); + } + + [Authorize] + [HttpGet("/api/health/dashboard")] + public async Task GetDashboard() + { + var userId = GetCurrentUserId(); + var user = await GetCurrentUser(userId); + var profile = await _profileRepository.GetFirstAsync(item => item.UserId == userId); + var modules = await _moduleRepository.AsQueryable() + .Where(item => item.UserId == userId && item.IsEnabled) + .Select(item => item.ModuleCode) + .ToListAsync(); + var plan = await _planRepository.AsQueryable() + .Where(item => item.UserId == userId && item.IsCurrent) + .OrderByDescending(item => item.Version) + .FirstAsync(); + + return new HealthDashboardOutput + { + User = MapUser(user), + Profile = profile is null ? null : MapProfile(profile), + SelectedModules = modules, + CurrentPlan = plan is null ? null : MapPlan(plan) + }; + } + + [Authorize] + [HttpPut("/api/health/profile")] + public async Task SaveProfile(SaveHealthProfileInput input) + { + var userId = GetCurrentUserId(); + var profile = await _profileRepository.GetFirstAsync(item => item.UserId == userId); + if (profile is null) + { + profile = new HealthProfile { UserId = userId }; + ApplyProfile(profile, input); + await _profileRepository.InsertAsync(profile); + } + else + { + ApplyProfile(profile, input); + await _profileRepository.UpdateAsync(profile); + } + + return MapProfile(profile); + } + + [Authorize] + [HttpPut("/api/health/modules")] + [UnitOfWork] + public async Task> SaveModules(SaveHealthModulesInput input) + { + var userId = GetCurrentUserId(); + var modules = input.ModuleCodes + .Select(item => item.Trim().ToLowerInvariant()) + .Where(SupportedModules.Contains) + .Distinct() + .ToList(); + + if (modules.Count == 0) + throw Oops.Oh("请至少选择一个健康板块"); + + await _moduleRepository.DeleteAsync(item => item.UserId == userId); + await _moduleRepository.InsertRangeAsync(modules.Select(module => new HealthModuleSelection + { + UserId = userId, + ModuleCode = module, + IsEnabled = true + }).ToList()); + + return modules; + } + + [Authorize] + [HttpPost("/api/health/plans/generate")] + [UnitOfWork] + public async Task GeneratePlan() + { + var userId = GetCurrentUserId(); + var profile = await _profileRepository.GetFirstAsync(item => item.UserId == userId); + var modules = await _moduleRepository.AsQueryable() + .Where(item => item.UserId == userId && item.IsEnabled) + .Select(item => item.ModuleCode) + .ToListAsync(); + + if (modules.Count == 0) + throw Oops.Oh("请先选择健康板块"); + + var items = BuildPlanItems(profile, modules); + var currentVersion = await _planRepository.AsQueryable() + .Where(item => item.UserId == userId) + .MaxAsync(item => (int?)item.Version) ?? 0; + + await _planRepository.Context.Updateable() + .SetColumns(item => item.IsCurrent == false) + .Where(item => item.UserId == userId && item.IsCurrent) + .ExecuteCommandAsync(); + + var plan = await _planRepository.AsInsertable(new HealthPlan + { + UserId = userId, + Version = currentVersion + 1, + Title = "您的专属健康方案", + Summary = BuildSummary(profile, modules), + ParametersJson = JsonSerializer.Serialize(items), + IsCurrent = true + }).ExecuteReturnEntityAsync(); + + return MapPlan(plan); + } + + private long GetCurrentUserId() + { + var value = _httpContextAccessor.HttpContext?.User.FindFirstValue(HealthUserIdClaim); + if (!long.TryParse(value, out var userId)) + throw Oops.Oh("登录状态已失效").StatusCode(401); + return userId; + } + + private async Task GetCurrentUser(long userId) + { + return await _userRepository.GetFirstAsync(item => item.Id == userId && !item.IsDelete) + ?? throw Oops.Oh("用户不存在").StatusCode(401); + } + + private static void ApplyProfile(HealthProfile profile, SaveHealthProfileInput input) + { + profile.Birthday = input.Birthday; + profile.Gender = input.Gender?.Trim(); + profile.HeightCm = input.HeightCm; + profile.WeightKg = input.WeightKg; + profile.AverageSleepHours = input.AverageSleepHours; + profile.DailyExerciseMinutes = input.DailyExerciseMinutes; + profile.DietPreference = input.DietPreference?.Trim(); + profile.HealthGoal = input.HealthGoal?.Trim(); + } + + private static SaveHealthProfileInput MapProfile(HealthProfile profile) => new() + { + Birthday = profile.Birthday, + Gender = profile.Gender, + HeightCm = profile.HeightCm, + WeightKg = profile.WeightKg, + AverageSleepHours = profile.AverageSleepHours, + DailyExerciseMinutes = profile.DailyExerciseMinutes, + DietPreference = profile.DietPreference, + HealthGoal = profile.HealthGoal + }; + + private static HealthUserOutput MapUser(HealthUser user) => new() + { + Id = user.Id, + Phone = user.Phone, + DisplayName = user.DisplayName, + AvatarUrl = user.AvatarUrl + }; + + private static List BuildPlanItems(HealthProfile? profile, IEnumerable modules) + { + var items = new List(); + foreach (var module in modules) + { + switch (module) + { + case "sleep": + items.Add(new HealthPlanItemOutput + { + ModuleCode = module, + Title = "睡眠质量", + Target = profile?.AverageSleepHours is < 7 ? "逐步达到每晚 7–8 小时" : "保持每晚 7–8 小时", + Advice = "固定入睡时间,睡前一小时降低灯光亮度,并使用卧室舒睡场景。" + }); + break; + case "exercise": + items.Add(new HealthPlanItemOutput + { + ModuleCode = module, + Title = "运动情况", + Target = profile?.DailyExerciseMinutes is >= 30 ? "保持每日 30 分钟以上活动" : "从每日 20 分钟步行开始", + Advice = "优先安排低到中等强度运动,每坐一小时起身活动 3–5 分钟。" + }); + break; + case "diet": + items.Add(new HealthPlanItemOutput + { + ModuleCode = module, + Title = "饮食情况", + Target = "三餐规律,晚餐不过量", + Advice = string.IsNullOrWhiteSpace(profile?.DietPreference) + ? "记录三餐与饮水情况,优先增加蔬菜和优质蛋白。" + : $"结合“{profile.DietPreference}”偏好控制总量,并保持食物多样性。" + }); + break; + } + } + return items; + } + + private static string BuildSummary(HealthProfile? profile, IReadOnlyCollection modules) + { + var bmiText = string.Empty; + if (profile?.HeightCm > 0 && profile.WeightKg > 0) + { + var height = profile.HeightCm.Value / 100m; + var bmi = Math.Round(profile.WeightKg.Value / (height * height), 1); + bmiText = $" 当前 BMI 约为 {bmi}。"; + } + + return $"方案依据您选择的 {modules.Count} 个健康板块生成。{bmiText}第一版参数用于健康管理演示,不替代专业医疗诊断。"; + } + + private static HealthPlanOutput MapPlan(HealthPlan plan) => new() + { + Id = plan.Id, + Version = plan.Version, + Title = plan.Title, + Summary = plan.Summary, + CreateTime = plan.CreateTime, + Items = JsonSerializer.Deserialize>(plan.ParametersJson) ?? [] + }; + + private static HealthAuthOutput CreateAuthOutput(HealthUser user) + { + const int accessTokenMinutes = 24 * 60; + const int refreshTokenMinutes = 7 * 24 * 60; + var accessToken = JWTEncryption.Encrypt(new Dictionary + { + [HealthUserIdClaim] = user.Id, + [ClaimTypes.NameIdentifier] = user.Id, + [ClaimTypes.Name] = user.DisplayName, + [ClaimTypes.MobilePhone] = user.Phone, + ["account_type"] = "health_user" + }, accessTokenMinutes); + + return new HealthAuthOutput + { + AccessToken = accessToken, + RefreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenMinutes), + User = MapUser(user) + }; + } +} diff --git a/Evergreen.Health.Application/Startup.cs b/Evergreen.Health.Application/Startup.cs new file mode 100644 index 0000000..5b022df --- /dev/null +++ b/Evergreen.Health.Application/Startup.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Evergreen.Health.Application; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } +} \ No newline at end of file diff --git a/Evergreen.Health.sln b/Evergreen.Health.sln new file mode 100644 index 0000000..d90d7a6 --- /dev/null +++ b/Evergreen.Health.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Evergreen.Health.Application", "Evergreen.Health.Application\Evergreen.Health.Application.csproj", "{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Core", "Admin.NET.Core\Admin.NET.Core.csproj", "{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Web.Core", "Admin.NET.Web.Core\Admin.NET.Web.Core.csproj", "{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Evergreen.Health.Api", "Evergreen.Health.Api\Evergreen.Health.Api.csproj", "{11EA630B-4600-4236-A117-CE6C6CD67586}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Test", "Admin.NET.Test\Admin.NET.Test.csproj", "{57350E6A-B5A0-452C-9FD4-C69617C6DA30}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{662E0B8E-F23E-4C7D-80BD-CAA5707503CC}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|Any CPU.Build.0 = Release|Any CPU + {3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|Any CPU.Build.0 = Release|Any CPU + {8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|Any CPU.Build.0 = Release|Any CPU + {11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11EA630B-4600-4236-A117-CE6C6CD67586}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11EA630B-4600-4236-A117-CE6C6CD67586}.Release|Any CPU.Build.0 = Release|Any CPU + {57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5CD801D7-984A-4F5C-8FA2-211B7A5EA9F3} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..a09b822 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Evergreen Health Backend + +常青树健康管理系统后台接口,基于 .NET 8、Admin.NET、SqlSugar 和 MySQL。 + +## 本地启动 + +1. 复制 `Evergreen.Health.Api/Configuration/Database.local.example.json` 为 `Database.local.json`。 +2. 修改 `Database.local.json` 中的 MySQL 连接字符串。该文件已被 Git 忽略,不会提交密码。 +3. 执行 `dotnet restore Evergreen.Health.sln`。 +4. 执行 `dotnet run --project Evergreen.Health.Api/Evergreen.Health.Api.csproj`。 + +## 健康业务接口 + +- `POST /api/health/auth/register` 用户注册 +- `POST /api/health/auth/login` 用户登录 +- `GET /api/health/dashboard` 健康首页 +- `PUT /api/health/profile` 更新个人健康档案 +- `PUT /api/health/modules` 选择健康板块 +- `POST /api/health/plans/generate` 生成个性化健康参数 + +接口文档以应用启动后生成的 OpenAPI/Scalar 页面为准。