commit b1a69d50e71d039572d9d77d11b4ccfe8870f096
Author: wanggang <76527413@qq.com>
Date: Mon Jun 26 17:46:40 2023 +0800
添加文档和UI原型菜单部分
diff --git a/docs/demo/.editorconfig b/docs/demo/.editorconfig
new file mode 100644
index 00000000..20273079
--- /dev/null
+++ b/docs/demo/.editorconfig
@@ -0,0 +1,536 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.cs]
+indent_size = 4
+dotnet_sort_system_directives_first = true
+
+# Don't use this. qualifier
+dotnet_style_qualification_for_field = true:suggestion
+dotnet_style_qualification_for_property = true:suggestion
+
+# use int x = .. over Int32
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+
+# use int.MaxValue over Int32.MaxValue
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Require var all the time.
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# Disallow throw expressions.
+csharp_style_throw_expression = false:suggestion
+
+# Newline settings
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+
+# Namespace settings
+csharp_style_namespace_declarations = file_scoped:silent
+
+# Brace settings
+csharp_prefer_braces = true:silent# Prefer curly braces even for one line of code
+
+[*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+[*.json]
+indent_size = 2
+
+[*.{ps1,psm1}]
+indent_size = 4
+
+[*.sh]
+indent_size = 4
+end_of_line = lf
+
+[*.{razor,cshtml}]
+charset = utf-8-bom
+
+[*.{cs,vb}]
+
+# SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
+dotnet_diagnostic.SYSLIB1054.severity = warning
+
+# CA1018: Mark attributes with AttributeUsageAttribute
+dotnet_diagnostic.CA1018.severity = warning
+
+# CA1047: Do not declare protected member in sealed type
+dotnet_diagnostic.CA1047.severity = warning
+
+# CA1305: Specify IFormatProvider
+dotnet_diagnostic.CA1305.severity = warning
+
+# CA1416: Validate platform compatibility
+dotnet_diagnostic.CA1416.severity = warning
+
+# CA1507: Use nameof to express symbol names
+dotnet_diagnostic.CA1507.severity = warning
+
+# CA1725: Parameter names should match base declaration
+dotnet_diagnostic.CA1725.severity = suggestion
+
+# CA1802: Use literals where appropriate
+dotnet_diagnostic.CA1802.severity = warning
+
+# CA1805: Do not initialize unnecessarily
+dotnet_diagnostic.CA1805.severity = warning
+
+# CA1810: Do not initialize unnecessarily
+dotnet_diagnostic.CA1810.severity = warning
+
+# CA1821: Remove empty Finalizers
+dotnet_diagnostic.CA1821.severity = warning
+
+# CA1822: Make member static
+dotnet_diagnostic.CA1822.severity = warning
+dotnet_code_quality.CA1822.api_surface = private, internal
+
+# CA1823: Avoid unused private fields
+dotnet_diagnostic.CA1823.severity = warning
+
+# CA1825: Avoid zero-length array allocations
+dotnet_diagnostic.CA1825.severity = warning
+
+# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly
+dotnet_diagnostic.CA1826.severity = warning
+
+# CA1827: Do not use Count() or LongCount() when Any() can be used
+dotnet_diagnostic.CA1827.severity = warning
+
+# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used
+dotnet_diagnostic.CA1828.severity = warning
+
+# CA1829: Use Length/Count property instead of Count() when available
+dotnet_diagnostic.CA1829.severity = warning
+
+# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder
+dotnet_diagnostic.CA1830.severity = warning
+
+# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
+# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
+# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
+dotnet_diagnostic.CA1831.severity = warning
+dotnet_diagnostic.CA1832.severity = warning
+dotnet_diagnostic.CA1833.severity = warning
+
+# CA1834: Consider using 'StringBuilder.Append(char)' when applicable
+dotnet_diagnostic.CA1834.severity = warning
+
+# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
+dotnet_diagnostic.CA1835.severity = warning
+
+# CA1836: Prefer IsEmpty over Count
+dotnet_diagnostic.CA1836.severity = warning
+
+# CA1837: Use 'Environment.ProcessId'
+dotnet_diagnostic.CA1837.severity = warning
+
+# CA1838: Avoid 'StringBuilder' parameters for P/Invokes
+dotnet_diagnostic.CA1838.severity = warning
+
+# CA1839: Use 'Environment.ProcessPath'
+dotnet_diagnostic.CA1839.severity = warning
+
+# CA1840: Use 'Environment.CurrentManagedThreadId'
+dotnet_diagnostic.CA1840.severity = warning
+
+# CA1841: Prefer Dictionary.Contains methods
+dotnet_diagnostic.CA1841.severity = warning
+
+# CA1842: Do not use 'WhenAll' with a single task
+dotnet_diagnostic.CA1842.severity = warning
+
+# CA1843: Do not use 'WaitAll' with a single task
+dotnet_diagnostic.CA1843.severity = warning
+
+# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'
+dotnet_diagnostic.CA1844.severity = warning
+
+# CA1845: Use span-based 'string.Concat'
+dotnet_diagnostic.CA1845.severity = warning
+
+# CA1846: Prefer AsSpan over Substring
+dotnet_diagnostic.CA1846.severity = warning
+
+# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
+dotnet_diagnostic.CA1847.severity = warning
+
+# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
+dotnet_diagnostic.CA1854.severity = warning
+
+# CA2007: Consider calling ConfigureAwait on the awaited task
+dotnet_diagnostic.CA2007.severity = warning
+
+# CA2008: Do not create tasks without passing a TaskScheduler
+dotnet_diagnostic.CA2008.severity = warning
+
+# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value
+dotnet_diagnostic.CA2009.severity = warning
+
+# CA2011: Avoid infinite recursion
+dotnet_diagnostic.CA2011.severity = warning
+
+# CA2012: Use ValueTask correctly
+dotnet_diagnostic.CA2012.severity = warning
+
+# CA2013: Do not use ReferenceEquals with value types
+dotnet_diagnostic.CA2013.severity = warning
+
+# CA2014: Do not use stackalloc in loops.
+dotnet_diagnostic.CA2014.severity = warning
+
+# CA2016: Forward the 'CancellationToken' parameter to methods that take one
+dotnet_diagnostic.CA2016.severity = warning
+
+# CA2200: Rethrow to preserve stack details
+dotnet_diagnostic.CA2200.severity = warning
+
+# CA2208: Instantiate argument exceptions correctly
+dotnet_diagnostic.CA2208.severity = warning
+
+# CA2245: Do not assign a property to itself
+dotnet_diagnostic.CA2245.severity = warning
+
+# CA2246: Assigning symbol and its member in the same statement
+dotnet_diagnostic.CA2246.severity = warning
+
+# CA2249: Use string.Contains instead of string.IndexOf to improve readability.
+dotnet_diagnostic.CA2249.severity = warning
+
+# IDE0005: Remove unnecessary usings
+dotnet_diagnostic.IDE0005.severity = warning
+
+# IDE0011: Curly braces to surround blocks of code
+dotnet_diagnostic.IDE0011.severity = warning
+
+# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable)
+dotnet_diagnostic.IDE0020.severity = warning
+
+# IDE0029: Use coalesce expression (non-nullable types)
+dotnet_diagnostic.IDE0029.severity = warning
+
+# IDE0030: Use coalesce expression (nullable types)
+dotnet_diagnostic.IDE0030.severity = warning
+
+# IDE0031: Use null propagation
+dotnet_diagnostic.IDE0031.severity = warning
+
+# IDE0035: Remove unreachable code
+dotnet_diagnostic.IDE0035.severity = warning
+
+# IDE0036: Order modifiers
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+dotnet_diagnostic.IDE0036.severity = warning
+
+# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable)
+dotnet_diagnostic.IDE0038.severity = warning
+
+# IDE0043: Format string contains invalid placeholder
+dotnet_diagnostic.IDE0043.severity = warning
+
+# IDE0044: Make field readonly
+dotnet_diagnostic.IDE0044.severity = warning
+
+# IDE0051: Remove unused private members
+dotnet_diagnostic.IDE0051.severity = warning
+
+# IDE0055: All formatting rules
+dotnet_diagnostic.IDE0055.severity = suggestion
+
+# IDE0059: Unnecessary assignment to a value
+dotnet_diagnostic.IDE0059.severity = warning
+
+# IDE0060: Remove unused parameter
+dotnet_code_quality_unused_parameters = non_public:suggestion
+dotnet_diagnostic.IDE0060.severity = warning
+
+# IDE0062: Make local function static
+dotnet_diagnostic.IDE0062.severity = warning
+
+# IDE0161: Convert to file-scoped namespace
+dotnet_diagnostic.IDE0161.severity = warning
+
+# IDE0200: Lambda expression can be removed
+dotnet_diagnostic.IDE0200.severity = warning
+
+# IDE2000: Disallow multiple blank lines
+dotnet_style_allow_multiple_blank_lines_experimental = false:silent
+dotnet_diagnostic.IDE2000.severity = warning
+
+[{eng/tools/**.cs,**/{test,testassets,samples,Samples,perf,scripts,stress}/**.cs}]
+# CA1018: Mark attributes with AttributeUsageAttribute
+dotnet_diagnostic.CA1018.severity = suggestion
+# CA1507: Use nameof to express symbol names
+dotnet_diagnostic.CA1507.severity = suggestion
+# CA1802: Use literals where appropriate
+dotnet_diagnostic.CA1802.severity = suggestion
+# CA1805: Do not initialize unnecessarily
+dotnet_diagnostic.CA1805.severity = suggestion
+# CA1810: Do not initialize unnecessarily
+dotnet_diagnostic.CA1810.severity = suggestion
+# CA1822: Make member static
+dotnet_diagnostic.CA1822.severity = suggestion
+# CA1823: Avoid zero-length array allocations
+dotnet_diagnostic.CA1825.severity = suggestion
+# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly
+dotnet_diagnostic.CA1826.severity = suggestion
+# CA1827: Do not use Count() or LongCount() when Any() can be used
+dotnet_diagnostic.CA1827.severity = suggestion
+# CA1829: Use Length/Count property instead of Count() when available
+dotnet_diagnostic.CA1829.severity = suggestion
+# CA1834: Consider using 'StringBuilder.Append(char)' when applicable
+dotnet_diagnostic.CA1834.severity = suggestion
+# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
+dotnet_diagnostic.CA1835.severity = suggestion
+# CA1837: Use 'Environment.ProcessId'
+dotnet_diagnostic.CA1837.severity = suggestion
+# CA1838: Avoid 'StringBuilder' parameters for P/Invokes
+dotnet_diagnostic.CA1838.severity = suggestion
+# CA1841: Prefer Dictionary.Contains methods
+dotnet_diagnostic.CA1841.severity = suggestion
+# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'
+dotnet_diagnostic.CA1844.severity = suggestion
+# CA1845: Use span-based 'string.Concat'
+dotnet_diagnostic.CA1845.severity = suggestion
+# CA1846: Prefer AsSpan over Substring
+dotnet_diagnostic.CA1846.severity = suggestion
+# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
+dotnet_diagnostic.CA1847.severity = suggestion
+# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
+dotnet_diagnostic.CA1854.severity = suggestion
+# CA2007: Consider calling ConfigureAwait on the awaited task
+dotnet_diagnostic.CA2007.severity = suggestion
+# CA2008: Do not create tasks without passing a TaskScheduler
+dotnet_diagnostic.CA2008.severity = suggestion
+# CA2012: Use ValueTask correctly
+dotnet_diagnostic.CA2012.severity = suggestion
+# CA2249: Use string.Contains instead of string.IndexOf to improve readability.
+dotnet_diagnostic.CA2249.severity = suggestion
+# IDE0005: Remove unnecessary usings
+dotnet_diagnostic.IDE0005.severity = suggestion
+# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable)
+dotnet_diagnostic.IDE0020.severity = suggestion
+# IDE0029: Use coalesce expression (non-nullable types)
+dotnet_diagnostic.IDE0029.severity = suggestion
+# IDE0030: Use coalesce expression (nullable types)
+dotnet_diagnostic.IDE0030.severity = suggestion
+# IDE0031: Use null propagation
+dotnet_diagnostic.IDE0031.severity = suggestion
+# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable)
+dotnet_diagnostic.IDE0038.severity = suggestion
+# IDE0044: Make field readonly
+dotnet_diagnostic.IDE0044.severity = suggestion
+# IDE0051: Remove unused private members
+dotnet_diagnostic.IDE0051.severity = suggestion
+# IDE0059: Unnecessary assignment to a value
+dotnet_diagnostic.IDE0059.severity = suggestion
+# IDE0060: Remove unused parameters
+dotnet_diagnostic.IDE0060.severity = suggestion
+# IDE0062: Make local function static
+dotnet_diagnostic.IDE0062.severity = suggestion
+# IDE0200: Lambda expression can be removed
+dotnet_diagnostic.IDE0200.severity = suggestion
+
+# CA2016: Forward the 'CancellationToken' parameter to methods that take one
+dotnet_diagnostic.CA2016.severity = suggestion
+
+# Defaults for content in the shared src/ and shared runtime dir
+
+[{**/Shared/runtime/**.{cs,vb},src/Shared/test/Shared.Tests/runtime/**.{cs,vb},**/microsoft.extensions.hostfactoryresolver.sources/**.{cs,vb}}]
+# CA1822: Make member static
+dotnet_diagnostic.CA1822.severity = silent
+# IDE0011: Use braces
+dotnet_diagnostic.IDE0011.severity = silent
+# IDE0055: Fix formatting
+dotnet_diagnostic.IDE0055.severity = silent
+# IDE0060: Remove unused parameters
+dotnet_diagnostic.IDE0060.severity = silent
+# IDE0062: Make local function static
+dotnet_diagnostic.IDE0062.severity = silent
+# IDE0161: Convert to file-scoped namespace
+dotnet_diagnostic.IDE0161.severity = silent
+
+[{**/Shared/**.cs,**/microsoft.extensions.hostfactoryresolver.sources/**.{cs,vb}}]
+# IDE0005: Remove unused usings. Ignore for shared src files since imports for those depend on the projects in which they are included.
+dotnet_diagnostic.IDE0005.severity = silent
+dotnet_style_qualification_for_field = true:suggestion
+dotnet_style_qualification_for_property = true:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_readonly_field = true:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_arithmetic_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_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:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+[*.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_prefer_simple_using_statement = true:suggestion
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_prefer_static_local_function = true:suggestion
+csharp_style_prefer_readonly_struct = 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_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
+
+[*.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
diff --git a/docs/demo/.gitattributes b/docs/demo/.gitattributes
new file mode 100644
index 00000000..314766e9
--- /dev/null
+++ b/docs/demo/.gitattributes
@@ -0,0 +1,3 @@
+* text=auto eol=lf
+*.{cmd,[cC][mM][dD]} text eol=crlf
+*.{bat,[bB][aA][tT]} text eol=crlf
diff --git a/docs/demo/.gitignore b/docs/demo/.gitignore
new file mode 100644
index 00000000..bfcbf6ee
--- /dev/null
+++ b/docs/demo/.gitignore
@@ -0,0 +1,16 @@
+*.bak
+
+#fe
+node_modules/
+dist/
+
+#be
+.vs/
+bin/
+obj/
+*.suo
+*.user
+*.db
+*.db-shm
+*.db-wal
+
diff --git a/docs/demo/.vscode/launch.json b/docs/demo/.vscode/launch.json
new file mode 100644
index 00000000..558e1798
--- /dev/null
+++ b/docs/demo/.vscode/launch.json
@@ -0,0 +1,35 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ // Use IntelliSense to find out which attributes exist for C# debugging
+ // Use hover for the description of the existing attributes
+ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
+ "name": ".NET Core Launch (web)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // If you have changed target frameworks, make sure to update the program path.
+ "program": "${workspaceFolder}/src/WTA/bin/Debug/net7.0/WTA.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/src/WTA",
+ "stopAtEntry": false,
+ // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
+ "serverReadyAction": {
+ "action": "openExternally",
+ "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
+ },
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "sourceFileMap": {
+ "/Views": "${workspaceFolder}/Views"
+ }
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/demo/.vscode/tasks.json b/docs/demo/.vscode/tasks.json
new file mode 100644
index 00000000..6fef5425
--- /dev/null
+++ b/docs/demo/.vscode/tasks.json
@@ -0,0 +1,41 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/src/WTA/WTA.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "publish",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/src/WTA/WTA.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "--project",
+ "${workspaceFolder}/src/WTA/WTA.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/demo/Demo.sln b/docs/demo/Demo.sln
new file mode 100644
index 00000000..85a99122
--- /dev/null
+++ b/docs/demo/Demo.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.6.33717.318
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WTA", "src\WTA\WTA.csproj", "{FFACA971-25FC-4652-A510-8BFC76E42D87}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WTA.Shared", "src\WTA.Shared\WTA.Shared.csproj", "{146358B1-1BD3-41B0-8D20-E613F422BA49}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WTA.Infrastructure", "src\WTA.Infrastructure\WTA.Infrastructure.csproj", "{8A09D8E5-2DB8-4741-BFBF-0542968F4D26}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WTA.Application", "src\WTA.Application\WTA.Application.csproj", "{82A8512D-456F-4849-BA9D-953B0C5E194A}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{F181D9F7-4185-4AE2-B32B-A57411029F69}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Packages.props = Directory.Packages.props
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {FFACA971-25FC-4652-A510-8BFC76E42D87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FFACA971-25FC-4652-A510-8BFC76E42D87}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FFACA971-25FC-4652-A510-8BFC76E42D87}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FFACA971-25FC-4652-A510-8BFC76E42D87}.Release|Any CPU.Build.0 = Release|Any CPU
+ {146358B1-1BD3-41B0-8D20-E613F422BA49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {146358B1-1BD3-41B0-8D20-E613F422BA49}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {146358B1-1BD3-41B0-8D20-E613F422BA49}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {146358B1-1BD3-41B0-8D20-E613F422BA49}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8A09D8E5-2DB8-4741-BFBF-0542968F4D26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8A09D8E5-2DB8-4741-BFBF-0542968F4D26}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8A09D8E5-2DB8-4741-BFBF-0542968F4D26}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8A09D8E5-2DB8-4741-BFBF-0542968F4D26}.Release|Any CPU.Build.0 = Release|Any CPU
+ {82A8512D-456F-4849-BA9D-953B0C5E194A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {82A8512D-456F-4849-BA9D-953B0C5E194A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {82A8512D-456F-4849-BA9D-953B0C5E194A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {82A8512D-456F-4849-BA9D-953B0C5E194A}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {53B37CFF-A4EE-43D2-97DE-F714E9F12F66}
+ EndGlobalSection
+EndGlobal
diff --git a/docs/demo/Directory.Packages.props b/docs/demo/Directory.Packages.props
new file mode 100644
index 00000000..98774428
--- /dev/null
+++ b/docs/demo/Directory.Packages.props
@@ -0,0 +1,39 @@
+
+
+ true
+ net7.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/demo/NuGet.Config b/docs/demo/NuGet.Config
new file mode 100644
index 00000000..2d29d123
--- /dev/null
+++ b/docs/demo/NuGet.Config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/docs/demo/README.md b/docs/demo/README.md
new file mode 100644
index 00000000..fed40760
--- /dev/null
+++ b/docs/demo/README.md
@@ -0,0 +1,37 @@
+# 说明
+
+## 模块划分
+
+1. 一个模块有多个数据库上下文
+1. 一个数据库上下文有多个实体配置和种子配置
+1. 每个实体配置和种子配置关联到一个数据上下文
+
+## 权限自动生成
+
+1. IResource 标记资源,IResourceService\ where TResource:IResource 标记服务
+1. Entity 默认实现 IResource 接口
+1. 泛型控制器 GenericControlle 默认实现 Entity 的增删改查导入导出操作,其他自定义操作可以继承泛型控制器
+1. 非 Entity 资源可以手动实现 IResource 和 IResourceService 接口
+
+## C# 到 JSON Schema 类型映射
+
+### 值类型
+
+1. bool?=>boolean[nullable]
+1. int/long?=>integer[nullable]
+1. float/double/decimal?=>number[nullable]
+1. Guid/DateTime/Enum?=>string[nullable]
+
+### 引用类型
+
+1. string=>string
+1. object=>object
+1. IEnumerable=>array
+
+## 格式化
+
+format 用于格式验证
+
+## 输入
+
+input 用于输入控件
diff --git a/docs/demo/src/WTA.Application/Captcha/CaptchaController.cs b/docs/demo/src/WTA.Application/Captcha/CaptchaController.cs
new file mode 100644
index 00000000..01eb1de6
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Captcha/CaptchaController.cs
@@ -0,0 +1,47 @@
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.OutputCaching;
+using Microsoft.Extensions.Caching.Distributed;
+using WTA.Shared.Captcha;
+
+namespace WTA.Application.Captcha;
+
+[Route("api/[controller]")]
+public class CaptchaController : Controller
+{
+ private readonly IDistributedCache _cache;
+ private readonly ICaptchaService _captchaService;
+
+ public CaptchaController(IDistributedCache cache, ICaptchaService captchaService)
+ {
+ this._cache = cache;
+ this._captchaService = captchaService;
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ [OutputCache(NoStore = true)]
+ public IActionResult Index()
+ {
+ var code = string.Empty;
+ var builder = new StringBuilder();
+ builder.Append(code);
+ for (var i = 0; i < 4; i++)
+ {
+ var random = new byte[1];
+ using var generator = RandomNumberGenerator.Create();
+ generator.GetBytes(random);
+ builder.Append(new Random(Convert.ToInt32(random[0])).Next(0, 9));
+ }
+ code = builder.ToString();
+ var key = Guid.NewGuid().ToString();
+ this._cache.SetString(key, code, new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(5) });
+ return Json(new
+ {
+ Captcha = this._captchaService.Create(code),
+ CaptchaKey = key
+ });
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Controllers/ConnectionStringController.cs b/docs/demo/src/WTA.Application/Identity/Controllers/ConnectionStringController.cs
new file mode 100644
index 00000000..62dd41dd
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Controllers/ConnectionStringController.cs
@@ -0,0 +1,14 @@
+using Microsoft.Extensions.Logging;
+using WTA.Application.Identity.Entities.Tenants;
+using WTA.Shared.Controllers;
+using WTA.Shared.Data;
+
+namespace WTA.Application.Identity.Controllers;
+
+public class ConnectionStringController : GenericController
+{
+ public ConnectionStringController(ILogger logger, IRepository repository) : base(logger, repository)
+ {
+ this.Repository.DisableTenantFilter();
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Controllers/PermissionController.cs b/docs/demo/src/WTA.Application/Identity/Controllers/PermissionController.cs
new file mode 100644
index 00000000..0cc95b22
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Controllers/PermissionController.cs
@@ -0,0 +1,14 @@
+using Microsoft.Extensions.Logging;
+using WTA.Application.Identity.Entities.SystemManagement;
+using WTA.Shared.Controllers;
+using WTA.Shared.Data;
+
+namespace WTA.Application.Identity.Controllers;
+
+public class PermissionController : GenericController
+{
+ public PermissionController(ILogger logger, IRepository repository) : base(logger, repository)
+ {
+ this.Repository.DisableTenantFilter();
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Controllers/TenantController.cs b/docs/demo/src/WTA.Application/Identity/Controllers/TenantController.cs
new file mode 100644
index 00000000..e0947994
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Controllers/TenantController.cs
@@ -0,0 +1,14 @@
+using Microsoft.Extensions.Logging;
+using WTA.Application.Identity.Entities.Tenants;
+using WTA.Shared.Controllers;
+using WTA.Shared.Data;
+
+namespace WTA.Application.Identity.Controllers;
+
+public class TenantController : GenericController
+{
+ public TenantController(ILogger logger, IRepository repository) : base(logger, repository)
+ {
+ this.Repository.DisableTenantFilter();
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Controllers/TokenController.cs b/docs/demo/src/WTA.Application/Identity/Controllers/TokenController.cs
new file mode 100644
index 00000000..5ebbe69c
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Controllers/TokenController.cs
@@ -0,0 +1,208 @@
+using System.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using WTA.Application.Identity.Entities.SystemManagement;
+using WTA.Application.Identity.Entities.Tenants;
+using WTA.Application.Identity.Models;
+using WTA.Shared.Application;
+using WTA.Shared.Authentication;
+using WTA.Shared.Controllers;
+using WTA.Shared.Data;
+using WTA.Shared.Extensions;
+using WTA.Shared.Identity;
+
+namespace WTA.Application.Identity.Controllers;
+
+[Route("api/{culture=zh}/[controller]")]
+[ApiExplorerSettings(GroupName = nameof(IdentityModule))]
+public class TokenController : BaseController, IResourceService
+{
+ private readonly TokenValidationParameters _tokenValidationParameters;
+ private readonly IdentityOptions _identityOptions;
+ private readonly IPasswordHasher _passwordHasher;
+ private readonly JwtSecurityTokenHandler _jwtSecurityTokenHandler;
+ private readonly SigningCredentials _credentials;
+ private readonly IRepository _tenantRepository;
+ private readonly IRepository _userRepository;
+
+ public TokenController(TokenValidationParameters tokenValidationParameters,
+ JwtSecurityTokenHandler jwtSecurityTokenHandler,
+ SigningCredentials credentials,
+ IOptions identityOptions,
+ IPasswordHasher passwordHasher,
+ IRepository _tenantRepository,
+ IRepository userRepository)
+ {
+ this._tokenValidationParameters = tokenValidationParameters;
+ this._jwtSecurityTokenHandler = jwtSecurityTokenHandler;
+ this._credentials = credentials;
+ this._identityOptions = identityOptions.Value;
+ this._passwordHasher = passwordHasher;
+ this._tenantRepository = _tenantRepository;
+ this._userRepository = userRepository;
+ this._userRepository.DisableTenantFilter();
+ }
+
+ [HttpGet("[action]")]
+ [AllowAnonymous]
+ public object Create()
+ {
+ return typeof(LoginRequestModel).GetViewModel();
+ }
+
+ [HttpPost("[action]")]
+ [AllowAnonymous]
+ public IActionResult Create([FromBody] LoginRequestModel model)
+ {
+ if (this.ModelState.IsValid)
+ {
+ try
+ {
+ var additionalClaims = new List();
+ if (model.TenantId != null)
+ {
+ if (this._tenantRepository.AsNoTracking().Any(o => o.Id == model.TenantId))
+ {
+ additionalClaims.Add(new Claim(nameof(model.TenantId), model.TenantId?.ToString()!));
+ }
+ else
+ {//租户不存在
+ this.ModelState.AddModelError(nameof(LoginRequestModel.TenantId), "租户不存在");
+ }
+ }
+ if (this.ModelState.IsValid)
+ {
+ var userQuery = this._userRepository.Queryable();
+ var user = userQuery.FirstOrDefault(o => o.UserName == model.UserName);
+ if (!this._identityOptions.SupportsUserLockout)
+ {//未启用登录锁定
+ if (user == null || user.PasswordHash != this._passwordHasher.HashPassword(model.Password, user.SecurityStamp!))
+ {
+ this.ModelState.AddModelError("", "用户名或密码错误");
+ }
+ }
+ else
+ {//启用登录锁定
+ if (user != null)
+ {
+ if (user.LockoutEnd.HasValue)
+ {//已锁定
+ if (DateTime.UtcNow > user.LockoutEnd.Value)
+ {//超时则解锁
+ user.AccessFailedCount = 0;
+ user.LockoutEnd = null;
+ this._userRepository.SaveChanges();
+ }
+ else
+ {//未超时禁止登录
+ var minutes = (user.LockoutEnd!.Value - DateTime.UtcNow).TotalMinutes.ToString("f0", CultureInfo.InvariantCulture);
+ this.ModelState.AddModelError(nameof(LoginRequestModel.UserName), $"用户已锁定,{minutes} 分钟后解除锁定");
+ }
+ }
+ if (!user.LockoutEnd.HasValue)
+ {//未锁定
+ if (user.PasswordHash != this._passwordHasher.HashPassword(model.Password, user.SecurityStamp!))
+ {//密码不正确
+ user.AccessFailedCount++;
+ if (user.AccessFailedCount >= this._identityOptions.MaxFailedAccessAttempts)
+ {//超过次数则锁定
+ user.LockoutEnd = DateTime.UtcNow + this._identityOptions.DefaultLockoutTimeSpan;
+ var minutes = (user.LockoutEnd!.Value - DateTime.UtcNow).TotalMinutes.ToString("f0", CultureInfo.InvariantCulture);
+ this.ModelState.AddModelError(nameof(LoginRequestModel.UserName), $"用户已锁定,{minutes} 分钟后解除锁定");
+ }
+ else
+ {//未超过次数提示剩余次数
+ this.ModelState.AddModelError(nameof(LoginRequestModel.Password), $"密码错误,{this._identityOptions.MaxFailedAccessAttempts - user.AccessFailedCount}次失败后将锁定用户");
+ }
+ this._userRepository.SaveChanges();
+ }
+ }
+ }
+ else
+ {
+ this.ModelState.AddModelError(nameof(LoginRequestModel.UserName), "用户不存在");
+ }
+ }
+ if (this.ModelState.IsValid)
+ {//验证成功
+ var roles = this._userRepository.AsNoTracking()
+ .Where(o => o.UserName == model.UserName)
+ .SelectMany(o => o.UserRoles)
+ .Select(o => o.Role.Name)
+ .ToList()
+ .Select(o => new Claim(this._tokenValidationParameters.RoleClaimType, o));
+ additionalClaims.AddRange(roles);
+ var subject = CreateSubject(model.UserName, additionalClaims);
+ return Json(new LoginResponseModel
+ {
+ AccessToken = CreateToken(subject, this._identityOptions.AccessTokenExpires),
+ RefreshToken = CreateToken(subject, model.RememberMe ? TimeSpan.FromDays(365) : this._identityOptions.RefreshTokenExpires),
+ ExpiresIn = (long)this._identityOptions.AccessTokenExpires.TotalSeconds
+ });
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.ToString());
+ return Problem(ex.ToString());
+ }
+ }
+ return BadRequest(this.ModelState.ToErrors());
+ }
+
+ [HttpPost("[action]")]
+ [AllowAnonymous]
+ public LoginResponseModel Refresh(string refreshToken)
+ {
+ var validationResult = this._jwtSecurityTokenHandler.ValidateTokenAsync(refreshToken, this._tokenValidationParameters).Result;
+ if (!validationResult.IsValid)
+ {
+ throw new Exception("RefreshToken验证失败", innerException: validationResult.Exception);
+ }
+ var subject = validationResult.ClaimsIdentity;
+ return new LoginResponseModel
+ {
+ AccessToken = CreateToken(subject, this._identityOptions.AccessTokenExpires),
+ RefreshToken = CreateToken(subject, validationResult.SecurityToken.ValidTo.Subtract(validationResult.SecurityToken.ValidFrom)),
+ ExpiresIn = (long)this._identityOptions.AccessTokenExpires.TotalSeconds
+ };
+ }
+
+ private ClaimsIdentity CreateSubject(string userName, List additionalClaims)
+ {
+ var claims = new List(additionalClaims) { new Claim(this._tokenValidationParameters.NameClaimType, userName) };
+ var subject = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
+ return subject;
+ }
+
+ private string CreateToken(ClaimsIdentity subject, TimeSpan timeout)
+ {
+ var now = DateTime.UtcNow;
+ var tokenDescriptor = new SecurityTokenDescriptor()
+ {
+ // 签发者
+ Issuer = this._identityOptions.Issuer,
+ // 接收方
+ Audience = this._identityOptions.Audience,
+ // 凭据
+ SigningCredentials = _credentials,
+ // 声明
+ Subject = subject,
+ // 签发时间
+ IssuedAt = now,
+ // 生效时间
+ NotBefore = now,
+ // UTC 过期时间
+ Expires = now.Add(timeout),
+ };
+ var securityToken = this._jwtSecurityTokenHandler.CreateJwtSecurityToken(tokenDescriptor);
+ var token = this._jwtSecurityTokenHandler.WriteToken(securityToken);
+ return token;
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Controllers/UserController.cs b/docs/demo/src/WTA.Application/Identity/Controllers/UserController.cs
new file mode 100644
index 00000000..ea2075b1
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Controllers/UserController.cs
@@ -0,0 +1,73 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using WTA.Application.Identity.Entities.SystemManagement;
+using WTA.Shared.Attributes;
+using WTA.Shared.Authentication;
+using WTA.Shared.Controllers;
+using WTA.Shared.Data;
+using WTA.Shared.Extensions;
+
+namespace WTA.Application.Identity.Controllers;
+
+[Implement]
+public class UserController : GenericController, IAuthenticationService
+{
+ public UserController(ILogger logger, IRepository repository) : base(logger, repository)
+ {
+ this.Repository.DisableTenantFilter();
+ }
+
+ [HttpPost, Hidden]
+ public AuthenticateResult Authenticate(string name, string operation)
+ {
+ var query = this.Repository.AsNoTracking();
+ var result = new AuthenticateResult
+ {
+ Succeeded = query.Any(o => o.UserName == name &&
+ o.UserRoles.Any(o => o.Role.RolePermissions.Any(o => o.Permission.Type == PermissionType.Operation && o.Permission.Number == operation)))
+ };
+ if (result.Succeeded)
+ {
+ var rolePermissions = query
+ .Where(o => o.UserName == name)
+ .SelectMany(o => o.UserRoles)
+ .Select(o => o.Role)
+ .SelectMany(o => o.RolePermissions)
+ .Where(o => o.Permission.Children.Any(p => p.Number == operation))
+ .ToList();
+ result.EnableColumnLimit = rolePermissions.Any(o => o.EnableColumnLimit);
+ if (result.EnableColumnLimit)
+ {
+ result.Columns = rolePermissions.Where(o => o.EnableColumnLimit).SelectMany(o => o.Columns).Distinct().ToList();
+ }
+ result.EnableRowLimit = rolePermissions.Any(o => o.EnableRowLimit);
+ if (result.EnableRowLimit)
+ {
+ result.Rows = rolePermissions.Where(o => o.EnableColumnLimit).SelectMany(o => o.Rows).ToList();
+ }
+ }
+ return result;
+ }
+
+ [HttpPost, Hidden]
+ [Display(Name = "用户信息")]
+ public User? Info()
+ {
+ var user = this.Repository
+ .AsNoTracking()
+ .Include(o => o.Department)
+ .Include(o => o.UserRoles)
+ .ThenInclude(o => o.Role)
+ .ThenInclude(o => o.RolePermissions)
+ .ThenInclude(o => o.Permission)
+ .FirstOrDefault(o => o.UserName == this.User.Identity!.Name);
+ if (user != null)
+ {
+ user.SecurityStamp = string.Empty;
+ user.PasswordHash = string.Empty;
+ }
+ return user!;
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Data/IdentityConfiguration.cs b/docs/demo/src/WTA.Application/Identity/Data/IdentityConfiguration.cs
new file mode 100644
index 00000000..24ac70f3
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Data/IdentityConfiguration.cs
@@ -0,0 +1,90 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using WTA.Application.Identity.Entities.SystemManagement;
+using WTA.Application.Identity.Entities.Tenants;
+using WTA.Application.Monitor.Entities;
+using WTA.Shared.Extensions;
+
+namespace WTA.Shared.Data.Config;
+
+public class IdentityConfiguration : IDbConfig, IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration,
+ IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasIndex(o => o.Name).IsUnique();
+ builder.HasIndex(o => o.Number).IsUnique();
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasOne(o => o.Parent).WithMany(o => o.ConnectionStrings).HasForeignKey(o => o.ParentId).OnDelete(DeleteBehavior.Cascade);
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasOne(o => o.Post).WithMany(o => o.Users).HasForeignKey(o => o.PostId).OnDelete(DeleteBehavior.SetNull);
+ builder.HasOne(o => o.Department).WithMany(o => o.Users).HasForeignKey(o => o.DepartmentId).OnDelete(DeleteBehavior.SetNull);
+ builder.HasAlternateKey(o => o.UserName);
+ builder.HasIndex(o => o.NormalizedUserName).IsUnique();
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasAlternateKey(o => o.Number);
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.Property(o => o.Type).IsRequired();
+ builder.Property(o => o.Columns).HasConversion(p => p.ToJson(), p => p.FromJson>()!);
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasOne(o => o.User).WithMany(o => o.UserRoles).HasForeignKey(o => o.UserId).OnDelete(DeleteBehavior.Cascade);
+ builder.HasOne(o => o.Role).WithMany(o => o.UserRoles).HasForeignKey(o => o.RoleId).OnDelete(DeleteBehavior.Cascade);
+ builder.HasAlternateKey(o => new { o.UserId, o.RoleId });
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasOne(o => o.Role).WithMany(o => o.RolePermissions).HasForeignKey(o => o.RoleId).OnDelete(DeleteBehavior.Cascade);
+ builder.HasOne(o => o.Permission).WithMany(o => o.RolePermissions).HasForeignKey(o => o.PermissionId).OnDelete(DeleteBehavior.Cascade);
+ builder.HasAlternateKey(o => new { o.RoleId, o.PermissionId });
+ builder.Property(o => o.Columns).HasConversion(p => p.ToJson(), p => p.FromJson>()!);
+ builder.Property(o => o.Rows).HasConversion(p => p.ToJson(), p => p.FromJson>()!);
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasAlternateKey(o => o.Number);
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ }
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasAlternateKey(o => o.ConnectionId);
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Data/IdentityDbContext.cs b/docs/demo/src/WTA.Application/Identity/Data/IdentityDbContext.cs
new file mode 100644
index 00000000..776c4849
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Data/IdentityDbContext.cs
@@ -0,0 +1,12 @@
+using Microsoft.EntityFrameworkCore;
+using WTA.Shared.Attributes;
+
+namespace WTA.Shared.Data;
+
+[IgnoreMultiTenancy]
+public class IdentityDbContext : BaseDbContext
+{
+ public IdentityDbContext(DbContextOptions options) : base(options)
+ {
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Data/IdentityDbSeed.cs b/docs/demo/src/WTA.Application/Identity/Data/IdentityDbSeed.cs
new file mode 100644
index 00000000..53d36ea6
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Data/IdentityDbSeed.cs
@@ -0,0 +1,257 @@
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using LinqToDB.Extensions;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.EntityFrameworkCore;
+using WTA.Application.Identity.Entities.SystemManagement;
+using WTA.Application.Identity.Entities.Tenants;
+using WTA.Application.Monitor.Entities;
+using WTA.Shared;
+using WTA.Shared.Application;
+using WTA.Shared.Attributes;
+using WTA.Shared.Data;
+using WTA.Shared.Extensions;
+using WTA.Shared.Identity;
+
+namespace WTA.Application.Identity.Data;
+
+public class IdentityDbSeed : IDbSeed
+{
+ private readonly IPasswordHasher _passwordHasher;
+ private readonly IActionDescriptorCollectionProvider _actionProvider;
+
+ public IdentityDbSeed(IPasswordHasher passwordHasher, IActionDescriptorCollectionProvider actionProvider)
+ {
+ this._passwordHasher = passwordHasher;
+ this._actionProvider = actionProvider;
+ }
+
+ public void Seed(IdentityDbContext context)
+ {
+ //禁用多租户和软删除过滤器
+ context.DisableTenantFilter = true; ;
+ context.DisableSoftDeleteFilter = true;
+
+ // 定时器初始化
+ context.Set().Add(new JobItem { Name = "客户端监测", Cron = "* * * * *", Service = "WTA.Application.Monitor.UserLoginSrevice" });
+
+ // 数据字典初始化
+ InitDictionaries(context);
+
+ //租户初始化
+ var tenant = new Tenant
+ {
+ Name = "默认",
+ Number = "default",
+ DataBaseCreated = false,
+ ConnectionStrings = new List
+ {
+ new ConnectionString(){ Name="Identity",Value="Data Source=data2.db" }
+ }
+ }.SetIdBy(o => o.Number);
+ context.Set().Add(tenant);
+ var defaultTenantId = tenant.Id;
+
+ // 权限初始化
+ InitPermissions(context);
+
+ //部门初始化
+ context.Set().Add(
+ new Department
+ {
+ Name = "企业",
+ Number = "Enterprise",
+ Children = new List
+ {
+ new Department {
+ Name="总经办",
+ Number="Root",
+ Children=new List
+ {
+ new Department
+ {
+ Name="部门",
+ Number="Department"
+ }
+ }
+ }
+ }
+ }.UpdateId()
+ .UpdatePath());
+
+ // 角色初始化
+ var superRole = new Role
+ {
+ IsReadonly = true,
+ Name = "超级管理员角色",
+ Number = "super",
+ }.SetIdBy(o => o.Number);
+ context.Set().ToList().ForEach(o => superRole.RolePermissions.Add(new RolePermission
+ {
+ IsReadonly = true,
+ RoleId = superRole.Id,
+ PermissionId = o.Id,
+ EnableColumnLimit = false,
+ Columns = o.Columns.Keys.ToList(),
+ // EnableRowLimit = true,
+ // RowLimit = "CreatorId = {User.Id}"
+ }.SetIdBy(o => new { o.RoleId, o.PermissionId })));
+ context.Set().Add(superRole);
+ context.SaveChanges();
+
+ // 用户初始化
+ var superUser = new User
+ {
+ IsReadonly = true,
+ UserName = "super",
+ NormalizedUserName = "super".Normalize(),
+ Name = "超级管理员",
+ SecurityStamp = "123456",
+ PasswordHash = this._passwordHasher.HashPassword("123456", "123456"),
+ Properties = new Dictionary { { "key1", "value1" } }
+ }.SetIdBy(o => o.UserName);
+ superUser.UserRoles.Add(new UserRole { IsReadonly = true, UserId = superUser.Id, RoleId = superRole.Id }.SetIdBy(o => new { o.UserId, o.RoleId }));
+ context.Set().Add(superUser);
+ }
+
+ private void InitDictionaries(DbContext context)
+ {
+ }
+
+ private void InitPermissions(DbContext context)
+ {
+ //var controllerFeature = new ControllerFeature();
+ //this._partManager.PopulateFeature(controllerFeature);
+ //var controllerTypeInfos = controllerFeature.Controllers;
+ var actionDescriptors = this._actionProvider.ActionDescriptors.Items;
+
+ context.Set().Add(new Permission
+ {
+ IsReadonly = true,
+ Type = PermissionType.Resource,
+ Name = "首页",
+ Number = "home",
+ Path = "home",
+ Method = "POST",
+ Component = "home",
+ Icon = "home",
+ Order = -3,
+ }.UpdateId().UpdatePath());
+
+ //context.Set().Add(new Permission
+ //{
+ // IsExternal = true,
+ // IsReadonly = true,
+ // Type = PermissionType.Resource,
+ // Name = "帮助",
+ // Number = "help",
+ // Path = "https://element-plus.org/",
+ // Method = "GET",
+ // Icon = "ep-link",
+ // Order = 1000,
+ //}.UpdateId().UpdatePath());
+
+ WebApp.Current.Assemblies.SelectMany(o => o.GetTypes()).Where(o => o.IsClass && !o.IsAbstract && o.IsAssignableTo(typeof(IResource))).ForEach(resourceType =>
+ {
+ // 获取列
+ var columns = resourceType.GetProperties()
+ //.Where(o => o.PropertyType.IsValueType || o.PropertyType == typeof(string))
+ .OrderBy(o => o.GetCustomAttribute()?.GetOrder())
+ .ToDictionary(o => o.Name, o => o.GetDisplayName());
+ // 创建资源权限
+ var resourcePermission = new Permission
+ {
+ IsReadonly = true,
+ Type = PermissionType.Resource,
+ Name = resourceType.GetDisplayName(),
+ Number = resourceType.Name,
+ Path = resourceType.Name.ToSlugify(),
+ Component = resourceType.GetCustomAttribute()?.Component,
+ IsHidden = resourceType.HasAttribute(),
+ Icon = resourceType.GetCustomAttribute()?.Icon ?? IconAttribute.File,
+ Order = resourceType.GetCustomAttribute()?.Order ?? OrderAttribute.Default,
+ Columns = columns
+ }.UpdateId();
+ // 创建按钮权限
+ var resourceServiceType = typeof(IResourceService<>).MakeGenericType(resourceType);
+ actionDescriptors
+ .Select(o => o as ControllerActionDescriptor)
+ .Where(o => o != null && o.ControllerTypeInfo.AsType().IsAssignableTo(resourceServiceType))
+ .ForEach(actionDescriptor =>
+ {
+ var operation = actionDescriptor?.ActionName!;
+ var methodInfo = actionDescriptor?.MethodInfo!;
+ var method = (methodInfo.GetCustomAttributes().FirstOrDefault(o => o.GetType().IsAssignableTo(typeof(HttpMethodAttribute)))
+ as HttpMethodAttribute)?.HttpMethods?.FirstOrDefault() ?? "POST";
+ if (method != "GET")
+ {
+ resourcePermission.Children.Add(new Permission
+ {
+ IsReadonly = true,
+ Type = PermissionType.Operation,
+ Name = methodInfo.GetDisplayName(),
+ Number = $"{actionDescriptor?.ControllerName}.{operation}",
+ Path = $"{operation.TrimEnd("Async").ToSlugify()}",
+ IsHidden = methodInfo.GetCustomAttributes().Any(),
+ Method = method,
+ Icon = methodInfo.GetCustomAttribute()?.Icon ?? $"{operation.TrimEnd("Async").ToSlugify()}",
+ Order = methodInfo.GetCustomAttribute()?.Order ?? OrderAttribute.Default,
+ HtmlClass = methodInfo.GetCustomAttribute()?.Class ?? HtmlClassAttribute.Default,
+ IsTop = methodInfo.GetCustomAttribute() != null
+ }.UpdateId());
+ }
+ });
+ // 实体分组
+ var groupAttribute = resourceType.GetCustomAttributes().FirstOrDefault(o => o.GetType().IsAssignableTo(typeof(GroupAttribute)));
+ if (groupAttribute != null)
+ {
+ var groupNumber = groupAttribute.GetType().Name;
+ var groupPermission = context.Set().FirstOrDefault(o => o.Number == groupAttribute.GetType().Name);
+ groupPermission ??= new Permission
+ {
+ Type = PermissionType.Group,
+ Name = groupAttribute.GetType().GetDisplayName(),
+ Number = groupNumber,
+ Path = $"{groupAttribute.GetType().Name.TrimEnd("Attribute").ToSlugify()}",
+ IsHidden = groupAttribute.GetType().HasAttribute(),
+ Icon = groupAttribute.GetType().GetCustomAttribute()?.Icon ?? IconAttribute.Folder,
+ Order = groupAttribute.GetType().GetCustomAttribute()?.Order ?? OrderAttribute.Default
+ }.UpdateId();
+ groupPermission.Children.Add(resourcePermission);
+ var moduleType = groupAttribute.GetType().GetCustomAttributes()
+ .Where(o => o.GetType().IsGenericType && o.GetType().GetGenericTypeDefinition() == typeof(ModuleAttribute<>))
+ .Select(o => o as ITypeAttribute).Select(o => o?.Type).FirstOrDefault();
+ if (moduleType != null)
+ {
+ var modulePermission = context.Set().FirstOrDefault(o => o.Number == moduleType.Name);
+ if (modulePermission == null)
+ {
+ modulePermission = new Permission
+ {
+ Type = PermissionType.Module,
+ Name = moduleType.GetDisplayName(),
+ Number = moduleType.Name,
+ Path = moduleType.Name.TrimEnd("Module").ToSlugify(),
+ IsHidden = moduleType.HasAttribute(),
+ Icon = moduleType.GetCustomAttribute()?.Icon ?? IconAttribute.Folder,
+ Order = moduleType.GetCustomAttribute()?.Order ?? OrderAttribute.Default
+ }.UpdateId();
+ context.Set().Add(modulePermission.UpdatePath());
+ }
+ modulePermission.Children.Add(groupPermission);
+ }
+ else
+ {
+ context.Set().AddOrUpdate(groupPermission.UpdatePath());
+ }
+ }
+ else
+ {
+ context.Set().Add(resourcePermission.UpdatePath());
+ }
+ context.SaveChanges();
+ });
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/BaseData/Class1.cs b/docs/demo/src/WTA.Application/Identity/Entities/BaseData/Class1.cs
new file mode 100644
index 00000000..023ac67a
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/BaseData/Class1.cs
@@ -0,0 +1,642 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using WTA.Shared.Attributes;
+using WTA.Shared.Data;
+using WTA.Shared.Domain;
+using WTA.Shared.Module;
+
+namespace WTA.Application.Identity.Entities.BaseData;
+
+[Order(7)]
+[SystemManagement]
+[Display(Name = "物料主数据")]
+public class Class1 : BaseEntity
+{
+}
+
+[Order(8)]
+[SystemManagement]
+[Display(Name = "客户零件关系")]
+public class Class2 : BaseEntity
+{
+}
+
+[Order(9)]
+[SystemManagement]
+[Display(Name = "客户端替换件关系")]
+public class Class3 : BaseEntity
+{
+}
+
+[Order(10)]
+[SystemManagement]
+[Display(Name = "寄售库出库总成替换关系")]
+public class Class4 : BaseEntity
+{
+}
+
+[Order(11)]
+[SystemManagement]
+[Display(Name = "期间设置")]
+public class Class5 : BaseEntity
+{
+}
+
+[Order(12)]
+[SystemManagement]
+[Display(Name = "销售价格单")]
+public class Class6 : BaseEntity
+{
+}
+
+[Order(2)]
+[Display(Name = "EDI业务")]
+public class EdiAttribute : GroupAttribute
+{
+}
+
+[Order(1)]
+[Edi]
+[Display(Name = "EDI和HBPO核对")]
+public class Class7 : BaseEntity
+{
+}
+
+[Order(2)]
+[Edi]
+[Display(Name = "EDI和BBAC核对")]
+public class Class8 : BaseEntity
+{
+}
+
+[Order(2)]
+[Display(Name = "JIS业务")]
+public class JISModule : BaseModule
+{
+}
+
+[Order(1)]
+[Module]
+[Display(Name = "数据输入")]
+public class JISDataInputAttribute : GroupAttribute
+{
+}
+
+[Order(2)]
+[Module]
+[Display(Name = "数据输出")]
+public class JISDataOutputAttribute : GroupAttribute
+{
+}
+
+[Order(1)]
+[JISDataInput]
+[Display(Name = "HBPO结算导入")]
+public class Class9 : BaseEntity
+{
+}
+
+[Order(2)]
+[JISDataInput]
+[Display(Name = "BBAC结算导入")]
+public class Class10 : BaseEntity
+{
+}
+
+[Order(3)]
+[JISDataInput]
+[Display(Name = "HBPO发运数据")]
+public class Class11 : BaseEntity
+{
+}
+
+[Order(4)]
+[JISDataInput]
+[Display(Name = "BBAC发运数据")]
+public class Class12 : BaseEntity
+{
+}
+
+[Order(1)]
+[JISDataOutput]
+[Display(Name = "HBPO结算核对明细输出")]
+public class Class13 : BaseEntity
+{
+}
+
+[Order(2)]
+[JISDataOutput]
+[Display(Name = "BBAC结算核对明细输出")]
+public class Class14 : BaseEntity
+{
+}
+
+[Order(3)]
+[JISDataOutput]
+[Display(Name = "HBPO无法出库明细与汇总输出")]
+public class Class15 : BaseEntity
+{
+}
+
+[Order(4)]
+[JISDataOutput]
+[Display(Name = "BBAC无法出库明细与汇总输出")]
+public class Class16 : BaseEntity
+{
+}
+
+[Order(5)]
+[JISDataOutput]
+[Display(Name = "HBPO结算发货明细与汇总")]
+public class Class17 : BaseEntity
+{
+}
+
+[Order(6)]
+[JISDataOutput]
+[Display(Name = "BBAC结算发货明细与汇总")]
+public class Class18 : BaseEntity
+{
+}
+
+/////
+
+[Order(3)]
+[Display(Name = "JIT业务")]
+public class JITModule : BaseModule
+{
+}
+
+[Order(1)]
+[Module]
+[Display(Name = "数据输入")]
+public class JITDataInputAttribute : GroupAttribute
+{
+}
+
+[Order(2)]
+[Module]
+[Display(Name = "数据输出")]
+public class JITDataOutputAttribute : GroupAttribute
+{
+}
+
+[Order(1)]
+[JITDataInput]
+[Display(Name = "JIT件结算导入")]
+public class Class19 : BaseEntity
+{
+}
+
+[Order(2)]
+[JITDataInput]
+[Display(Name = "JIT发运数据查询")]
+public class Class20 : BaseEntity
+{
+}
+
+[Order(1)]
+[JITDataOutput]
+[Display(Name = "JIT件结算核对明细输出")]
+public class Class21 : BaseEntity
+{
+}
+
+[Order(2)]
+[JITDataOutput]
+[Display(Name = "JIT件寄售库不能出库明细与汇总")]
+public class Class22 : BaseEntity
+{
+}
+
+[Order(3)]
+[JITDataOutput]
+[Display(Name = "JIT件结算发货明细与汇总")]
+public class Class23 : BaseEntity
+{
+}
+
+/////
+
+[Order(4)]
+[Display(Name = "备件业务")]
+public class BeiJianModule : BaseModule
+{
+}
+
+[Order(1)]
+[Module]
+[Display(Name = "数据输入")]
+public class BeiJianDataInputAttribute : GroupAttribute
+{
+}
+
+[Order(2)]
+[Module]
+[Display(Name = "数据输出")]
+public class BeiJianDataOutputAttribute : GroupAttribute
+{
+}
+
+[Order(1)]
+[BeiJianDataInput]
+[Display(Name = "备件结算导入")]
+public class Class24 : BaseEntity
+{
+}
+
+[Order(2)]
+[BeiJianDataInput]
+[Display(Name = "备件发运数据查询")]
+public class Class25 : BaseEntity
+{
+}
+
+[Order(1)]
+[BeiJianDataOutput]
+[Display(Name = "备件结算核对明细输出")]
+public class Class26 : BaseEntity
+{
+}
+
+[Order(2)]
+[BeiJianDataOutput]
+[Display(Name = "备件寄售库不能出库明细与汇总输出")]
+public class Class27 : BaseEntity
+{
+}
+
+[Order(3)]
+[BeiJianDataOutput]
+[Display(Name = "备件有结算有发货明细与汇总输出")]
+public class Class28 : BaseEntity
+{
+}
+
+[Order(4)]
+[BeiJianDataOutput]
+[Display(Name = "备件有结算无发货明细与汇总输出")]
+public class Class29 : BaseEntity
+{
+}
+
+/////
+
+[Order(5)]
+[Display(Name = "备件业务")]
+public class MaiDanJianModule : BaseModule
+{
+}
+
+[Order(1)]
+[Module]
+[Display(Name = "数据输入")]
+public class MaiDanJianDataInputAttribute : GroupAttribute
+{
+}
+
+[Order(2)]
+[Module]
+[Display(Name = "数据输出")]
+public class MaiDanJianDataOutputAttribute : GroupAttribute
+{
+}
+
+[Order(1)]
+[MaiDanJianDataInput]
+[Display(Name = "印度件结算导入")]
+public class Class30 : BaseEntity
+{
+}
+
+[Order(2)]
+[MaiDanJianDataInput]
+[Display(Name = "印度件发运数据查询")]
+public class Class31 : BaseEntity
+{
+}
+
+[Order(1)]
+[MaiDanJianDataOutput]
+[Display(Name = "印度件结算核对明细输出")]
+public class Class32 : BaseEntity
+{
+}
+
+[Order(2)]
+[MaiDanJianDataOutput]
+[Display(Name = "印度件寄售库不能出库明细与汇总输出")]
+public class Class33 : BaseEntity
+{
+}
+
+[Order(3)]
+[MaiDanJianDataOutput]
+[Display(Name = "印度件有结算有发货明细与汇总输出")]
+public class Class34 : BaseEntity
+{
+}
+
+[Order(4)]
+[MaiDanJianDataOutput]
+[Display(Name = "印度件有结算无发货明细与汇总输出")]
+public class Class35 : BaseEntity
+{
+}
+
+//////
+[Order(6)]
+[Display(Name = "出库单")]
+public class ChuKuDanGroup : GroupAttribute
+{
+}
+
+[Order(1)]
+[ChuKuDanGroup]
+[Display(Name = "HBPO-JIS出库单")]
+public class Class36 : BaseEntity
+{
+}
+
+[Order(2)]
+[ChuKuDanGroup]
+[Display(Name = "BBAC-JIS出库单")]
+public class Class37 : BaseEntity
+{
+}
+
+[Order(3)]
+[ChuKuDanGroup]
+[Display(Name = "JIT件件出库单")]
+public class Class38 : BaseEntity
+{
+}
+
+[Order(4)]
+[ChuKuDanGroup]
+[Display(Name = "备件出库单")]
+public class Class39 : BaseEntity
+{
+}
+
+[Order(5)]
+[ChuKuDanGroup]
+[Display(Name = "印度件出库单")]
+public class Class40 : BaseEntity
+{
+}
+
+[Order(6)]
+[ChuKuDanGroup]
+[Display(Name = "不能出库记录出库业务")]
+public class Class41 : BaseEntity
+{
+}
+
+//////
+[Order(7)]
+[Display(Name = "商务审核")]
+public class ShangWuShenHeGroup : GroupAttribute
+{
+}
+
+[Order(1)]
+[ShangWuShenHeGroup]
+[Display(Name = "HBPO-JIS 商务待开票")]
+public class Class42 : BaseEntity
+{
+}
+
+[Order(2)]
+[ShangWuShenHeGroup]
+[Display(Name = "BBAC-JIS商务待开票")]
+public class Class43 : BaseEntity
+{
+}
+
+[Order(3)]
+[ShangWuShenHeGroup]
+[Display(Name = "JIT件商务发票待开票")]
+public class Class44 : BaseEntity
+{
+}
+
+[Order(4)]
+[ShangWuShenHeGroup]
+[Display(Name = "备件商务发票待开票")]
+public class Class45 : BaseEntity
+{
+}
+
+[Order(5)]
+[ShangWuShenHeGroup]
+[Display(Name = "印度件商务发票待开票")]
+public class Class46 : BaseEntity
+{
+}
+
+//////
+[Order(7)]
+[Display(Name = "财务审核")]
+public class CaiWuShenHeGroup : GroupAttribute
+{
+}
+
+[Order(1)]
+[CaiWuShenHeGroup]
+[Display(Name = " BBAC-JIS财务管理审核")]
+public class Class47 : BaseEntity
+{
+}
+
+public class BaseDataDbConfig : IDbConfig,
+ IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration,
+IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+
+ public void Configure(EntityTypeBuilder builder)
+ { }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Department.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Department.cs
new file mode 100644
index 00000000..fe4b2ed6
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Department.cs
@@ -0,0 +1,12 @@
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[Hidden]
+[Order(4)]
+[SystemManagement]
+public class Department : BaseTreeEntity
+{
+ public List Users { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/DictionaryItem.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/DictionaryItem.cs
new file mode 100644
index 00000000..d926a0bc
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/DictionaryItem.cs
@@ -0,0 +1,10 @@
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[Order(6)]
+[SystemManagement]
+public class DictionaryItem : BaseTreeEntity
+{
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Permission.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Permission.cs
new file mode 100644
index 00000000..64a5c933
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Permission.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations;
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[Order(3)]
+[SystemManagement]
+public class Permission : BaseTreeEntity
+{
+ [Required]
+ public PermissionType? Type { get; set; }
+
+ [Required]
+ public bool? IsHidden { get; set; } = false;
+
+ [Required]
+ public bool? IsExternal { get; set; } = false;
+
+ public string? Path { get; set; }
+ public string? Method { get; set; }
+ public string? Component { get; set; }
+ public string? Redirect { get; set; }
+ public string? Icon { get; set; }
+ public string? HtmlClass { get; set; }
+
+ [Required]
+ public bool? IsTop { get; set; } = false;
+
+ public Dictionary Columns { get; set; } = new Dictionary();
+ public List RolePermissions { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/PermissionType.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/PermissionType.cs
new file mode 100644
index 00000000..1eebdcbe
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/PermissionType.cs
@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+public enum PermissionType
+{
+ [Display(Name = "模块")]
+ Module = 10,
+
+ [Display(Name = "分组")]
+ Group = 20,
+
+ [Display(Name = "资源")]
+ Resource = 30,
+
+ [Display(Name = "操作")]
+ Operation = 40
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Post.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Post.cs
new file mode 100644
index 00000000..9f196856
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Post.cs
@@ -0,0 +1,14 @@
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[Hidden]
+[Order(5)]
+[SystemManagement]
+public class Post : BaseEntity
+{
+ public string Name { get; set; } = null!;
+ public string Number { get; set; } = null!;
+ public List Users { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Role.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Role.cs
new file mode 100644
index 00000000..c13eb638
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Role.cs
@@ -0,0 +1,14 @@
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[Order(2)]
+[SystemManagement]
+public class Role : BaseEntity
+{
+ public string Name { get; set; } = null!;
+ public string Number { get; set; } = null!;
+ public List UserRoles { get; set; } = new List();
+ public List RolePermissions { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/RolePermission.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/RolePermission.cs
new file mode 100644
index 00000000..ddb9a915
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/RolePermission.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[Hidden]
+[Display(Name = "角色权限")]
+[SystemManagement]
+public class RolePermission : BaseEntity
+{
+ public Guid RoleId { get; set; }
+ public Guid PermissionId { get; set; }
+ public Role Role { get; set; } = null!;
+ public Permission Permission { get; set; } = null!;
+ public bool EnableColumnLimit { get; internal set; }
+ public List Columns { get; set; } = new List();
+ public bool EnableRowLimit { get; set; }
+ public List Rows { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Token.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Token.cs
new file mode 100644
index 00000000..a76340fb
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/Token.cs
@@ -0,0 +1,9 @@
+using WTA.Shared.Application;
+using WTA.Shared.Attributes;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[SystemManagement, Hidden]
+public class Token : IResource
+{
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/User.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/User.cs
new file mode 100644
index 00000000..ccfea3e4
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/User.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel.DataAnnotations;
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[Order(1)]
+[SystemManagement]
+public class User : BaseEntity
+{
+ public string UserName { get; set; } = null!;
+
+ [ScaffoldColumn(false)]
+ public string NormalizedUserName { get; set; } = null!;
+
+ public string Name { get; set; } = null!;
+
+ [ScaffoldColumn(false)]
+ public string SecurityStamp { get; set; } = null!;
+
+ [ScaffoldColumn(false)]
+ public string PasswordHash { get; set; } = null!;
+
+ public int AccessFailedCount { get; set; }
+ public DateTime? LockoutEnd { get; set; }
+
+ [Navigation]
+ public Guid? DepartmentId { get; set; }
+
+ public Guid? PostId { get; set; }
+
+ public Department? Department { get; set; }
+ public Post? Post { get; set; }
+
+ public List UserRoles { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/UserRole.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/UserRole.cs
new file mode 100644
index 00000000..ca41dd6c
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagement/UserRole.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.SystemManagement;
+
+[Hidden]
+[Display(Name = "用户角色")]
+[SystemManagement]
+public class UserRole : BaseEntity
+{
+ public Guid UserId { get; set; }
+ public Guid RoleId { get; set; }
+ public User User { get; set; } = null!;
+ public Role Role { get; set; } = null!;
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/SystemManagementAttribute.cs b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagementAttribute.cs
new file mode 100644
index 00000000..9710a9de
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/SystemManagementAttribute.cs
@@ -0,0 +1,8 @@
+using WTA.Shared.Attributes;
+
+namespace WTA.Application.Identity.Entities;
+
+[Order(1)]
+public class SystemManagementAttribute : GroupAttribute
+{
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/Tenants/ConnectionString.cs b/docs/demo/src/WTA.Application/Identity/Entities/Tenants/ConnectionString.cs
new file mode 100644
index 00000000..13aaeb39
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/Tenants/ConnectionString.cs
@@ -0,0 +1,17 @@
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.Tenants;
+
+[Hidden]
+[Tenants]
+public class ConnectionString : BaseEntity
+{
+ public string Name { get; set; } = null!;
+ public string Value { get; set; } = null!;
+
+ [Navigation]
+ public Guid? ParentId { get; set; }
+
+ public Tenant? Parent { get; set; }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/Tenants/Tenant.cs b/docs/demo/src/WTA.Application/Identity/Entities/Tenants/Tenant.cs
new file mode 100644
index 00000000..2bdfadd9
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/Tenants/Tenant.cs
@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Identity.Entities.Tenants;
+
+[Hidden]
+[Tenants]
+public class Tenant : BaseEntity
+{
+ public string Name { get; set; } = null!;
+ public string Number { get; set; } = null!;
+
+ [Required]
+ public bool? DataBaseCreated { get; set; }
+
+ public List ConnectionStrings { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Entities/TenantsAttribute.cs b/docs/demo/src/WTA.Application/Identity/Entities/TenantsAttribute.cs
new file mode 100644
index 00000000..ab9bf2fb
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Entities/TenantsAttribute.cs
@@ -0,0 +1,8 @@
+using WTA.Shared.Attributes;
+
+namespace WTA.Application.Identity.Entities;
+
+//[Module]
+public class TenantsAttribute : GroupAttribute
+{
+}
diff --git a/docs/demo/src/WTA.Application/Identity/IdentityModule.cs b/docs/demo/src/WTA.Application/Identity/IdentityModule.cs
new file mode 100644
index 00000000..531c4f39
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/IdentityModule.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Builder;
+using WTA.Application.Monitor.Entities;
+using WTA.Shared;
+using WTA.Shared.Attributes;
+using WTA.Shared.Module;
+
+namespace WTA.Application.Identity;
+
+[Order(1)]
+public class IdentityModule : BaseModule
+{
+ public override void Configure(WebApplication app)
+ {
+ WebApp.Current.UseScheduler(app);
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Models/LoginRequestModel.cs b/docs/demo/src/WTA.Application/Identity/Models/LoginRequestModel.cs
new file mode 100644
index 00000000..240a24f3
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Models/LoginRequestModel.cs
@@ -0,0 +1,40 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace WTA.Application.Identity.Models;
+
+public class LoginRequestModel// : IValidatableObject
+{
+ [ScaffoldColumn(false)]
+ [UIHint("select")]
+ public Guid? TenantId { get; set; } = null!;
+
+ [MaxLength(64)]
+ public string UserName { get; set; } = null!;
+
+ [MaxLength(64)]
+ [DataType(DataType.Password)]
+ public string Password { get; set; } = null!;
+
+ //[ScaffoldColumn(false)]
+ //public string CaptchaKey { get; set; } = null!;
+
+ //[UIHint("captcha")]
+ //public string Captcha { get; set; } = null!;
+
+ public bool RememberMe { get; set; }
+
+ //public IEnumerable Validate(ValidationContext validationContext)
+ //{
+ // using var scope = WebApp.Current.Services.CreateScope();
+ // var cache = scope.ServiceProvider.GetRequiredService();
+ // var code = cache.GetString(this.CaptchaKey);
+ // if (code == null)
+ // {
+ // yield return new ValidationResult("CaptchaExpired", new string[] { "Captcha" });
+ // }
+ // else if (code != this.Captcha)
+ // {
+ // yield return new ValidationResult("CaptchaError", new string[] { "Captcha" });
+ // }
+ //}
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Models/LoginResponseModel.cs b/docs/demo/src/WTA.Application/Identity/Models/LoginResponseModel.cs
new file mode 100644
index 00000000..cb00d5b4
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Models/LoginResponseModel.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace WTA.Application.Identity.Models;
+
+public class LoginResponseModel
+{
+ [JsonPropertyName("token_type")]
+ public string TokenType = "Bearer";
+
+ [JsonPropertyName("access_token")]
+ public string? AccessToken { get; set; }
+
+ [JsonPropertyName("refresh_token")]
+ public string? RefreshToken { get; set; }
+
+ [JsonPropertyName("expires_in")]
+ public long? ExpiresIn { get; set; }
+}
diff --git a/docs/demo/src/WTA.Application/Identity/Models/UserInfoModel.cs b/docs/demo/src/WTA.Application/Identity/Models/UserInfoModel.cs
new file mode 100644
index 00000000..2e737a7b
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/Models/UserInfoModel.cs
@@ -0,0 +1,9 @@
+using WTA.Application.Identity.Entities.SystemManagement;
+
+namespace WTA.Application.Identity.Models;
+
+public class UserInfoModel
+{
+ public User User { get; set; } = null!;
+ public List Permissions { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Application/Identity/TenantService.cs b/docs/demo/src/WTA.Application/Identity/TenantService.cs
new file mode 100644
index 00000000..2ad475bd
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Identity/TenantService.cs
@@ -0,0 +1,35 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using WTA.Application.Identity.Entities.Tenants;
+using WTA.Shared.Attributes;
+using WTA.Shared.Data;
+using WTA.Shared.Tenants;
+
+namespace WTA.Application.Identity;
+
+[Implement]
+public class TenantService : ITenantService
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public string? TenantId { get; set; }
+
+ public TenantService(IHttpContextAccessor httpContextAccessor, IServiceProvider serviceProvider)
+ {
+ this._serviceProvider = serviceProvider;
+ this.TenantId = httpContextAccessor.HttpContext?.User.Claims.FirstOrDefault(o => o.Type == "TenantId")?.Value;
+ }
+
+ public string? GetConnectionString(string connectionStringName)
+ {
+ using var scope = this._serviceProvider.CreateScope();
+ var repository = scope.ServiceProvider.GetRequiredService>();
+ repository.DisableTenantFilter();
+ return repository
+ .AsNoTracking()
+ .Where(o => o.Number == this.TenantId)
+ .SelectMany(o => o.ConnectionStrings)
+ .FirstOrDefault(o => o.Name == connectionStringName)
+ ?.Value;
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Localization/LocalizationController.cs b/docs/demo/src/WTA.Application/Localization/LocalizationController.cs
new file mode 100644
index 00000000..c524412c
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Localization/LocalizationController.cs
@@ -0,0 +1,46 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+using WTA.Shared.Controllers;
+
+namespace WTA.Application.Localization;
+
+[Route("api/[controller]")]
+public class LocalizationController : BaseController
+{
+ private readonly IStringLocalizer _localizer;
+ private readonly RequestLocalizationOptions _options;
+
+ public LocalizationController(IOptions options, IStringLocalizer localizer)
+ {
+ this._options = options.Value;
+ this._localizer = localizer;
+ }
+
+ [HttpGet]
+ [AllowAnonymous]
+ public IActionResult Index(string? culture)
+ {
+ if (culture != null)
+ {
+ Thread.CurrentThread.CurrentCulture = this._options.SupportedCultures!.First(o => o.Name == culture);
+ }
+ var result = new
+ {
+ Options = this._options.SupportedUICultures?
+ .Select(o => new { Value = o.Name, Label = o.NativeName })
+ .ToList(),
+ Locale = Thread.CurrentThread.CurrentCulture.Name,
+ Messages = new Dictionary(),
+ };
+ foreach (var item in this._options.SupportedUICultures!)
+ {
+ Thread.CurrentThread.CurrentCulture = item;
+ result.Messages.Add(item.Name, this._localizer.GetAllStrings().ToDictionary(o => o.Name, o => o.Value));
+ }
+ return Json(result);
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Monitor/Controllers/Monitor.cs b/docs/demo/src/WTA.Application/Monitor/Controllers/Monitor.cs
new file mode 100644
index 00000000..b717405d
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Monitor/Controllers/Monitor.cs
@@ -0,0 +1,12 @@
+using WTA.Shared.Application;
+using WTA.Shared.Attributes;
+
+namespace WTA.Application.Monitor.Controllers;
+
+[Hidden]
+[Order(3)]
+[SystemMonitor]
+[Component("monitor")]
+public class Monitor : IResource
+{
+}
diff --git a/docs/demo/src/WTA.Application/Monitor/Controllers/MonitorController.cs b/docs/demo/src/WTA.Application/Monitor/Controllers/MonitorController.cs
new file mode 100644
index 00000000..de027a60
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Monitor/Controllers/MonitorController.cs
@@ -0,0 +1,30 @@
+using Microsoft.AspNetCore.Mvc;
+using WTA.Shared.Application;
+using WTA.Shared.Extensions;
+using WTA.Shared.Monitor;
+
+namespace WTA.Application.Monitor.Controllers;
+
+[ApiExplorerSettings(GroupName = nameof(MonitorModule))]
+[Route("api/{culture}/system-monitor/[controller]/[action]")]
+public class MonitorController : Controller, IResourceService
+{
+ private readonly IMonitorService _monitorService;
+
+ public MonitorController(IMonitorService monitorService)
+ {
+ this._monitorService = monitorService;
+ }
+
+ [HttpGet]
+ public IActionResult Index()
+ {
+ return Json(typeof(MonitorModel).GetMetadataForType());
+ }
+
+ [HttpPost]
+ public IActionResult Index(MonitorModel model)
+ {
+ return Json(this._monitorService.GetStatus());
+ }
+}
diff --git a/docs/demo/src/WTA.Application/Monitor/Controllers/SystemMonitorAttribute.cs b/docs/demo/src/WTA.Application/Monitor/Controllers/SystemMonitorAttribute.cs
new file mode 100644
index 00000000..f2cd76df
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Monitor/Controllers/SystemMonitorAttribute.cs
@@ -0,0 +1,8 @@
+using WTA.Shared.Attributes;
+
+namespace WTA.Application.Monitor.Controllers;
+
+[Order(2)]
+public class SystemMonitorAttribute : GroupAttribute
+{
+}
diff --git a/docs/demo/src/WTA.Application/Monitor/Entities/JobItem.cs b/docs/demo/src/WTA.Application/Monitor/Entities/JobItem.cs
new file mode 100644
index 00000000..f8ae42d8
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Monitor/Entities/JobItem.cs
@@ -0,0 +1,15 @@
+using WTA.Application.Monitor.Controllers;
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Monitor.Entities;
+
+[Hidden]
+[Order(2)]
+[SystemMonitor]
+public class JobItem : BaseEntity
+{
+ public string Name { get; set; } = null!;
+ public string Cron { get; set; } = null!;
+ public string Service { get; set; } = null!;
+}
diff --git a/docs/demo/src/WTA.Application/Monitor/Entities/UserLogin.cs b/docs/demo/src/WTA.Application/Monitor/Entities/UserLogin.cs
new file mode 100644
index 00000000..a69e9dc9
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Monitor/Entities/UserLogin.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel.DataAnnotations;
+using WTA.Application.Monitor.Controllers;
+using WTA.Shared.Attributes;
+using WTA.Shared.Domain;
+
+namespace WTA.Application.Monitor.Entities;
+
+[Hidden]
+[Order(1)]
+[SystemMonitor]
+public class UserLogin : BaseEntity
+{
+ public string ConnectionId { get; set; } = null!;
+ public string UserName { get; set; } = null!;
+
+ [Required]
+ public DateTime? Login { get; set; }
+
+ public DateTime? Logout { get; set; }
+
+ [Required]
+ public bool? IsOnline { get; set; }
+
+ public DateTime? Heartbeat { get; set; }
+ public string? UserAgent { get; set; }
+}
diff --git a/docs/demo/src/WTA.Application/Monitor/MonitorModule.cs b/docs/demo/src/WTA.Application/Monitor/MonitorModule.cs
new file mode 100644
index 00000000..7998a82e
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Monitor/MonitorModule.cs
@@ -0,0 +1,9 @@
+using WTA.Shared.Attributes;
+using WTA.Shared.Module;
+
+namespace WTA.Application.Monitor;
+
+[Order(100)]
+public class MonitorModule : BaseModule
+{
+}
diff --git a/docs/demo/src/WTA.Application/Monitor/UserLoginSrevice.cs b/docs/demo/src/WTA.Application/Monitor/UserLoginSrevice.cs
new file mode 100644
index 00000000..f8127246
--- /dev/null
+++ b/docs/demo/src/WTA.Application/Monitor/UserLoginSrevice.cs
@@ -0,0 +1,60 @@
+using WTA.Application.Monitor.Entities;
+using WTA.Shared.Data;
+using WTA.Shared.EventBus;
+using WTA.Shared.Job;
+using WTA.Shared.Mappers;
+using WTA.Shared.SignalR;
+
+namespace WTA.Application.Monitor;
+
+public class UserLoginSrevice : IEventHander,
+ IEventHander,
+ IEventHander,
+ IEventHander,
+ IJobService
+{
+ private readonly IRepository _repository;
+
+ public UserLoginSrevice(IRepository repository)
+ {
+ this._repository = repository;
+ }
+
+ public Task Handle(SignalRConnectedEvent data)
+ {
+ var entity = new UserLogin().FromModel(data);
+ entity.IsOnline = true;
+ this._repository.Insert(entity);
+ this._repository.SaveChanges();
+ return Task.CompletedTask;
+ }
+
+ public Task Handle(SignalRDisconnectedEvent data)
+ {
+ var entity = this._repository.Queryable().FirstOrDefault(o => o.ConnectionId == data.ConnectionId);
+ if (entity != null)
+ {
+ entity.FromObject(data);
+ entity.IsOnline = false;
+ this._repository.SaveChanges();
+ }
+ return Task.CompletedTask;
+ }
+
+ public Task Handle(SignalRHeartbeatEvent data)
+ {
+ this._repository.Update(o => o.SetProperty(c => c.Heartbeat, DateTime.UtcNow), o => o.ConnectionId == data.ConnectionId);
+ return Task.CompletedTask;
+ }
+
+ public Task Handle(SignalCommandREvent data)
+ {
+ return Task.CompletedTask;
+ }
+
+ public void Invoke()
+ {
+ var time = DateTime.UtcNow.AddMinutes(-1);
+ this._repository.Update(o => o.SetProperty(c => c.IsOnline, false).SetProperty(c => c.Logout, DateTime.UtcNow), o => o.Heartbeat == null || o.Heartbeat < time);
+ }
+}
diff --git a/docs/demo/src/WTA.Application/WTA.Application.csproj b/docs/demo/src/WTA.Application/WTA.Application.csproj
new file mode 100644
index 00000000..bd1bfde4
--- /dev/null
+++ b/docs/demo/src/WTA.Application/WTA.Application.csproj
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/docs/demo/src/WTA.Infrastructure/Resources/en.json b/docs/demo/src/WTA.Infrastructure/Resources/en.json
new file mode 100644
index 00000000..1ab9c343
--- /dev/null
+++ b/docs/demo/src/WTA.Infrastructure/Resources/en.json
@@ -0,0 +1,16 @@
+{
+ "Application": "WTA Framework",
+ "Copyright": "all rights reserved © copyright",
+ "Test": "test",
+ "Login": "Login",
+ "Logout": "Logout",
+ "ConfirmLogout": "Confirm Logout?",
+ "Register": "Register",
+ "Tip": "Tip",
+ "Cancel": "Cancel",
+ "UserName": "User Name",
+ "Password": "Password",
+ "RememberMe": "Remember Me",
+ "ResetPassword": "Reset Password",
+ "UserCenter": "User Center"
+}
diff --git a/docs/demo/src/WTA.Infrastructure/Resources/zh.json b/docs/demo/src/WTA.Infrastructure/Resources/zh.json
new file mode 100644
index 00000000..38e894dc
--- /dev/null
+++ b/docs/demo/src/WTA.Infrastructure/Resources/zh.json
@@ -0,0 +1,130 @@
+{
+ "Application": "WTA开发框架",
+ "Copyright": "版权所有 © copyright ",
+ "Test": "测试",
+ "CompareAttribute": "{0}”和{1}不匹配",
+ "FileExtensionsAttribute": "{0}只接受一下扩展名的文件: {1}",
+ "MaxLengthAttribute": "{0}的最大长度为 {1}",
+ "MinLengthAttribute": "{0}的最小长度为 {1}",
+ "RangeAttribute": "{0}必需在 {1} 和 {2} 之间",
+ "RegularExpressionAttribute": "{0}”必需匹配{1}",
+ "RequiredAttribute": "{0}不能为空",
+ "StringLengthAttribute": "{0}的最大长度为 {1}",
+ "StringLengthAttributeIncludingMinimum": "{0}的长度在 {2} 和 {1} 之间",
+ "DataTypeAttribute_CreditCard": "{0}不是有效的信用卡号码",
+ "DataTypeAttribute_EmailAddress": "{0}不是有效的 Email 地址",
+ "DataTypeAttribute_PhoneNumber": "{0}不是有效的手机号码",
+ "DataTypeAttribute_Url": "{0}不是有效的 Url",
+ "DataTypeAttribute_Upload": "{0}的扩展名必须为:{1}",
+ "DataTypeAttribute_DateTime": "{0}不是有效的日期格式",
+ "CustomValidationAttribute": "{0}验证失败",
+ "ValidationAttribute": "{0}验证失败",
+ "True": "是",
+ "False": "否",
+ "Select": "选择",
+ "Confirm": "确定",
+ "Reset": "重置",
+ "RowIndex": "行号",
+ "Name": "名称",
+ "Number": "编号",
+ "Value": "值",
+ "Order": "序号",
+ "IsDisabled": "禁用",
+ "Properties": "属性",
+ "ParentId": "上级",
+ "LockoutEnabled": "启用锁定",
+ "LockoutEnd": "锁定截止",
+ "AccessFailedCount": "登录失败次数",
+ "IsSystem": "系统内置",
+ "IsReadonly": "只读",
+ "Audit": "审计",
+ "SelectAll": "全选",
+ "SelectInverse": "反选",
+ "Filter": "过滤",
+ "CreatedOn": "创建时间",
+ "CreatedBy": "创建人",
+ "UpdatedOn": "修改时间",
+ "UpdatedBy": "修改人",
+ "DeletedOn": "删除时间",
+ "DeletedBy": "删除人",
+ "ConcurrencyStamp": "并发戳",
+ "Operations": "操作",
+ "Disabled": "已禁用",
+ "DisplayOrder": "序号",
+ "IsDeleted": "已删除",
+ "Path": "路径",
+ "Method": "方法",
+ "IsTop": "顶部",
+ "HTMLClass": "class",
+ "InternalPath": "内部路径",
+ "Component": "组件",
+ "ServerTime": "服务器时间",
+ "OSArchitecture": "系统架构",
+ "OSDescription": "操作系统",
+ "ProcessArchitecture": "进程架构",
+ "Tip": "提示",
+ "Cancel": "操作取消",
+ "Index": "查询",
+ "Details": "详情",
+ "Create": "新建",
+ "Update": "更新",
+ "Import": "导入",
+ "Export": "导出",
+ "Remove": "移除",
+ "Restore": "还原",
+ "Delete": "删除",
+ "Authenticate": "验证",
+ "LoginModel": "登录",
+ "Login": "登录",
+ "Logout": "注销",
+ "ConfirmLogout": "确认退出?",
+ "Register": "注册",
+ "UserName": "用户名",
+ "Password": "密码",
+ "Email": "邮箱",
+ "EmailConfirmed": "邮箱已确认",
+ "RememberMe": "记住我",
+ "ResetPassword": "重置密码",
+ "UserCenter": "用户中心",
+ "Avatar": "头像",
+ "Tenant": "租户",
+ "ConnectionString": "连接字符串",
+ "TenantId": "租户",
+ "Tenants": "租户管理",
+ "Identity": "认证中心",
+ "SystemManagement": "基础数据",
+ "RoleId": "角色",
+ "PermissionId": "权限",
+ "UserRoles": "用户角色",
+ "EnableColumnLimit": "列权限",
+ "EnableRowLimit": "行权限",
+ "RolePermissions": "角色权限",
+ "DepartmentId": "部门",
+ "Cron": "定时器",
+ "Icon": "图标",
+ "Type": "类型",
+ "IsExternal": "外链",
+ "IsHidden": "隐藏",
+ "Redirect": "跳转",
+ "Columns": "列",
+ "IdentityModule": "系统管理",
+ "User": "用户",
+ "Role": "角色",
+ "Permission": "权限",
+ "Department": "部门",
+ "Post": "岗位",
+ "Dict": "字典",
+ "SystemMonitor": "系统监控",
+ "Monitor": "服务监控",
+ "MonitorModule": "系统监控",
+ "JobItem": "定时任务",
+ "Captcha": "验证码",
+ "CaptchaExpired": "验证码已过期",
+ "CaptchaError": "验证码错误",
+ "DictionaryItem": "数据字典",
+ "UserLogin": "登录历史",
+ "ConnectionId": "连接Id",
+ "IsOnline": "在线",
+ "Heartbeat": "心跳",
+ "UserAgent": "用户代理"
+}
diff --git a/docs/demo/src/WTA.Infrastructure/WTA.Infrastructure.csproj b/docs/demo/src/WTA.Infrastructure/WTA.Infrastructure.csproj
new file mode 100644
index 00000000..f18c156c
--- /dev/null
+++ b/docs/demo/src/WTA.Infrastructure/WTA.Infrastructure.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/demo/src/WTA.Shared/Application/IExportModel.cs b/docs/demo/src/WTA.Shared/Application/IExportModel.cs
new file mode 100644
index 00000000..00de67a7
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Application/IExportModel.cs
@@ -0,0 +1,5 @@
+namespace WTA.Shared.Application;
+
+public interface IExportModel where TEntity : class
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Application/IImportModel.cs b/docs/demo/src/WTA.Shared/Application/IImportModel.cs
new file mode 100644
index 00000000..b2d5d242
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Application/IImportModel.cs
@@ -0,0 +1,5 @@
+namespace WTA.Shared.Application;
+
+public interface IImportModel where TEntity : class
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Application/IResource.cs b/docs/demo/src/WTA.Shared/Application/IResource.cs
new file mode 100644
index 00000000..49b32bdb
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Application/IResource.cs
@@ -0,0 +1,5 @@
+namespace WTA.Shared.Application;
+
+public interface IResource
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Application/IResourceService.cs b/docs/demo/src/WTA.Shared/Application/IResourceService.cs
new file mode 100644
index 00000000..e6515a0c
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Application/IResourceService.cs
@@ -0,0 +1,5 @@
+namespace WTA.Shared.Application;
+
+public interface IResourceService where TResource : IResource
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Application/PaginationModel.cs b/docs/demo/src/WTA.Shared/Application/PaginationModel.cs
new file mode 100644
index 00000000..6a4a0565
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Application/PaginationModel.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel;
+using WTA.Shared.Domain;
+
+namespace WTA.Shared.Application;
+
+public class PaginationModel
+{
+ public PaginationModel()
+ {
+ }
+
+ [DefaultValue(1)]
+ public int PageIndex { get; set; } = 1;
+
+ [DefaultValue(20)]
+ public int PageSize { get; set; } = 20;
+
+ public string? OrderBy { get; set; } = $"{nameof(BaseEntity.Order)},{nameof(BaseEntity.CreatedOn)}";
+ public int TotalCount { get; set; }
+ public List Items { get; set; } = new List();
+ public TSearchModel Query { get; set; } = Activator.CreateInstance();
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/AddOnlyAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/AddOnlyAttribute.cs
new file mode 100644
index 00000000..38e02f4a
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/AddOnlyAttribute.cs
@@ -0,0 +1,6 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class AddOnlyAttribute : Attribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/ComponentAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/ComponentAttribute.cs
new file mode 100644
index 00000000..3f35ae81
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/ComponentAttribute.cs
@@ -0,0 +1,12 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class ComponentAttribute : Attribute
+{
+ public ComponentAttribute(string? component = null)
+ {
+ this.Component = component;
+ }
+
+ public string? Component { get; }
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/DbContextAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/DbContextAttribute.cs
new file mode 100644
index 00000000..263cb6dd
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/DbContextAttribute.cs
@@ -0,0 +1,5 @@
+namespace WTA.Shared.Attributes;
+
+public class DbContextAttribute : GenericAttribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/DisplayOnlyAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/DisplayOnlyAttribute.cs
new file mode 100644
index 00000000..b6135d73
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/DisplayOnlyAttribute.cs
@@ -0,0 +1,6 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class DisplayOnlyAttribute : Attribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/ExpressionAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/ExpressionAttribute.cs
new file mode 100644
index 00000000..f1c43ef0
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/ExpressionAttribute.cs
@@ -0,0 +1,12 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Field)]
+public class ExpressionAttribute : Attribute
+{
+ public ExpressionAttribute(string expression)
+ {
+ this.Expression = expression;
+ }
+
+ public string Expression { get; }
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/GenericAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/GenericAttribute.cs
new file mode 100644
index 00000000..b094122f
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/GenericAttribute.cs
@@ -0,0 +1,12 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class GenericAttribute : Attribute, ITypeAttribute
+{
+ public Type Type => typeof(T);
+}
+
+public interface ITypeAttribute
+{
+ Type Type { get; }
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/GroupAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/GroupAttribute.cs
new file mode 100644
index 00000000..d6e16fcd
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/GroupAttribute.cs
@@ -0,0 +1,6 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class GroupAttribute : Attribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/HiddenAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/HiddenAttribute.cs
new file mode 100644
index 00000000..8abe2cc7
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/HiddenAttribute.cs
@@ -0,0 +1,6 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
+public class HiddenAttribute : Attribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/HtmlClassAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/HtmlClassAttribute.cs
new file mode 100644
index 00000000..15566cbb
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/HtmlClassAttribute.cs
@@ -0,0 +1,13 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Method)]
+public class HtmlClassAttribute : Attribute
+{
+ public HtmlClassAttribute(string @class)
+ {
+ this.Class = @class;
+ }
+
+ public static string Default { get; } = "el-button--primary";
+ public string? Class { get; } = Default;
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/IconAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/IconAttribute.cs
new file mode 100644
index 00000000..d59dae0c
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/IconAttribute.cs
@@ -0,0 +1,14 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class IconAttribute : Attribute
+{
+ public IconAttribute(string icon)
+ {
+ this.Icon = icon;
+ }
+
+ public static string File { get; } = "file";
+ public static string Folder { get; } = "folder";
+ public string? Icon { get; } = File;
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/IgnoreMultiTenancyAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/IgnoreMultiTenancyAttribute.cs
new file mode 100644
index 00000000..162e8eef
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/IgnoreMultiTenancyAttribute.cs
@@ -0,0 +1,6 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class IgnoreMultiTenancyAttribute : Attribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/IgnoreUpdateAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/IgnoreUpdateAttribute.cs
new file mode 100644
index 00000000..57be59d7
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/IgnoreUpdateAttribute.cs
@@ -0,0 +1,6 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class IgnoreUpdateAttribute : Attribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/ImplementAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/ImplementAttribute.cs
new file mode 100644
index 00000000..45adf5f7
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/ImplementAttribute.cs
@@ -0,0 +1,26 @@
+using Microsoft.Extensions.DependencyInjection;
+using WTA.Shared.DependencyInjection;
+
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+public class ImplementAttribute : Attribute, IImplementAttribute
+{
+ public ImplementAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient, PlatformType platformType = PlatformType.All)
+ {
+ this.ServiceType = typeof(T);
+ this.Lifetime = lifetime;
+ this.PlatformType = platformType;
+ }
+
+ public ServiceLifetime Lifetime { get; set; }
+ public PlatformType PlatformType { get; set; }
+ public Type ServiceType { get; }
+}
+
+public interface IImplementAttribute
+{
+ ServiceLifetime Lifetime { get; }
+ PlatformType PlatformType { get; }
+ Type ServiceType { get; }
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/ImporterHeaderAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/ImporterHeaderAttribute.cs
new file mode 100644
index 00000000..ba72fb38
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/ImporterHeaderAttribute.cs
@@ -0,0 +1,11 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class ImporterHeaderAttribute : Attribute
+{
+ public string Name { get; set; } = null!;
+ public bool IsIgnore { get; set; }
+ public bool IsAllowRepeat { get; set; }
+ public string ShowInputMessage { get; set; } = null!;
+ public string Format { get; set; } = null!;
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/ModuleAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/ModuleAttribute.cs
new file mode 100644
index 00000000..bf8259c2
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/ModuleAttribute.cs
@@ -0,0 +1,5 @@
+namespace WTA.Shared.Attributes;
+
+public class ModuleAttribute : GenericAttribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/MultipleAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/MultipleAttribute.cs
new file mode 100644
index 00000000..75e6da2c
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/MultipleAttribute.cs
@@ -0,0 +1,6 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Method)]
+public class MultipleAttribute : Attribute
+{
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/NavigationAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/NavigationAttribute.cs
new file mode 100644
index 00000000..5ebae6b0
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/NavigationAttribute.cs
@@ -0,0 +1,12 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class NavigationAttribute : Attribute
+{
+ public NavigationAttribute(string? path = null)
+ {
+ this.Path = path;
+ }
+
+ public string? Path { get; }
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/OperatorTypeAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/OperatorTypeAttribute.cs
new file mode 100644
index 00000000..308dc596
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/OperatorTypeAttribute.cs
@@ -0,0 +1,16 @@
+using WTA.Shared.Data;
+
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
+public class OperatorTypeAttribute : Attribute
+{
+ public OperatorTypeAttribute(OperatorType operatorType, string? propertyName = null)
+ {
+ this.OperatorType = operatorType;
+ this.PropertyName = propertyName;
+ }
+
+ public OperatorType OperatorType { get; }
+ public string? PropertyName { get; }
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/OptionsAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/OptionsAttribute.cs
new file mode 100644
index 00000000..af5307b2
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/OptionsAttribute.cs
@@ -0,0 +1,12 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class OptionsAttribute : Attribute
+{
+ public OptionsAttribute(string? section = null)
+ {
+ this.Section = section;
+ }
+
+ public string? Section { get; }
+}
diff --git a/docs/demo/src/WTA.Shared/Attributes/OrderAttribute.cs b/docs/demo/src/WTA.Shared/Attributes/OrderAttribute.cs
new file mode 100644
index 00000000..cca7c194
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Attributes/OrderAttribute.cs
@@ -0,0 +1,13 @@
+namespace WTA.Shared.Attributes;
+
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
+public class OrderAttribute : Attribute
+{
+ public OrderAttribute(int order)
+ {
+ this.Order = order;
+ }
+
+ public static int Default { get; }
+ public int? Order { get; } = Default;
+}
diff --git a/docs/demo/src/WTA.Shared/Authentication/AuthenticateResult.cs b/docs/demo/src/WTA.Shared/Authentication/AuthenticateResult.cs
new file mode 100644
index 00000000..0d1831dc
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Authentication/AuthenticateResult.cs
@@ -0,0 +1,11 @@
+namespace WTA.Shared.Authentication;
+
+public class AuthenticateResult
+{
+ public bool Succeeded { get; set; }
+ public bool Failed { get; set; }
+ public bool EnableColumnLimit { get; set; }
+ public bool EnableRowLimit { get; set; }
+ public List Columns { get; set; } = new List();
+ public List Rows { get; set; } = new List();
+}
diff --git a/docs/demo/src/WTA.Shared/Authentication/CustomClaimsPrincipal.cs b/docs/demo/src/WTA.Shared/Authentication/CustomClaimsPrincipal.cs
new file mode 100644
index 00000000..b88dab76
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Authentication/CustomClaimsPrincipal.cs
@@ -0,0 +1,44 @@
+using System.Security.Claims;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using WTA.Shared.Extensions;
+
+namespace WTA.Shared.Authentication;
+
+public class CustomClaimsPrincipal : ClaimsPrincipal
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public CustomClaimsPrincipal(IServiceProvider serviceProvider, ClaimsPrincipal claimsPrincipal) : base(claimsPrincipal)
+ {
+ this._serviceProvider = serviceProvider;
+ }
+
+ public AuthenticateResult? Result { get; private set; }
+
+ public override bool IsInRole(string role)
+ {
+ var permissionService = this._serviceProvider.GetService();
+ if (permissionService != null)
+ {
+ // 优先使用本地验证
+ this.Result = permissionService.Authenticate(this.Identity?.Name!, role);
+ }
+ else
+ {
+ var configuration = this._serviceProvider.GetRequiredService();
+ var authServer = configuration.GetValue("AuthServer") ?? throw new ArgumentException($"AuthServer 未配置");
+ var url = $"{authServer.TrimEnd('/')}/user/is-in-role";
+ var httpClientFactory = this._serviceProvider.GetRequiredService();
+ var client = httpClientFactory.CreateClient();
+ var data = new Dictionary
+ {
+ { "name", this.Identity?.Name! },
+ { "role", role },
+ };
+ var response = client.PostAsync(url, new FormUrlEncodedContent(data)).Result;
+ this.Result = response.Content.ReadAsStringAsync().Result.FromJson()!;
+ }
+ return this.Result.Succeeded;
+ }
+}
diff --git a/docs/demo/src/WTA.Shared/Authentication/CustomJwtBearerPostConfigureOptions.cs b/docs/demo/src/WTA.Shared/Authentication/CustomJwtBearerPostConfigureOptions.cs
new file mode 100644
index 00000000..d389ec18
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Authentication/CustomJwtBearerPostConfigureOptions.cs
@@ -0,0 +1,21 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.Options;
+
+namespace WTA.Shared.Authentication;
+
+public class CustomJwtBearerPostConfigureOptions : JwtBearerPostConfigureOptions, IPostConfigureOptions
+{
+ private readonly CustomJwtSecurityTokenHandler _customJwtSecurityTokenHandler;
+
+ public CustomJwtBearerPostConfigureOptions(CustomJwtSecurityTokenHandler customJwtSecurityTokenHandler)
+ {
+ this._customJwtSecurityTokenHandler = customJwtSecurityTokenHandler;
+ }
+
+ public new void PostConfigure(string? name, JwtBearerOptions options)
+ {
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(this._customJwtSecurityTokenHandler);
+ base.PostConfigure(name, options);
+ }
+}
diff --git a/docs/demo/src/WTA.Shared/Authentication/CustomJwtSecurityTokenHandler.cs b/docs/demo/src/WTA.Shared/Authentication/CustomJwtSecurityTokenHandler.cs
new file mode 100644
index 00000000..1dfcbd9a
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Authentication/CustomJwtSecurityTokenHandler.cs
@@ -0,0 +1,20 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using Microsoft.IdentityModel.Tokens;
+
+namespace WTA.Shared.Authentication;
+
+public class CustomJwtSecurityTokenHandler : JwtSecurityTokenHandler
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public CustomJwtSecurityTokenHandler(IServiceProvider serviceProvider)
+ {
+ this._serviceProvider = serviceProvider;
+ }
+
+ public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
+ {
+ return new CustomClaimsPrincipal(this._serviceProvider, base.ValidateToken(token, validationParameters, out validatedToken));
+ }
+}
diff --git a/docs/demo/src/WTA.Shared/Authentication/IAuthenticationService.cs b/docs/demo/src/WTA.Shared/Authentication/IAuthenticationService.cs
new file mode 100644
index 00000000..d1f5e061
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Authentication/IAuthenticationService.cs
@@ -0,0 +1,6 @@
+namespace WTA.Shared.Authentication;
+
+public interface IAuthenticationService
+{
+ AuthenticateResult Authenticate(string name, string operation);
+}
diff --git a/docs/demo/src/WTA.Shared/Authentication/IdentityOptions.cs b/docs/demo/src/WTA.Shared/Authentication/IdentityOptions.cs
new file mode 100644
index 00000000..817574ba
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Authentication/IdentityOptions.cs
@@ -0,0 +1,14 @@
+namespace WTA.Shared.Authentication;
+
+public class IdentityOptions
+{
+ public const string Position = "Identity";
+ public string Issuer { get; set; } = "value";
+ public string Audience { get; set; } = "value";
+ public string Key { get; set; } = "0123456789abcdef0123456789abcdef";
+ public TimeSpan AccessTokenExpires { get; set; } = TimeSpan.FromMinutes(10);
+ public TimeSpan RefreshTokenExpires { get; set; } = TimeSpan.FromDays(14);
+ public bool SupportsUserLockout { get; set; } = true;
+ public int MaxFailedAccessAttempts { get; set; } = 5;
+ public TimeSpan DefaultLockoutTimeSpan { get; set; } = TimeSpan.FromMinutes(10);
+}
diff --git a/docs/demo/src/WTA.Shared/Captcha/CaptchaService.cs b/docs/demo/src/WTA.Shared/Captcha/CaptchaService.cs
new file mode 100644
index 00000000..2b4dfac4
--- /dev/null
+++ b/docs/demo/src/WTA.Shared/Captcha/CaptchaService.cs
@@ -0,0 +1,20 @@
+using SkiaSharp;
+using WTA.Shared.Attributes;
+
+namespace WTA.Shared.Captcha;
+
+[Implement